(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 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
+ 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":
+ * foo test bar X
+ *
foo bar X
+ *
foo bar X
+ *
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 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 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::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;
diff --git a/kate/xmltools/plugin_katexmltools.h b/kate/xmltools/plugin_katexmltools.h
new file mode 100644
index 0000000..9be61c7
--- /dev/null
+++ b/kate/xmltools/plugin_katexmltools.h
@@ -0,0 +1,150 @@
+ /***************************************************************************
+ pluginKatexmltools.cpp
+ copyright : (C) 2001-2002 by Daniel Naber
+ email : daniel.naber@t-online.de
+ ***************************************************************************/
+
+/***************************************************************************
+ 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.
+ ***************************************************************************/
+
+#ifndef _PLUGIN_KANT_XMLTOOLS_H
+#define _PLUGIN_KANT_XMLTOOLS_H
+
+#include "pseudo_dtd.h"
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+#include
+#include
+#include
+
+class PluginKateXMLTools : public Kate::Plugin, Kate::PluginViewInterface
+{
+
+ Q_OBJECT
+
+ public:
+
+ PluginKateXMLTools( QObject* parent = 0, const char* name = 0, const QStringList& = QStringList() );
+ virtual ~PluginKateXMLTools();
+ void addView ( Kate::MainWindow *win );
+ void removeView( Kate::MainWindow *win );
+
+
+ public slots:
+
+ void getDTD();
+
+ void slotInsertElement();
+ void slotCloseElement();
+ void filterInsertString( KTextEditor::CompletionEntry *ce, QString *str );
+ void completionDone( KTextEditor::CompletionEntry completionEntry );
+ void completionAborted();
+
+ void slotFinished( KIO::Job *job );
+ void slotData( KIO::Job *, const QByteArray &data );
+
+ void backspacePressed();
+ void emptyKeyEvent();
+ void keyEvent( int, int, const QString & );
+
+ /// Connected to the document manager, to manage the dtd collection.
+ void slotDocumentDeleted( uint n );
+
+ protected:
+
+ static QStringList sortQStringList( QStringList list );
+ //bool eventFilter( QObject *object, QEvent *event );
+
+ QString insideTag( Kate::View &kv );
+ QString insideAttribute( Kate::View &kv );
+
+ bool isOpeningTag( QString tag );
+ bool isClosingTag( QString tag );
+ bool isEmptyTag( QString tag );
+ bool isQuote( QString ch );
+
+ QString getParentElement( Kate::View &view, bool ignoreSingleBracket );
+
+ enum Mode {none, entities, attributevalues, attributes, elements};
+ enum PopupMode {noPopup, tagname, attributename, attributevalue, entityname};
+
+ QValueList stringListToCompletionEntryList( QStringList list );
+
+ /// Assign the PseudoDTD @p dtd to the Kate::Document @p doc
+ void assignDTD( PseudoDTD *dtd, KTextEditor::Document *doc );
+
+ /// temporary placeholder for the metaDTD file
+ QString m_dtdString;
+ /// temporary placeholder for the document to assign a DTD to while the file is loaded
+ KTextEditor::Document *m_docToAssignTo;
+ /// URL of the last loaded meta DTD
+ QString m_urlString;
+
+ uint m_lastLine, m_lastCol;
+ QStringList m_lastAllowed;
+ int m_popupOpenCol;
+
+ Mode m_mode;
+ int m_correctPos;
+
+ // code completion stuff:
+ KTextEditor::CodeCompletionInterface* m_codeInterface;
+
+ /// maps KTE::Document::docNumber -> DTD
+ QIntDict m_docDtds;
+
+ /// maps DTD filename -> DTD
+ QDict m_dtds;
+
+ QPtrList m_views;
+
+ void connectSlots( Kate::View *kv );
+ void disconnectSlots( Kate::View *kv );
+
+ Kate::DocumentManager *m_documentManager;
+};
+
+class InsertElement : public KDialogBase
+{
+
+ Q_OBJECT
+
+ public:
+ InsertElement( QWidget *parent, const char *name );
+ ~InsertElement();
+ QString showDialog( QStringList &completions );
+ private slots:
+ void slotHistoryTextChanged( const QString& );
+
+};
+
+#endif // _PLUGIN_KANT_XMLTOOLS_H
+// kate: space-indent on; indent-width 2; replace-tabs on; mixed-indent off;
diff --git a/kate/xmltools/pseudo_dtd.cpp b/kate/xmltools/pseudo_dtd.cpp
new file mode 100644
index 0000000..b5c9cd1
--- /dev/null
+++ b/kate/xmltools/pseudo_dtd.cpp
@@ -0,0 +1,466 @@
+/***************************************************************************
+ pseudoDtd.cpp
+ copyright : (C) 2001-2002 by Daniel Naber
+ email : daniel.naber@t-online.de
+ ***************************************************************************/
+
+/***************************************************************************
+ 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.
+ ***************************************************************************/
+
+#include "pseudo_dtd.h"
+
+#include
+
+#include
+#include
+
+#include
+#include
+
+PseudoDTD::PseudoDTD()
+{
+ // "SGML support" only means case-insensivity, because HTML is case-insensitive up to version 4:
+ m_sgmlSupport = true; // TODO: make this an run-time option ( maybe automatically set )
+}
+
+PseudoDTD::~PseudoDTD()
+{
+}
+
+void PseudoDTD::analyzeDTD( QString &metaDtdUrl, QString &metaDtd )
+{
+ QDomDocument doc( "dtdIn_xml" );
+ if ( ! doc.setContent( metaDtd) )
+ {
+ KMessageBox::error(0, i18n("The file '%1' could not be parsed. "
+ "Please check that the file is well-formed XML.").arg( metaDtdUrl ),
+ i18n( "XML Plugin Error") );
+ return;
+ }
+
+ if ( doc.doctype().name() != "dtd" )
+ {
+ KMessageBox::error(0, i18n("The file '%1' is not in the expected format. "
+ "Please check that the file is of this type:\n"
+ "-//Norman Walsh//DTD DTDParse V2.0//EN\n"
+ "You can produce such files with dtdparse. "
+ "See the Kate Plugin documentation for more information.").arg( metaDtdUrl ),
+ i18n("XML Plugin Error") );
+ return;
+ }
+
+ uint listLength = 0;
+ listLength += doc.elementsByTagName( "entity" ).count();
+ listLength += doc.elementsByTagName( "element" ).count();
+ // count this twice, as it will be iterated twice ( TODO: optimize that? ):
+ listLength += doc.elementsByTagName( "attlist" ).count() * 2;
+
+ QProgressDialog progress( i18n("Analyzing meta DTD..."), i18n("Cancel"), listLength,
+ 0, "progress", TRUE );
+ progress.setMinimumDuration( 400 );
+ progress.setProgress(0);
+
+ // Get information from meta DTD and put it in Qt data structures for fast access:
+ if( ! parseEntities( &doc, &progress ) )
+ return;
+
+ if( ! parseElements( &doc, &progress ) )
+ return;
+
+ if( ! parseAttributes( &doc, &progress ) )
+ return;
+
+ if( ! parseAttributeValues( &doc, &progress ) )
+ return;
+
+ progress.setProgress( listLength ); // just to make sure the dialog disappears
+
+}
+
+// ========================================================================
+// DOM stuff:
+
+/**
+ * Iterate through the XML to get a mapping which sub-elements are allowed for
+ * all elements.
+ */
+bool PseudoDTD::parseElements( QDomDocument *doc, QProgressDialog *progress )
+{
+
+ m_elementsList.clear();
+ // We only display a list, i.e. we pretend that the content model is just
+ // a set, so we use a map. This is necessay e.g. for xhtml 1.0's head element,
+ // which would otherwise display some elements twice.
+ QMap subelementList; // the bool is not used
+
+ QDomNodeList list = doc->elementsByTagName( "element" );
+ uint listLength = list.count(); // speedup (really! )
+
+ for( uint i = 0; i < listLength; i++ )
+ {
+ if( progress->wasCancelled() )
+ return false;
+
+ progress->setProgress( progress->progress()+1 );
+ // FIXME!:
+ //qApp->processEvents();
+
+ subelementList.clear();
+ QDomNode node = list.item( i );
+ QDomElement elem = node.toElement();
+
+ if( !elem.isNull() )
+ {
+ // Enter the expanded content model, which may also include stuff not allowed.
+ // We do not care if it's a or whatever.
+ QDomNodeList contentModelList = elem.elementsByTagName( "content-model-expanded" );
+ QDomNode contentModelNode = contentModelList.item(0);
+ QDomElement contentModelElem = contentModelNode.toElement();
+ if( ! contentModelElem.isNull() )
+ {
+ // check for :
+ QDomNodeList pcdataList = contentModelElem.elementsByTagName( "pcdata" );
+
+ // check for other sub elements:
+ QDomNodeList subList = contentModelElem.elementsByTagName( "element-name" );
+ uint subListLength = subList.count();
+ for( uint l = 0; l < subListLength; l++ )
+ {
+ QDomNode subNode = subList.item(l);
+ QDomElement subElem = subNode.toElement();
+ if( !subElem.isNull() )
+ subelementList[subElem.attribute( "name" )] = true;
+ }
+
+ // anders: check if this is an EMPTY element, and put "__EMPTY" in the
+ // sub list, so that we can insert tags in empty form if required.
+ QDomNodeList emptyList = elem.elementsByTagName( "empty" );
+ if ( emptyList.count() )
+ subelementList["__EMPTY"] = true;
+ }
+
+ // Now remove the elements not allowed (e.g. is explicitely not allowed in
+ // in the HTML 4.01 Strict DTD):
+ QDomNodeList exclusionsList = elem.elementsByTagName( "exclusions" );
+ if( exclusionsList.length() > 0 )
+ { // sometimes there are no exclusions ( e.g. in XML DTDs there are never exclusions )
+ QDomNode exclusionsNode = exclusionsList.item(0);
+ QDomElement exclusionsElem = exclusionsNode.toElement();
+ if( ! exclusionsElem.isNull() )
+ {
+ QDomNodeList subList = exclusionsElem.elementsByTagName( "element-name" );
+ uint subListLength = subList.count();
+ for( uint l = 0; l < subListLength; l++ )
+ {
+ QDomNode subNode = subList.item(l);
+ QDomElement subElem = subNode.toElement();
+ if( !subElem.isNull() )
+ {
+ QMap::Iterator it = subelementList.find( subElem.attribute( "name" ) );
+ if( it != subelementList.end() )
+ subelementList.remove(it);
+ }
+ }
+ }
+ }
+
+ // turn the map into a list:
+ QStringList subelementListTmp;
+ QMap::Iterator it;
+ for( it = subelementList.begin(); it != subelementList.end(); ++it )
+ subelementListTmp.append( it.key() );
+
+ m_elementsList.insert( elem.attribute( "name" ), subelementListTmp );
+
+ }
+
+ } // end iteration over all nodes
+ return true;
+}
+
+/**
+ * Check which elements are allowed inside a parent element. This returns
+ * a list of allowed elements, but it doesn't care about order or if only a certain
+ * number of occurences is allowed.
+ */
+QStringList PseudoDTD::allowedElements( QString parentElement )
+{
+ if( m_sgmlSupport )
+ {
+ // find the matching element, ignoring case:
+ QMap::Iterator it;
+ for( it = m_elementsList.begin(); it != m_elementsList.end(); ++it )
+ {
+ if( it.key().lower() == parentElement.lower() )
+ return it.data();
+ }
+ }
+ else if( m_elementsList.contains(parentElement) )
+ return m_elementsList[parentElement];
+
+ return QStringList();
+}
+
+/**
+ * Iterate through the XML to get a mapping which attributes are allowed inside
+ * all elements.
+ */
+bool PseudoDTD::parseAttributes( QDomDocument *doc, QProgressDialog *progress )
+{
+ m_attributesList.clear();
+// QStringList allowedAttributes;
+ QDomNodeList list = doc->elementsByTagName( "attlist" );
+ uint listLength = list.count();
+
+ for( uint i = 0; i < listLength; i++ )
+ {
+ if( progress->wasCancelled() )
+ return false;
+
+ progress->setProgress( progress->progress()+1 );
+ // FIXME!!
+ //qApp->processEvents();
+
+ ElementAttributes attrs;
+ QDomNode node = list.item(i);
+ QDomElement elem = node.toElement();
+ if( !elem.isNull() )
+ {
+ QDomNodeList attributeList = elem.elementsByTagName( "attribute" );
+ uint attributeListLength = attributeList.count();
+ for( uint l = 0; l < attributeListLength; l++ )
+ {
+ QDomNode attributeNode = attributeList.item(l);
+ QDomElement attributeElem = attributeNode.toElement();
+
+ if( ! attributeElem.isNull() )
+ {
+ if ( attributeElem.attribute("type") == "#REQUIRED" )
+ attrs.requiredAttributes.append( attributeElem.attribute("name") );
+ else
+ attrs.optionalAttributes.append( attributeElem.attribute("name") );
+ }
+ }
+ m_attributesList.insert( elem.attribute("name"), attrs );
+ }
+ }
+
+ return true;
+}
+
+/** Check which attributes are allowed for an element.
+ */
+QStringList PseudoDTD::allowedAttributes( QString element )
+{
+ if( m_sgmlSupport )
+ {
+ // find the matching element, ignoring case:
+ QMap::Iterator it;
+ for( it = m_attributesList.begin(); it != m_attributesList.end(); ++it ) {
+ if( it.key().lower() == element.lower() ) {
+ return it.data().optionalAttributes + it.data().requiredAttributes;
+ }
+ }
+ }
+ else if( m_attributesList.contains(element) )
+ return m_attributesList[element].optionalAttributes + m_attributesList[element].requiredAttributes;
+
+ return QStringList();
+}
+
+QStringList PseudoDTD::requiredAttributes( const QString &element ) const
+{
+ if ( m_sgmlSupport )
+ {
+ QMap::ConstIterator it;
+ for( it = m_attributesList.begin(); it != m_attributesList.end(); ++it )
+ {
+ if( it.key().lower() == element.lower() )
+ return it.data().requiredAttributes;
+ }
+ }
+ else if( m_attributesList.contains(element) )
+ return m_attributesList[element].requiredAttributes;
+
+ return QStringList();
+}
+
+/**
+ * Iterate through the XML to get a mapping which attribute values are allowed
+ * for all attributes inside all elements.
+ */
+bool PseudoDTD::parseAttributeValues( QDomDocument *doc, QProgressDialog *progress )
+{
+ m_attributevaluesList.clear(); // 1 element : n possible attributes
+ QMap attributevaluesTmp; // 1 attribute : n possible values
+ QDomNodeList list = doc->elementsByTagName( "attlist" );
+ uint listLength = list.count();
+
+ for( uint i = 0; i < listLength; i++ )
+ {
+ if( progress->wasCancelled() )
+ return false;
+
+ progress->setProgress( progress->progress()+1 );
+ // FIXME!
+ //qApp->processEvents();
+
+ attributevaluesTmp.clear();
+ QDomNode node = list.item(i);
+ QDomElement elem = node.toElement();
+ if( !elem.isNull() )
+ {
+ // Enter the list of :
+ QDomNodeList attributeList = elem.elementsByTagName( "attribute" );
+ uint attributeListLength = attributeList.count();
+ for( uint l = 0; l < attributeListLength; l++ )
+ {
+ QDomNode attributeNode = attributeList.item(l);
+ QDomElement attributeElem = attributeNode.toElement();
+ if( ! attributeElem.isNull() )
+ {
+ QString value = attributeElem.attribute( "value" );
+ attributevaluesTmp.insert( attributeElem.attribute("name"), QStringList::split(QRegExp(" "), value) );
+ }
+ }
+ m_attributevaluesList.insert( elem.attribute("name"), attributevaluesTmp );
+ }
+ }
+ return true;
+}
+
+/**
+ * Check which attributes values are allowed for an attribute in an element
+ * (the element is necessary because e.g. "href" inside could be different
+ * to an "href" inside ):
+ */
+QStringList PseudoDTD::attributeValues( QString element, QString attribute )
+{
+ // Direct access would be faster than iteration of course but not always correct,
+ // because we need to be case-insensitive.
+ if( m_sgmlSupport ) {
+ // first find the matching element, ignoring case:
+ QMap< QString,QMap >::Iterator it;
+ for( it = m_attributevaluesList.begin(); it != m_attributevaluesList.end(); ++it )
+ {
+ if( it.key().lower() == element.lower() )
+ {
+ QMap attrVals = it.data();
+ QMap::Iterator itV;
+ // then find the matching attribute for that element, ignoring case:
+ for( itV = attrVals.begin(); itV != attrVals.end(); ++itV )
+ {
+ if( itV.key().lower() == attribute.lower() )
+ return( itV.data() );
+ }
+ }
+ }
+ }
+ else if( m_attributevaluesList.contains(element) )
+ {
+ QMap attrVals = m_attributevaluesList[element];
+ if( attrVals.contains(attribute) )
+ return attrVals[attribute];
+ }
+
+ // no predefined values available:
+ return QStringList();
+}
+
+/**
+ * Iterate through the XML to get a mapping of all entity names and their expanded
+ * version, e.g. nbsp => . Parameter entities are ignored.
+ */
+bool PseudoDTD::parseEntities( QDomDocument *doc, QProgressDialog *progress )
+{
+ m_entityList.clear();
+ QDomNodeList list = doc->elementsByTagName( "entity" );
+ uint listLength = list.count();
+
+ for( uint i = 0; i < listLength; i++ )
+ {
+ if( progress->wasCancelled() )
+ return false;
+
+ progress->setProgress( progress->progress()+1 );
+ //FIXME!!
+ //qApp->processEvents();
+ QDomNode node = list.item(i);
+ QDomElement elem = node.toElement();
+ if( !elem.isNull()
+ && elem.attribute( "type" ) != "param" )
+ { // TODO: what's cdata <-> gen ?
+ QDomNodeList expandedList = elem.elementsByTagName( "text-expanded" );
+ QDomNode expandedNode = expandedList.item(0);
+ QDomElement expandedElem = expandedNode.toElement();
+ if( ! expandedElem.isNull() )
+ {
+ QString exp = expandedElem.text();
+ // TODO: support more than one ...; in the expanded text
+ /* TODO include do this when the unicode font problem is solved:
+ if( exp.contains(QRegExp("^[a-zA-Z0-9]+;$")) ) {
+ // hexadecimal numbers, e.g. "ȶ"
+ uint end = exp.find( ";" );
+ exp = exp.mid( 3, end-3 );
+ exp = QChar();
+ } else if( exp.contains(QRegExp("^[0-9]+;$")) ) {
+ // decimal numbers, e.g. "ì"
+ uint end = exp.find( ";" );
+ exp = exp.mid( 2, end-2 );
+ exp = QChar( exp.toInt() );
+ }
+ */
+ m_entityList.insert( elem.attribute("name"), exp );
+ }
+ else
+ {
+ m_entityList.insert( elem.attribute("name"), QString() );
+ }
+ }
+ }
+ return true;
+}
+
+/**
+ * Get a list of all ( non-parameter ) entities that start with a certain string.
+ */
+QStringList PseudoDTD::entities( QString start )
+{
+ QStringList entities;
+ QMap::Iterator it;
+ for( it = m_entityList.begin(); it != m_entityList.end(); ++it ) {
+ if( (*it).startsWith(start) )
+ {
+ QString str = it.key();
+ /* TODO: show entities as unicode character
+ if( !it.data().isEmpty() ) {
+ //str += " -- " + it.data();
+ QRegExp re( "(\\d+);" );
+ if( re.search(it.data()) != -1 ) {
+ uint ch = re.cap( 1).toUInt();
+ str += " -- " + QChar( ch).decomposition();
+ }
+ //kdDebug() << "#" << it.data() << endl;
+ }
+ */
+ entities.append( str );
+ // TODO: later use a table view
+ }
+ }
+ return entities;
+}
+
+// kate: space-indent on; indent-width 2; replace-tabs on; mixed-indent off;
diff --git a/kate/xmltools/pseudo_dtd.h b/kate/xmltools/pseudo_dtd.h
new file mode 100644
index 0000000..f5cf37e
--- /dev/null
+++ b/kate/xmltools/pseudo_dtd.h
@@ -0,0 +1,76 @@
+ /***************************************************************************
+ pseudoDtd.cpp
+ copyright : (C) 2001-2002 by Daniel Naber
+ email : daniel.naber@t-online.de
+ ***************************************************************************/
+
+/***************************************************************************
+ 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.
+ ***************************************************************************/
+
+#ifndef _PLUGIN_KANT_XMLTOOLS_DTD_H
+#define _PLUGIN_KANT_XMLTOOLS_DTD_H
+
+#include
+#include
+
+/**
+ * This class contains the attributes for one element.
+ * To get ALL attributes, concatenate the two lists.
+ */
+class ElementAttributes
+{
+ public:
+ QStringList optionalAttributes;
+ QStringList requiredAttributes;
+};
+
+class PseudoDTD
+{
+
+ public:
+ PseudoDTD();
+ ~PseudoDTD();
+
+ void analyzeDTD( QString &metaDtdUrl, QString &metaDtd );
+
+ QStringList allowedElements( QString parentElement );
+ QStringList allowedAttributes( QString parentElement );
+ QStringList attributeValues( QString element, QString attribute );
+ QStringList entities( QString start );
+ QStringList requiredAttributes( const QString &parentElement ) const;
+
+ protected:
+
+ bool parseElements( QDomDocument *doc, QProgressDialog *progress );
+ bool parseAttributes( QDomDocument *doc, QProgressDialog *progress );
+ bool parseAttributeValues( QDomDocument *doc, QProgressDialog *progress );
+ bool parseEntities( QDomDocument *doc, QProgressDialog *progress );
+
+ bool m_sgmlSupport;
+
+ // Entities, e.g. <"nbsp", "160">
+ QMap m_entityList;
+ // Elements, e.g. <"a", ( "b", "i", "em", "strong" )>
+ QMap m_elementsList;
+ // Attributes e.g. <"a", ( "href", "lang", "title" )>
+ QMap m_attributesList;
+ // Attribute values e.g. <"td", <"align", ( "left", "right", "justify" )>>
+ QMap< QString,QMap > m_attributevaluesList;
+
+};
+
+#endif // _PLUGIN_KANT_XMLTOOLS_DTD_H
+// kate: space-indent on; indent-width 2; replace-tabs on; mixed-indent off;
diff --git a/kate/xmltools/simplify_dtd.xsl b/kate/xmltools/simplify_dtd.xsl
new file mode 100644
index 0000000..53d3a13
--- /dev/null
+++ b/kate/xmltools/simplify_dtd.xsl
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ XSLT Simplifier
+ (original version)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/kate/xmltools/testcases.xml b/kate/xmltools/testcases.xml
new file mode 100644
index 0000000..aa1e7dd
--- /dev/null
+++ b/kate/xmltools/testcases.xml
@@ -0,0 +1,73 @@
+This is a pseudo XML file to test the functions of the XML Plugin.
+v0.8, 2002-04-21
+
+1. Choose "Assign Meta DTD..." and load the file meta-dtd/html4-loose.dtd.xml
+
+2. Place cursor at the "X" and choose "Close Element" from the
+"XML Plugin" menu. "
" resp. "