diff options
Diffstat (limited to 'kate/xmltools/plugin_katexmltools.cpp')
-rw-r--r-- | kate/xmltools/plugin_katexmltools.cpp | 1141 |
1 files changed, 1141 insertions, 0 deletions
diff --git a/kate/xmltools/plugin_katexmltools.cpp b/kate/xmltools/plugin_katexmltools.cpp new file mode 100644 index 0000000..05f7802 --- /dev/null +++ b/kate/xmltools/plugin_katexmltools.cpp @@ -0,0 +1,1141 @@ +/*************************************************************************** + pluginKatexmltools.cpp + + List elements, attributes, attribute values and entities allowed by DTD. + Needs a DTD in XML format ( as produced by dtdparse ) for most features. + + copyright : ( C ) 2001-2002 by Daniel Naber + email : daniel.naber@t-online.de + + Copyright (C) 2005 by Anders Lund <anders@alweb.dk> + ***************************************************************************/ + +/*************************************************************************** + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or ( at your option ) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + ***************************************************************************/ + +/* +README: +The basic idea is this: certain keyEvents(), namely [<&" ], trigger a completion box. +This is intended as a help for editing. There are some cases where the XML +spec is not followed, e.g. one can add the same attribute twice to an element. +Also see the user documentation. If backspace is pressed after a completion popup +was closed, the popup will re-open. This way typos can be corrected and the popup +will reappear, which is quite comfortable. + +FIXME for jowenn if he has time: +-Ctrl-Z doesn't work if completion is visible +-Typing with popup works, but right/left cursor keys and start/end don't, i.e. + they should be ignored by the completion ( ? ) +-popup not completely visible if it's long and appears at the bottom of the screen + +FIXME: +-( docbook ) <author lang="">: insert space between the quotes, press "de" and return -> only "d" inserted +-Correctly support more than one view: + charactersInteractivelyInserted( ..) is tied to kv->document() + but filterInsertString( .. ) is tied to kv +-The "Insert Element" dialog isn't case insensitive, but it should be +-fix upper/lower case problems ( start typing lowercase if the tag etc. is upper case ) +-See the "fixme"'s in the code + +TODO: +-check for mem leaks +-add "Go to opening/parent tag"? +-check doctype to get top-level element +-can undo behaviour be improved?, e.g. the plugins internal deletions of text + don't have to be an extra step +-don't offer entities if inside tag but outside attribute value + +-Support for more than one namespace at the same time ( e.g. XSLT + XSL-FO )? +=>This could also be handled in the XSLT DTD fragment, as described in the XSLT 1.0 spec, + but then at <xsl:template match="/"><html> it will only show you HTML elements! +=>So better "Assign meta DTD" and "Add meta DTD", the latter will expand the current meta DTD +-Option to insert empty element in <empty/> form +-Show expanded entities with QChar::QChar( int rc ) + unicode font +-Don't ignore entities defined in the document's prologue +-Only offer 'valid' elements, i.e. don't take the elements as a set but check + if the DTD is matched ( order, number of occurences, ... ) + +-Maybe only read the meta DTD file once, then store the resulting QMap on disk ( using QDataStream )? + We'll then have to compare timeOf_cacheFile <-> timeOf_metaDtd. +-Try to use libxml +*/ + +#include "plugin_katexmltools.h" +#include "plugin_katexmltools.moc" + +#include <assert.h> + +#include <qdatetime.h> +#include <qdom.h> +#include <qfile.h> +#include <qlayout.h> +#include <qlistbox.h> +#include <qprogressdialog.h> +#include <qpushbutton.h> +#include <qregexp.h> +#include <qstring.h> +#include <qtimer.h> + +#include <kaction.h> +#include <kbuttonbox.h> +#include <klineedit.h> +#include <kcursor.h> +#include <kdebug.h> +#include <kfiledialog.h> +#include <kglobal.h> +#include <kinstance.h> +#include <kio/job.h> +#include <klocale.h> +#include <kmessagebox.h> +#include <kstandarddirs.h> +#include <kgenericfactory.h> + +K_EXPORT_COMPONENT_FACTORY( katexmltoolsplugin, KGenericFactory<PluginKateXMLTools>( "katexmltools" ) ) + +class PluginView : public KXMLGUIClient +{ + friend class PluginKateXMLTools; + + public: + Kate::MainWindow *win; +}; + +PluginKateXMLTools::PluginKateXMLTools( QObject* parent, const char* name, const QStringList& ) + : Kate::Plugin ( (Kate::Application*)parent, name ) +{ + //kdDebug() << "PluginKateXMLTools constructor called" << endl; + + m_dtdString = QString(); + m_urlString = QString(); + m_docToAssignTo = 0L; + + m_mode = none; + m_correctPos = 0; + + m_lastLine = 0; + m_lastCol = 0; + m_lastAllowed = QStringList(); + m_popupOpenCol = -1; + + m_dtds.setAutoDelete( true ); + + m_documentManager = ((Kate::Application*)parent)->documentManager(); + +// connect( m_documentManager, SIGNAL(documentCreated()), +// this, SLOT(slotDocumentCreated()) ); + connect( m_documentManager, SIGNAL(documentDeleted(uint)), + this, SLOT(slotDocumentDeleted(uint)) ); +} + +PluginKateXMLTools::~PluginKateXMLTools() +{ + //kdDebug() << "xml tools descructor 1..." << endl; +} + +void PluginKateXMLTools::addView( Kate::MainWindow *win ) +{ + // TODO: doesn't this have to be deleted? + PluginView *view = new PluginView (); + ( void) new KAction ( i18n("&Insert Element..."), CTRL+Key_Return, this, + SLOT( slotInsertElement()), view->actionCollection(), "xml_tool_insert_element" ); + ( void) new KAction ( i18n("&Close Element"), CTRL+Key_Less, this, + SLOT( slotCloseElement()), view->actionCollection(), "xml_tool_close_element" ); + ( void) new KAction ( i18n("Assign Meta &DTD..." ), 0, this, + SLOT( getDTD()), view->actionCollection(), "xml_tool_assign" ); + + view->setInstance( new KInstance("kate") ); + view->setXMLFile( "plugins/katexmltools/ui.rc" ); + win->guiFactory()->addClient( view ); + + view->win = win; + m_views.append( view ); +} + +void PluginKateXMLTools::removeView( Kate::MainWindow *win ) +{ + for ( uint z=0; z < m_views.count(); z++ ) + { + if ( m_views.at(z)->win == win ) + { + PluginView *view = m_views.at( z ); + m_views.remove ( view ); + win->guiFactory()->removeClient( view ); + delete view; + } + } +} + +void PluginKateXMLTools::slotDocumentDeleted( uint documentNumber ) +{ + // Remove the document from m_DTDs, and also delete the PseudoDTD + // if it becomes unused. + if ( m_docDtds[ documentNumber ] ) + { + kdDebug()<<"XMLTools:slotDocumentDeleted: documents: "<<m_docDtds.count()<<", DTDs: "<<m_dtds.count()<<endl; + PseudoDTD *dtd = m_docDtds.take( documentNumber ); + + QIntDictIterator<PseudoDTD> it ( m_docDtds ); + for ( ; it.current(); ++it ) + { + if ( it.current() == dtd ) + return; + } + + QDictIterator<PseudoDTD> it1( m_dtds ); + for ( ; it1.current() ; ++it1 ) + { + if ( it1.current() == dtd ) + { + m_dtds.remove( it1.currentKey() ); + return; + } + } + } +} + +void PluginKateXMLTools::backspacePressed() +{ + kdDebug() << "xml tools backspacePressed" << endl; + + if ( !application()->activeMainWindow() ) + return; + + Kate::View *kv = application()->activeMainWindow()->viewManager()->activeView(); + if( ! kv ) + { + kdDebug() << "Warning: no Kate::View" << endl; + return; + } + uint line, col; + kv->cursorPositionReal( &line, &col ); + + //kdDebug() << "++ redisplay popup? line:" << line << ", col: " << col << endl; + if( m_lastLine == line && col == m_lastCol ) + { + int len = col - m_popupOpenCol; + if( len < 0 ) + { + kdDebug() << "**Warning: len < 0" << endl; + return; + } + //kdDebug() << "++ redisplay popup, " << m_lastAllowed.count() << ", len:" << len <<endl; + connectSlots( kv ); + kv->showCompletionBox( stringListToCompletionEntryList(m_lastAllowed), len, false ); + } +} + +void PluginKateXMLTools::emptyKeyEvent() +{ + keyEvent( 0, 0, QString::null ); +} + +void PluginKateXMLTools::keyEvent( int, int, const QString &/*s*/ ) +{ + //kdDebug() << "xml tools keyEvent: '" << s << endl; + + if ( !application()->activeMainWindow() ) + return; + + Kate::View *kv = application()->activeMainWindow()->viewManager()->activeView(); + if( ! kv ) + { + kdDebug() << "Warning: no Kate::View" << endl; + return; + } + + uint docNumber = kv->document()->documentNumber(); + if( ! m_docDtds[ docNumber ] ) + // no meta DTD assigned yet + return; + + // debug to test speed: + //QTime t; t.start(); + + QStringList allowed = QStringList(); + + // get char on the left of the cursor: + uint line, col; + kv->cursorPositionReal( &line, &col ); + QString lineStr = kv->getDoc()->textLine( line ); + QString leftCh = lineStr.mid( col-1, 1 ); + QString secondLeftCh = lineStr.mid( col-2, 1 ); + + if( leftCh == "&" ) + { + kdDebug() << "Getting entities" << endl; + allowed = m_docDtds[docNumber]->entities("" ); + m_mode = entities; + } + else if( leftCh == "<" ) + { + kdDebug() << "*outside tag -> get elements" << endl; + QString parentElement = getParentElement( *kv, true ); + kdDebug() << "parent: " << parentElement << endl; + allowed = m_docDtds[docNumber]->allowedElements(parentElement ); + m_mode = elements; + } + // TODO: optionally close parent tag if not left=="/>" + else if( leftCh == " " || (isQuote(leftCh) && secondLeftCh == "=") ) + { + // TODO: check secondLeftChar, too?! then you don't need to trigger + // with space and we yet save CPU power + QString currentElement = insideTag( *kv ); + QString currentAttribute; + if( ! currentElement.isEmpty() ) + currentAttribute = insideAttribute( *kv ); + + kdDebug() << "Tag: " << currentElement << endl; + kdDebug() << "Attr: " << currentAttribute << endl; + + if( ! currentElement.isEmpty() && ! currentAttribute.isEmpty() ) + { + kdDebug() << "*inside attribute -> get attribute values" << endl; + allowed = m_docDtds[docNumber]->attributeValues(currentElement, currentAttribute ); + if( allowed.count() == 1 && + (allowed[0] == "CDATA" || allowed[0] == "ID" || allowed[0] == "IDREF" || + allowed[0] == "IDREFS" || allowed[0] == "ENTITY" || allowed[0] == "ENTITIES" || + allowed[0] == "NMTOKEN" || allowed[0] == "NMTOKENS" || allowed[0] == "NAME") ) + { + // these must not be taken literally, e.g. don't insert the string "CDATA" + allowed.clear(); + } + else + { + m_mode = attributevalues; + } + } + else if( ! currentElement.isEmpty() ) + { + kdDebug() << "*inside tag -> get attributes" << endl; + allowed = m_docDtds[docNumber]->allowedAttributes(currentElement ); + m_mode = attributes; + } + } + + //kdDebug() << "time elapsed (ms): " << t.elapsed() << endl; + //kdDebug() << "Allowed strings: " << allowed.count() << endl; + + if( allowed.count() >= 1 && allowed[0] != "__EMPTY" ) + { + allowed = sortQStringList( allowed ); + connectSlots( kv ); + kv->showCompletionBox( stringListToCompletionEntryList( allowed ), 0, false ); + m_popupOpenCol = col; + m_lastAllowed = allowed; + } + //else { + // m_lastAllowed.clear(); + //} +} + +QValueList<KTextEditor::CompletionEntry> +PluginKateXMLTools::stringListToCompletionEntryList( QStringList list ) +{ + QValueList<KTextEditor::CompletionEntry> compList; + KTextEditor::CompletionEntry entry; + for( QStringList::Iterator it = list.begin(); it != list.end(); ++it ) + { + entry.text = ( *it ); + compList << entry; + } + return compList; +} + + +/** + * disconnect all signals of a specified kateview from the local slots + * + */ +void PluginKateXMLTools::disconnectSlots( Kate::View *kv ) +{ + disconnect( kv, SIGNAL(filterInsertString(KTextEditor::CompletionEntry*,QString*)), this, 0 ); + disconnect( kv, SIGNAL(completionDone(KTextEditor::CompletionEntry)), this, 0 ); + disconnect( kv, SIGNAL(completionAborted()), this, 0 ); +} + +/** + * connect all signals of a specified kateview to the local slots + * + */ +void PluginKateXMLTools::connectSlots( Kate::View *kv ) +{ + connect( kv, SIGNAL(filterInsertString(KTextEditor::CompletionEntry*,QString*) ), + this, SLOT(filterInsertString(KTextEditor::CompletionEntry*,QString*)) ); + connect( kv, SIGNAL(completionDone(KTextEditor::CompletionEntry) ), + this, SLOT(completionDone(KTextEditor::CompletionEntry)) ); + connect( kv, SIGNAL(completionAborted()), this, SLOT(completionAborted()) ); +} + +/** + * Load the meta DTD. In case of success set the 'ready' + * flag to true, to show that we're is ready to give hints about the DTD. + */ +void PluginKateXMLTools::getDTD() +{ + if ( !application()->activeMainWindow() ) + return; + + Kate::View *kv = application()->activeMainWindow()->viewManager()->activeView(); + if( ! kv ) + { + kdDebug() << "Warning: no Kate::View" << endl; + return; + } + + // ### replace this with something more sane + // Start where the supplied XML-DTDs are fed by default unless + // user changed directory last time: + + QString defaultDir = KGlobal::dirs()->findResourceDir("data", "katexmltools/" ) + "katexmltools/"; + if( m_urlString.isNull() ) { + m_urlString = defaultDir; + } + KURL url; + + // Guess the meta DTD by looking at the doctype's public identifier. + // XML allows comments etc. before the doctype, so look further than + // just the first line. + // Example syntax: + // <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "DTD/xhtml1-transitional.dtd"> + uint checkMaxLines = 200; + QString documentStart = kv->getDoc()->text(0, 0, checkMaxLines+1, 0 ); + QRegExp re( "<!DOCTYPE\\s+(.*)\\s+PUBLIC\\s+[\"'](.*)[\"']", false ); + re.setMinimal( true ); + int matchPos = re.search( documentStart ); + QString filename; + QString doctype; + QString topElement; + + if( matchPos != -1 ) { + topElement = re.cap( 1 ); + doctype = re.cap( 2 ); + kdDebug() << "Top element: " << topElement << endl; + kdDebug() << "Doctype match: " << doctype << endl; + // XHTML: + if( doctype == "-//W3C//DTD XHTML 1.0 Transitional//EN" ) + filename = "xhtml1-transitional.dtd.xml"; + else if( doctype == "-//W3C//DTD XHTML 1.0 Strict//EN" ) + filename = "xhtml1-strict.dtd.xml"; + else if( doctype == "-//W3C//DTD XHTML 1.0 Frameset//EN" ) + filename = "xhtml1-frameset.dtd.xml"; + // HTML 4.0: + else if ( doctype == "-//W3C//DTD HTML 4.01 Transitional//EN" ) + filename = "html4-loose.dtd.xml"; + else if ( doctype == "-//W3C//DTD HTML 4.01//EN" ) + filename = "html4-strict.dtd.xml"; + // KDE Docbook: + else if ( doctype == "-//KDE//DTD DocBook XML V4.1.2-Based Variant V1.1//EN" ) + filename = "kde-docbook.dtd.xml"; + } + else if( documentStart.find("<xsl:stylesheet" ) != -1 && + documentStart.find( "xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\"") != -1 ) + { + /* XSLT doesn't have a doctype/DTD. We look for an xsl:stylesheet tag instead. + Example: + <xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns="http://www.w3.org/TR/xhtml1/strict"> + */ + filename = "xslt-1.0.dtd.xml"; + doctype = "XSLT 1.0"; + } + else + kdDebug() << "No doctype found" << endl; + + if( filename.isEmpty() ) + { + // no meta dtd found for this file + url = KFileDialog::getOpenURL(m_urlString, "*.xml", + 0, i18n( "Assign Meta DTD in XML Format") ); + } + else + { + url.setFileName( defaultDir + filename ); + KMessageBox::information(0, i18n("The current file has been identified " + "as a document of type \"%1\". The meta DTD for this document type " + "will now be loaded.").arg( doctype ), + i18n( "Loading XML Meta DTD" ), + QString::fromLatin1( "DTDAssigned") ); + } + + if( url.isEmpty() ) + return; + + m_urlString = url.url(); // remember directory for next time + + if ( m_dtds[ m_urlString ] ) + assignDTD( m_dtds[ m_urlString ], kv->document() ); + else + { + m_dtdString = ""; + m_docToAssignTo = kv->document(); + + QApplication::setOverrideCursor( KCursor::waitCursor() ); + KIO::Job *job = KIO::get( url ); + connect( job, SIGNAL(result(KIO::Job *)), this, SLOT(slotFinished(KIO::Job *)) ); + connect( job, SIGNAL(data(KIO::Job *, const QByteArray &)), + this, SLOT(slotData(KIO::Job *, const QByteArray &)) ); + } + kdDebug()<<"XMLTools::getDTD: Documents: "<<m_docDtds.count()<<", DTDs: "<<m_dtds.count()<<endl; +} + +void PluginKateXMLTools::slotFinished( KIO::Job *job ) +{ + if( job->error() ) + { + //kdDebug() << "XML Plugin error: DTD in XML format (" << filename << " ) could not be loaded" << endl; + job->showErrorDialog( 0 ); + } + else if ( static_cast<KIO::TransferJob *>(job)->isErrorPage() ) + { + // catch failed loading loading via http: + KMessageBox::error(0, i18n("The file '%1' could not be opened. " + "The server returned an error.").arg( m_urlString ), + i18n( "XML Plugin Error") ); + } + else + { + PseudoDTD *dtd = new PseudoDTD(); + dtd->analyzeDTD( m_urlString, m_dtdString ); + + m_dtds.insert( m_urlString, dtd ); + assignDTD( dtd, m_docToAssignTo ); + + // clean up a bit + m_docToAssignTo = 0; + m_dtdString = ""; + } + QApplication::restoreOverrideCursor(); +} + +void PluginKateXMLTools::slotData( KIO::Job *, const QByteArray &data ) +{ + m_dtdString += QString( data ); +} + +void PluginKateXMLTools::assignDTD( PseudoDTD *dtd, KTextEditor::Document *doc ) +{ + m_docDtds.replace( doc->documentNumber(), dtd ); + connect( doc, SIGNAL(charactersInteractivelyInserted(int,int,const QString&) ), + this, SLOT(keyEvent(int,int,const QString&)) ); + + disconnect( doc, SIGNAL(backspacePressed()), this, 0 ); + connect( doc, SIGNAL(backspacePressed() ), + this, SLOT(backspacePressed()) ); +} + +/** + * Offer a line edit with completion for possible elements at cursor position and insert the + * tag one chosen/entered by the user, plus its closing tag. If there's a text selection, + * add the markup around it. + */ +void PluginKateXMLTools::slotInsertElement() +{ + if ( !application()->activeMainWindow() ) + return; + + Kate::View *kv = application()->activeMainWindow()->viewManager()->activeView(); + if( ! kv ) + { + kdDebug() << "Warning: no Kate::View" << endl; + return; + } + + PseudoDTD *dtd = m_docDtds[kv->document()->documentNumber()]; + QString parentElement = getParentElement( *kv, false ); + QStringList allowed; + + if( dtd ) + allowed = dtd->allowedElements(parentElement ); + + InsertElement *dialog = new InsertElement( + ( QWidget *)application()->activeMainWindow()->viewManager()->activeView(), "insertXml" ); + QString text = dialog->showDialog( allowed ); + delete dialog; + + if( !text.isEmpty() ) + { + QStringList list = QStringList::split( ' ', text ); + QString pre; + QString post; + // anders: use <tagname/> if the tag is required to be empty. + // In that case maybe we should not remove the selection? or overwrite it? + int adjust = 0; // how much to move cursor. + // if we know that we have attributes, it goes + // just after the tag name, otherwise between tags. + if ( dtd && dtd->allowedAttributes(list[0]).count() ) + adjust++; // the ">" + + if ( dtd && dtd->allowedElements(list[0]).contains("__EMPTY") ) + { + pre = "<" + text + "/>"; + if ( adjust ) + adjust++; // for the "/" + } + else + { + pre = "<" + text + ">"; + post ="</" + list[0] + ">"; + } + + QString marked; + if ( ! post.isEmpty() ) + marked = kv->getDoc()->selection(); + + if( marked.length() > 0 ) + kv->getDoc()->removeSelectedText(); + + kv->insertText( pre + marked + post ); + } +} + +/** + * Insert a closing tag for the nearest not-closed parent element. + */ +void PluginKateXMLTools::slotCloseElement() +{ + if ( !application()->activeMainWindow() ) + return; + + Kate::View *kv = application()->activeMainWindow()->viewManager()->activeView(); + if( ! kv ) + { + kdDebug() << "Warning: no Kate::View" << endl; + return; + } + QString parentElement = getParentElement( *kv, false ); + + //kdDebug() << "parentElement: '" << parentElement << "'" << endl; + QString closeTag = "</" + parentElement + ">"; + if( ! parentElement.isEmpty() ) + kv->insertText( closeTag ); +} + +// modify the completion string before it gets inserted +void PluginKateXMLTools::filterInsertString( KTextEditor::CompletionEntry *ce, QString *text ) +{ + kdDebug() << "filterInsertString str: " << *text << endl; + kdDebug() << "filterInsertString text: " << ce->text << endl; + + if ( !application()->activeMainWindow() ) + return; + + Kate::View *kv = application()->activeMainWindow()->viewManager()->activeView(); + if( ! kv ) + { + kdDebug() << "Warning (filterInsertString() ): no Kate::View" << endl; + return; + } + + uint line, col; + kv->cursorPositionReal( &line, &col ); + QString lineStr = kv->getDoc()->textLine(line ); + QString leftCh = lineStr.mid( col-1, 1 ); + QString rightCh = lineStr.mid( col, 1 ); + + m_correctPos = 0; // where to move the cursor after completion ( >0 = move right ) + if( m_mode == entities ) + { + // This is a bit ugly, but entities are case-sensitive + // and we want the correct completion even if the user started typing + // e.g. in lower case but the entity is in upper case + kv->getDoc()->removeText( line, col - (ce->text.length() - text->length()), line, col ); + *text = ce->text + ";"; + } + + else if( m_mode == attributes ) + { + *text = *text + "=\"\""; + m_correctPos = -1; + if( !rightCh.isEmpty() && rightCh != ">" && rightCh != "/" && rightCh != " " ) + { // TODO: other whitespaces + // add space in front of the next attribute + *text = *text + " "; + m_correctPos--; + } + } + + else if( m_mode == attributevalues ) + { + // TODO: support more than one line + uint startAttValue = 0; + uint endAttValue = 0; + + // find left quote: + for( startAttValue = col; startAttValue > 0; startAttValue-- ) + { + QString ch = lineStr.mid( startAttValue-1, 1 ); + if( isQuote(ch) ) + break; + } + + // find right quote: + for( endAttValue = col; endAttValue <= lineStr.length(); endAttValue++ ) + { + QString ch = lineStr.mid( endAttValue-1, 1 ); + if( isQuote(ch) ) + break; + } + + // maybe the user has already typed something to trigger completion, + // don't overwrite that: + startAttValue += ce->text.length() - text->length(); + // delete the current contents of the attribute: + if( startAttValue < endAttValue ) + { + kv->getDoc()->removeText( line, startAttValue, line, endAttValue-1 ); + // FIXME: this makes the scrollbar jump + // but without it, inserting sometimes goes crazy :-( + kv->setCursorPositionReal( line, startAttValue ); + } + } + + else if( m_mode == elements ) + { + // anders: if the tag is marked EMPTY, insert in form <tagname/> + QString str; + int docNumber = kv->document()->documentNumber(); + bool isEmptyTag =m_docDtds[docNumber]->allowedElements(ce->text).contains( "__EMPTY" ); + if ( isEmptyTag ) + str = "/>"; + else + str = "></" + ce->text + ">"; + *text = *text + str; + + // Place the cursor where it is most likely wanted: + // allways inside the tag if the tag is empty AND the DTD indicates that there are attribs) + // outside for open tags, UNLESS there are mandatory attributes + if ( m_docDtds[docNumber]->requiredAttributes(ce->text).count() + || ( isEmptyTag && m_docDtds[docNumber]->allowedAttributes( ce->text).count() ) ) + m_correctPos = - str.length(); + else if ( ! isEmptyTag ) + m_correctPos = -str.length() + 1; + } +} + +static void correctPos( Kate::View *kv, int count ) +{ + if( count > 0 ) + { + for( int i = 0; i < count; i++ ) + kv->cursorRight(); + } + else if( count < 0 ) + { + for( int i = 0; i < -count; i++ ) + kv->cursorLeft(); + } +} + +void PluginKateXMLTools::completionAborted() +{ + if ( !application()->activeMainWindow() ) + return; + + Kate::View *kv = application()->activeMainWindow()->viewManager()->activeView(); + if( ! kv ) + { + kdDebug() << "Warning (completionAborted() ): no Kate::View" << endl; + return; + } + disconnectSlots( kv ); + kv->cursorPositionReal( &m_lastLine, &m_lastCol ); + m_lastCol--; + + correctPos( kv,m_correctPos ); + m_correctPos = 0; + + kdDebug() << "completionAborted() at line:" << m_lastLine << ", col:" << m_lastCol << endl; +} + +void PluginKateXMLTools::completionDone( KTextEditor::CompletionEntry ) +{ + kdDebug() << "completionDone()" << endl; + + if ( !application()->activeMainWindow() ) + return; + + Kate::View *kv = application()->activeMainWindow()->viewManager()->activeView(); + if( ! kv ) + { + kdDebug() << "Warning (completionDone() ): no Kate::View" << endl; + return; + } + disconnectSlots( kv ); + + correctPos( kv,m_correctPos ); + m_correctPos = 0; + + if( m_mode == attributes ) + { + // immediately show attribute values: + QTimer::singleShot( 10, this, SLOT(emptyKeyEvent()) ); + } + +} + +// ======================================================================== +// Pseudo-XML stuff: + +/** + * Check if cursor is inside a tag, that is + * if "<" occurs before ">" occurs ( on the left side of the cursor ). + * Return the tag name, return "" if we cursor is outside a tag. + */ +QString PluginKateXMLTools::insideTag( Kate::View &kv ) +{ + uint line = 0, col = 0; + kv.cursorPositionReal( &line, &col ); + int y = line; // another variable because uint <-> int + + do { + QString lineStr = kv.getDoc()->textLine(y ); + for( uint x = col; x > 0; x-- ) + { + QString ch = lineStr.mid( x-1, 1 ); + if( ch == ">" ) // cursor is outside tag + return ""; + + if( ch == "<" ) + { + QString tag; + // look for white space on the right to get the tag name + for( uint z = x; z <= lineStr.length() ; z++ ) + { + ch = lineStr.mid( z-1, 1 ); + if( ch.at(0).isSpace() || ch == "/" || ch == ">" ) + return tag.right( tag.length()-1 ); + + if( z == lineStr.length() ) + { + tag += ch; + return tag.right( tag.length()-1 ); + } + + tag += ch; + } + } + } + y--; + col = kv.getDoc()->textLine(y).length(); + } while( y >= 0 ); + + return ""; +} + +/** + * Check if cursor is inside an attribute value, that is + * if '="' is on the left, and if it's nearer than "<" or ">". + * + * @Return the attribute name or "" if we're outside an attribute + * value. + * + * Note: only call when insideTag() == true. + * TODO: allow whitespace around "=" + */ +QString PluginKateXMLTools::insideAttribute( Kate::View &kv ) +{ + uint line = 0, col = 0; + kv.cursorPositionReal( &line, &col ); + int y = line; // another variable because uint <-> int + uint x = 0; + QString lineStr = ""; + QString ch = ""; + + do { + lineStr = kv.getDoc()->textLine(y ); + for( x = col; x > 0; x-- ) + { + ch = lineStr.mid( x-1, 1 ); + QString chLeft = lineStr.mid( x-2, 1 ); + // TODO: allow whitespace + if( isQuote(ch) && chLeft == "=" ) + break; + else if( isQuote(ch) && chLeft != "=" ) + return ""; + else if( ch == "<" || ch == ">" ) + return ""; + } + y--; + col = kv.getDoc()->textLine(y).length(); + } while( !isQuote(ch) ); + + // look for next white space on the left to get the tag name + QString attr = ""; + for( int z = x; z >= 0; z-- ) + { + ch = lineStr.mid( z-1, 1 ); + + if( ch.at(0).isSpace() ) + break; + + if( z == 0 ) + { // start of line == whitespace + attr += ch; + break; + } + + attr = ch + attr; + } + + return attr.left( attr.length()-2 ); +} + +/** + * Find the parent element for the current cursor position. That is, + * go left and find the first opening element that's not closed yet, + * ignoring empty elements. + * Examples: If cursor is at "X", the correct parent element is "p": + * <p> <a x="xyz"> foo <i> test </i> bar </a> X + * <p> <a x="xyz"> foo bar </a> X + * <p> foo <img/> bar X + * <p> foo bar X + */ +QString PluginKateXMLTools::getParentElement( Kate::View &kv, bool ignoreSingleChar ) +{ + enum { + parsingText, + parsingElement, + parsingElementBoundary, + parsingNonElement, + parsingAttributeDquote, + parsingAttributeSquote, + parsingIgnore + } parseState; + parseState = ignoreSingleChar ? parsingIgnore : parsingText; + + int nestingLevel = 0; + + uint line, col; + kv.cursorPositionReal( &line, &col ); + QString str = kv.getDoc()->textLine(line ); + + while( true ) + { + // move left a character + if( !col-- ) + { + do + { + if( !line-- ) return QString::null; // reached start of document + str = kv.getDoc()->textLine(line ); + col = str.length(); + } while( !col ); + --col; + } + + ushort ch = str.at( col).unicode(); + + switch( parseState ) + { + case parsingIgnore: + parseState = parsingText; + break; + + case parsingText: + switch( ch ) + { + case '<': + // hmm... we were actually inside an element + return QString::null; + + case '>': + // we just hit an element boundary + parseState = parsingElementBoundary; + break; + } + break; + + case parsingElement: + switch( ch ) + { + case '"': // attribute ( double quoted ) + parseState = parsingAttributeDquote; + break; + + case '\'': // attribute ( single quoted ) + parseState = parsingAttributeSquote; + break; + + case '/': // close tag + parseState = parsingNonElement; + ++nestingLevel; + break; + + case '<': + // we just hit the start of the element... + if( nestingLevel-- ) break; + + QString tag = str.mid( col + 1 ); + for( uint pos = 0, len = tag.length(); pos < len; ++pos ) { + ch = tag.at( pos).unicode(); + if( ch == ' ' || ch == '\t' || ch == '>' ) { + tag.truncate( pos ); + break; + } + } + return tag; + } + break; + + case parsingElementBoundary: + switch( ch ) + { + case '?': // processing instruction + case '-': // comment + case '/': // empty element + parseState = parsingNonElement; + break; + + case '"': + parseState = parsingAttributeDquote; + break; + + case '\'': + parseState = parsingAttributeSquote; + break; + + case '<': // empty tag ( bad XML ) + parseState = parsingText; + break; + + default: + parseState = parsingElement; + } + break; + + case parsingAttributeDquote: + if( ch == '"' ) parseState = parsingElement; + break; + + case parsingAttributeSquote: + if( ch == '\'' ) parseState = parsingElement; + break; + + case parsingNonElement: + if( ch == '<' ) parseState = parsingText; + break; + } + } +} + +/** + * Return true if the tag is neither a closing tag + * nor an empty tag, nor a comment, nor processing instruction. + */ +bool PluginKateXMLTools::isOpeningTag( QString tag ) +{ + return ( !isClosingTag(tag) && !isEmptyTag(tag ) && + !tag.startsWith( "<?") && !tag.startsWith("<!") ); +} + +/** + * Return true if the tag is a closing tag. Return false + * if the tag is an opening tag or an empty tag ( ! ) + */ +bool PluginKateXMLTools::isClosingTag( QString tag ) +{ + return ( tag.startsWith("</") ); +} + +bool PluginKateXMLTools::isEmptyTag( QString tag ) +{ + return ( tag.right(2) == "/>" ); +} + +/** + * Return true if ch is a single or double quote. Expects ch to be of length 1. + */ +bool PluginKateXMLTools::isQuote( QString ch ) +{ + return ( ch == "\"" || ch == "'" ); +} + + +// ======================================================================== +// Tools: + +/** Sort a QStringList case-insensitively. Static. TODO: make it more simple. */ +QStringList PluginKateXMLTools::sortQStringList( QStringList list ) { + // Sort list case-insensitive. This looks complicated but using a QMap + // is even suggested by the Qt documentation. + QMap<QString,QString> mapList; + for ( QStringList::Iterator it = list.begin(); it != list.end(); ++it ) + { + QString str = *it; + if( mapList.contains(str.lower()) ) + { + // do not override a previous value, e.g. "Auml" and "auml" are two different + // entities, but they should be sorted next to each other. + // TODO: currently it's undefined if e.g. "A" or "a" comes first, it depends on + // the meta DTD ( really? it seems to work okay?!? ) + mapList[str.lower()+"_"] = str; + } + else + mapList[str.lower()] = str; + } + + list.clear(); + QMap<QString,QString>::Iterator it; + + // Qt doc: "the items are alphabetically sorted [by key] when iterating over the map": + for( it = mapList.begin(); it != mapList.end(); ++it ) + list.append( it.data() ); + + return list; +} + +//BEGIN InsertElement dialog +InsertElement::InsertElement( QWidget *parent, const char *name ) + :KDialogBase( parent, name, true, i18n("Insert XML Element" ), + KDialogBase::Ok|KDialogBase::Cancel) +{ +} + +InsertElement::~InsertElement() +{ +} + +void InsertElement::slotHistoryTextChanged( const QString& text ) +{ + enableButtonOK( !text.isEmpty() ); +} + +QString InsertElement::showDialog( QStringList &completions ) +{ + QWidget *page = new QWidget( this ); + setMainWidget( page ); + QVBoxLayout *topLayout = new QVBoxLayout( page, 0, spacingHint() ); + + KHistoryCombo *combo = new KHistoryCombo( page, "value" ); + combo->setHistoryItems( completions, true ); + connect( combo->lineEdit(), SIGNAL(textChanged ( const QString & )), + this, SLOT(slotHistoryTextChanged(const QString &)) ); + QString text = i18n( "Enter XML tag name and attributes (\"<\", \">\" and closing tag will be supplied):" ); + QLabel *label = new QLabel( text, page, "insert" ); + + topLayout->addWidget( label ); + topLayout->addWidget( combo ); + + combo->setFocus(); + slotHistoryTextChanged( combo->lineEdit()->text() ); + if( exec() ) + return combo->currentText(); + + return QString::null; +} +//END InsertElement dialog +// kate: space-indent on; indent-width 2; replace-tabs on; mixed-indent off; |