You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1141 lines
34 KiB
1141 lines
34 KiB
/***************************************************************************
|
|
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 TQChar::TQChar( 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 TQMap on disk ( using TQDataStream )?
|
|
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 <tqdatetime.h>
|
|
#include <tqdom.h>
|
|
#include <tqfile.h>
|
|
#include <tqlayout.h>
|
|
#include <tqlistbox.h>
|
|
#include <tqprogressdialog.h>
|
|
#include <tqpushbutton.h>
|
|
#include <tqregexp.h>
|
|
#include <tqstring.h>
|
|
#include <tqtimer.h>
|
|
|
|
#include <tdeaction.h>
|
|
#include <kbuttonbox.h>
|
|
#include <klineedit.h>
|
|
#include <kcursor.h>
|
|
#include <kdebug.h>
|
|
#include <tdefiledialog.h>
|
|
#include <tdeglobal.h>
|
|
#include <kinstance.h>
|
|
#include <tdeio/job.h>
|
|
#include <tdelocale.h>
|
|
#include <tdemessagebox.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( TQObject* parent, const char* name, const TQStringList& )
|
|
: Kate::Plugin ( (Kate::Application*)parent, name )
|
|
{
|
|
//kdDebug() << "PluginKateXMLTools constructor called" << endl;
|
|
|
|
m_dtdString = TQString();
|
|
m_urlString = TQString();
|
|
m_docToAssignTo = 0L;
|
|
|
|
m_mode = none;
|
|
m_correctPos = 0;
|
|
|
|
m_lastLine = 0;
|
|
m_lastCol = 0;
|
|
m_lastAllowed = TQStringList();
|
|
m_popupOpenCol = -1;
|
|
|
|
m_dtds.setAutoDelete( true );
|
|
|
|
m_documentManager = ((Kate::Application*)parent)->documentManager();
|
|
|
|
// connect( m_documentManager, TQ_SIGNAL(documentCreated()),
|
|
// this, TQ_SLOT(slotDocumentCreated()) );
|
|
connect( m_documentManager, TQ_SIGNAL(documentDeleted(uint)),
|
|
this, TQ_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 TDEAction ( i18n("&Insert Element..."), CTRL+Key_Return, this,
|
|
TQ_SLOT( slotInsertElement()), view->actionCollection(), "xml_tool_insert_element" );
|
|
( void) new TDEAction ( i18n("&Close Element"), CTRL+Key_Less, this,
|
|
TQ_SLOT( slotCloseElement()), view->actionCollection(), "xml_tool_close_element" );
|
|
( void) new TDEAction ( i18n("Assign Meta &DTD..." ), 0, this,
|
|
TQ_SLOT( getDTD()), view->actionCollection(), "xml_tool_assign" );
|
|
|
|
view->setInstance( new TDEInstance("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 );
|
|
|
|
TQIntDictIterator<PseudoDTD> it ( m_docDtds );
|
|
for ( ; it.current(); ++it )
|
|
{
|
|
if ( it.current() == dtd )
|
|
return;
|
|
}
|
|
|
|
TQDictIterator<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, TQString() );
|
|
}
|
|
|
|
void PluginKateXMLTools::keyEvent( int, int, const TQString &/*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:
|
|
//TQTime t; t.start();
|
|
|
|
TQStringList allowed = TQStringList();
|
|
|
|
// get char on the left of the cursor:
|
|
uint line, col;
|
|
kv->cursorPositionReal( &line, &col );
|
|
TQString lineStr = kv->getDoc()->textLine( line );
|
|
TQString leftCh = lineStr.mid( col-1, 1 );
|
|
TQString 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;
|
|
TQString 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
|
|
TQString currentElement = insideTag( *kv );
|
|
TQString 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 = sortTQStringList( allowed );
|
|
connectSlots( kv );
|
|
kv->showCompletionBox( stringListToCompletionEntryList( allowed ), 0, false );
|
|
m_popupOpenCol = col;
|
|
m_lastAllowed = allowed;
|
|
}
|
|
//else {
|
|
// m_lastAllowed.clear();
|
|
//}
|
|
}
|
|
|
|
TQValueList<KTextEditor::CompletionEntry>
|
|
PluginKateXMLTools::stringListToCompletionEntryList( TQStringList list )
|
|
{
|
|
TQValueList<KTextEditor::CompletionEntry> compList;
|
|
KTextEditor::CompletionEntry entry;
|
|
for( TQStringList::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, TQ_SIGNAL(filterInsertString(KTextEditor::CompletionEntry*,TQString*)), this, 0 );
|
|
disconnect( kv, TQ_SIGNAL(completionDone(KTextEditor::CompletionEntry)), this, 0 );
|
|
disconnect( kv, TQ_SIGNAL(completionAborted()), this, 0 );
|
|
}
|
|
|
|
/**
|
|
* connect all signals of a specified kateview to the local slots
|
|
*
|
|
*/
|
|
void PluginKateXMLTools::connectSlots( Kate::View *kv )
|
|
{
|
|
connect( kv, TQ_SIGNAL(filterInsertString(KTextEditor::CompletionEntry*,TQString*) ),
|
|
this, TQ_SLOT(filterInsertString(KTextEditor::CompletionEntry*,TQString*)) );
|
|
connect( kv, TQ_SIGNAL(completionDone(KTextEditor::CompletionEntry) ),
|
|
this, TQ_SLOT(completionDone(KTextEditor::CompletionEntry)) );
|
|
connect( kv, TQ_SIGNAL(completionAborted()), this, TQ_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:
|
|
|
|
TQString defaultDir = TDEGlobal::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;
|
|
TQString documentStart = kv->getDoc()->text(0, 0, checkMaxLines+1, 0 );
|
|
TQRegExp re( "<!DOCTYPE\\s+(.*)\\s+PUBLIC\\s+[\"'](.*)[\"']", false );
|
|
re.setMinimal( true );
|
|
int matchPos = re.search( documentStart );
|
|
TQString filename;
|
|
TQString doctype;
|
|
TQString 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" ),
|
|
TQString::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();
|
|
|
|
TQApplication::setOverrideCursor( KCursor::waitCursor() );
|
|
TDEIO::Job *job = TDEIO::get( url );
|
|
connect( job, TQ_SIGNAL(result(TDEIO::Job *)), this, TQ_SLOT(slotFinished(TDEIO::Job *)) );
|
|
connect( job, TQ_SIGNAL(data(TDEIO::Job *, const TQByteArray &)),
|
|
this, TQ_SLOT(slotData(TDEIO::Job *, const TQByteArray &)) );
|
|
}
|
|
kdDebug()<<"XMLTools::getDTD: Documents: "<<m_docDtds.count()<<", DTDs: "<<m_dtds.count()<<endl;
|
|
}
|
|
|
|
void PluginKateXMLTools::slotFinished( TDEIO::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<TDEIO::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 = "";
|
|
}
|
|
TQApplication::restoreOverrideCursor();
|
|
}
|
|
|
|
void PluginKateXMLTools::slotData( TDEIO::Job *, const TQByteArray &data )
|
|
{
|
|
m_dtdString += TQString( data );
|
|
}
|
|
|
|
void PluginKateXMLTools::assignDTD( PseudoDTD *dtd, KTextEditor::Document *doc )
|
|
{
|
|
m_docDtds.replace( doc->documentNumber(), dtd );
|
|
connect( doc, TQ_SIGNAL(charactersInteractivelyInserted(int,int,const TQString&) ),
|
|
this, TQ_SLOT(keyEvent(int,int,const TQString&)) );
|
|
|
|
disconnect( doc, TQ_SIGNAL(backspacePressed()), this, 0 );
|
|
connect( doc, TQ_SIGNAL(backspacePressed() ),
|
|
this, TQ_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()];
|
|
TQString parentElement = getParentElement( *kv, false );
|
|
TQStringList allowed;
|
|
|
|
if( dtd )
|
|
allowed = dtd->allowedElements(parentElement );
|
|
|
|
InsertElement *dialog = new InsertElement(
|
|
( TQWidget *)application()->activeMainWindow()->viewManager()->activeView(), "insertXml" );
|
|
TQString text = dialog->showDialog( allowed );
|
|
delete dialog;
|
|
|
|
if( !text.isEmpty() )
|
|
{
|
|
TQStringList list = TQStringList::split( ' ', text );
|
|
TQString pre;
|
|
TQString 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] + ">";
|
|
}
|
|
|
|
TQString 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;
|
|
}
|
|
TQString parentElement = getParentElement( *kv, false );
|
|
|
|
//kdDebug() << "parentElement: '" << parentElement << "'" << endl;
|
|
TQString closeTag = "</" + parentElement + ">";
|
|
if( ! parentElement.isEmpty() )
|
|
kv->insertText( closeTag );
|
|
}
|
|
|
|
// modify the completion string before it gets inserted
|
|
void PluginKateXMLTools::filterInsertString( KTextEditor::CompletionEntry *ce, TQString *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 );
|
|
TQString lineStr = kv->getDoc()->textLine(line );
|
|
TQString leftCh = lineStr.mid( col-1, 1 );
|
|
TQString 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-- )
|
|
{
|
|
TQString ch = lineStr.mid( startAttValue-1, 1 );
|
|
if( isQuote(ch) )
|
|
break;
|
|
}
|
|
|
|
// find right quote:
|
|
for( endAttValue = col; endAttValue <= lineStr.length(); endAttValue++ )
|
|
{
|
|
TQString 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/>
|
|
TQString 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:
|
|
TQTimer::singleShot( 10, this, TQ_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.
|
|
*/
|
|
TQString PluginKateXMLTools::insideTag( Kate::View &kv )
|
|
{
|
|
uint line = 0, col = 0;
|
|
kv.cursorPositionReal( &line, &col );
|
|
int y = line; // another variable because uint <-> int
|
|
|
|
do {
|
|
TQString lineStr = kv.getDoc()->textLine(y );
|
|
for( uint x = col; x > 0; x-- )
|
|
{
|
|
TQString ch = lineStr.mid( x-1, 1 );
|
|
if( ch == ">" ) // cursor is outside tag
|
|
return "";
|
|
|
|
if( ch == "<" )
|
|
{
|
|
TQString 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 "="
|
|
*/
|
|
TQString 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;
|
|
TQString lineStr = "";
|
|
TQString ch = "";
|
|
|
|
do {
|
|
lineStr = kv.getDoc()->textLine(y );
|
|
for( x = col; x > 0; x-- )
|
|
{
|
|
ch = lineStr.mid( x-1, 1 );
|
|
TQString 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
|
|
TQString 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
|
|
*/
|
|
TQString 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 );
|
|
TQString str = kv.getDoc()->textLine(line );
|
|
|
|
while( true )
|
|
{
|
|
// move left a character
|
|
if( !col-- )
|
|
{
|
|
do
|
|
{
|
|
if( !line-- ) return TQString(); // 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 TQString();
|
|
|
|
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;
|
|
|
|
TQString 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( TQString 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( TQString tag )
|
|
{
|
|
return ( tag.startsWith("</") );
|
|
}
|
|
|
|
bool PluginKateXMLTools::isEmptyTag( TQString 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( TQString ch )
|
|
{
|
|
return ( ch == "\"" || ch == "'" );
|
|
}
|
|
|
|
|
|
// ========================================================================
|
|
// Tools:
|
|
|
|
/** Sort a TQStringList case-insensitively. Static. TODO: make it more simple. */
|
|
TQStringList PluginKateXMLTools::sortTQStringList( TQStringList list ) {
|
|
// Sort list case-insensitive. This looks complicated but using a TQMap
|
|
// is even suggested by the TQt documentation.
|
|
TQMap<TQString,TQString> mapList;
|
|
for ( TQStringList::Iterator it = list.begin(); it != list.end(); ++it )
|
|
{
|
|
TQString 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();
|
|
TQMap<TQString,TQString>::Iterator it;
|
|
|
|
// TQt 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( TQWidget *parent, const char *name )
|
|
:KDialogBase( parent, name, true, i18n("Insert XML Element" ),
|
|
KDialogBase::Ok|KDialogBase::Cancel)
|
|
{
|
|
}
|
|
|
|
InsertElement::~InsertElement()
|
|
{
|
|
}
|
|
|
|
void InsertElement::slotHistoryTextChanged( const TQString& text )
|
|
{
|
|
enableButtonOK( !text.isEmpty() );
|
|
}
|
|
|
|
TQString InsertElement::showDialog( TQStringList &completions )
|
|
{
|
|
TQWidget *page = new TQWidget( this );
|
|
setMainWidget( page );
|
|
TQVBoxLayout *topLayout = new TQVBoxLayout( page, 0, spacingHint() );
|
|
|
|
KHistoryCombo *combo = new KHistoryCombo( page, "value" );
|
|
combo->setHistoryItems( completions, true );
|
|
connect( combo->lineEdit(), TQ_SIGNAL(textChanged ( const TQString & )),
|
|
this, TQ_SLOT(slotHistoryTextChanged(const TQString &)) );
|
|
TQString text = i18n( "Enter XML tag name and attributes (\"<\", \">\" and closing tag will be supplied):" );
|
|
TQLabel *label = new TQLabel( text, page, "insert" );
|
|
|
|
topLayout->addWidget( label );
|
|
topLayout->addWidget( combo );
|
|
|
|
combo->setFocus();
|
|
slotHistoryTextChanged( combo->lineEdit()->text() );
|
|
if( exec() )
|
|
return combo->currentText();
|
|
|
|
return TQString();
|
|
}
|
|
//END InsertElement dialog
|