1/***************************************************************************
2 plugin_katexmlcheck.cpp - checks XML files using xmllint
3 -------------------
4 begin : 2002-07-06
5 copyright : (C) 2002 by Daniel Naber
6 email : daniel.naber@t-online.de
7 ***************************************************************************/
8
9/***************************************************************************
10 This program is free software; you can redistribute it and/or
11 modify it under the terms of the GNU General Public License
12 as published by the Free Software Foundation; either version 2
13 of the License, or (at your option) any later version.
14
15 This program is distributed in the hope that it will be useful,
16 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 GNU General Public License for more details.
19
20 You should have received a copy of the GNU General Public License
21 along with this program; if not, write to the Free Software
22 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
23 ***************************************************************************/
24
25/*
26-fixme: show dock if "Validate XML" is selected (doesn't currently work when Kate
27 was just started and the dockwidget isn't yet visible)
28-fixme(?): doesn't correctly disappear when deactivated in config
29*/
30
31#include "plugin_katexmlcheck.h"
32#include <QHBoxLayout>
33#include "plugin_katexmlcheck.moc"
34
35#include <qfile.h>
36#include <qinputdialog.h>
37#include <qregexp.h>
38#include <qstring.h>
39#include <qtextstream.h>
40#include <kactioncollection.h>
41#include <QApplication>
42#include <QTreeWidget>
43#include <QHeaderView>
44
45#include <kdefakes.h> // for setenv
46#include <kaction.h>
47#include <kcursor.h>
48#include <kdebug.h>
49#include <kcomponentdata.h>
50#include <klocale.h>
51#include <kmessagebox.h>
52#include <kstandarddirs.h>
53#include <ktemporaryfile.h>
54#include <kpluginfactory.h>
55#include <kprocess.h>
56
57K_PLUGIN_FACTORY(PluginKateXMLCheckFactory, registerPlugin<PluginKateXMLCheck>();)
58K_EXPORT_PLUGIN(PluginKateXMLCheckFactory("katexmlcheck"))
59
60PluginKateXMLCheck::PluginKateXMLCheck( QObject* parent, const QVariantList& )
61 : Kate::Plugin ( (Kate::Application *)parent )
62{
63}
64
65
66PluginKateXMLCheck::~PluginKateXMLCheck()
67{
68}
69
70
71Kate::PluginView *PluginKateXMLCheck::createView(Kate::MainWindow *mainWindow)
72{
73 return new PluginKateXMLCheckView(mainWindow);
74}
75
76
77//---------------------------------
78PluginKateXMLCheckView::PluginKateXMLCheckView(Kate::MainWindow *mainwin)
79 : Kate::PluginView (mainwin), Kate::XMLGUIClient(PluginKateXMLCheckFactory::componentData()),win(mainwin)
80{
81 dock = win->createToolView("kate_plugin_xmlcheck_ouputview", Kate::MainWindow::Bottom, SmallIcon("misc"), i18n("XML Checker Output"));
82 listview = new QTreeWidget( dock );
83 m_tmp_file=0;
84 m_proc=0;
85 QAction *a = actionCollection()->addAction("xml_check");
86 a->setText(i18n("Validate XML"));
87 connect(a, SIGNAL(triggered()), this, SLOT(slotValidate()));
88 // TODO?:
89 //(void) new KAction ( i18n("Indent XML"), KShortcut(), this,
90 // SLOT(slotIndent()), actionCollection(), "xml_indent" );
91
92 listview->setFocusPolicy(Qt::NoFocus);
93 QStringList headers;
94 headers << i18n("#");
95 headers << i18n("Line");
96 headers << i18n("Column");
97 headers << i18n("Message");
98 listview->setHeaderLabels(headers);
99 listview->setRootIsDecorated(false);
100 connect(listview, SIGNAL(itemClicked(QTreeWidgetItem*,int)), SLOT(slotClicked(QTreeWidgetItem*,int)));
101
102 QHeaderView *header = listview->header();
103 header->setResizeMode(0, QHeaderView::ResizeToContents);
104 header->setResizeMode(1, QHeaderView::ResizeToContents);
105 header->setResizeMode(2, QHeaderView::ResizeToContents);
106
107/* TODO?: invalidate the listview when document has changed
108 Kate::View *kv = application()->activeMainWindow()->activeView();
109 if( ! kv ) {
110 kDebug() << "Warning: no Kate::View";
111 return;
112 }
113 connect(kv, SIGNAL(modifiedChanged()), this, SLOT(slotUpdate()));
114*/
115
116 m_proc = new KProcess();
117 connect(m_proc, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(slotProcExited(int,QProcess::ExitStatus)));
118 // we currently only want errors:
119 m_proc->setOutputChannelMode(KProcess::OnlyStderrChannel);
120
121 mainWindow()->guiFactory()->addClient(this);
122}
123
124PluginKateXMLCheckView::~PluginKateXMLCheckView()
125{
126 mainWindow()->guiFactory()->removeClient( this );
127 delete m_proc;
128 delete m_tmp_file;
129 delete dock;
130}
131
132void PluginKateXMLCheckView::slotProcExited(int exitCode, QProcess::ExitStatus exitStatus)
133{
134 Q_UNUSED(exitCode);
135
136 // FIXME: doesn't work correct the first time:
137 //if( m_dockwidget->isDockBackPossible() ) {
138 // m_dockwidget->dockBack();
139// }
140
141 if (exitStatus != QProcess::NormalExit) {
142 QTreeWidgetItem *item = new QTreeWidgetItem();
143 item->setText(0, QString("1").rightJustified(4,' '));
144 item->setText(3, "Validate process crashed.");
145 listview->addTopLevelItem(item);
146 return;
147 }
148
149 kDebug() << "slotProcExited()";
150 QApplication::restoreOverrideCursor();
151 delete m_tmp_file;
152 QString proc_stderr = QString::fromLocal8Bit(m_proc->readAllStandardError());
153 m_tmp_file=0;
154 listview->clear();
155 uint list_count = 0;
156 uint err_count = 0;
157 if( ! m_validating ) {
158 // no i18n here, so we don't get an ugly English<->Non-english mixup:
159 QString msg;
160 if( m_dtdname.isEmpty() ) {
161 msg = "No DOCTYPE found, will only check well-formedness.";
162 } else {
163 msg = '\'' + m_dtdname + "' not found, will only check well-formedness.";
164 }
165 QTreeWidgetItem *item = new QTreeWidgetItem();
166 item->setText(0, QString("1").rightJustified(4,' '));
167 item->setText(3, msg);
168 listview->addTopLevelItem(item);
169 list_count++;
170 }
171 if( ! proc_stderr.isEmpty() ) {
172 QStringList lines = proc_stderr.split("\n", QString::SkipEmptyParts);
173 QString linenumber, msg;
174 int line_count = 0;
175 for(QStringList::Iterator it = lines.begin(); it != lines.end(); ++it) {
176 QString line = *it;
177 line_count++;
178 int semicolon_1 = line.indexOf(':');
179 int semicolon_2 = line.indexOf(':', semicolon_1+1);
180 int semicolon_3 = line.indexOf(':', semicolon_2+2);
181 int caret_pos = line.indexOf('^');
182 if( semicolon_1 != -1 && semicolon_2 != -1 && semicolon_3 != -1 ) {
183 linenumber = line.mid(semicolon_1+1, semicolon_2-semicolon_1-1).trimmed();
184 linenumber = linenumber.rightJustified(6, ' '); // for sorting numbers
185 msg = line.mid(semicolon_3+1, line.length()-semicolon_3-1).trimmed();
186 } else if( caret_pos != -1 || line_count == lines.size() ) {
187 // TODO: this fails if "^" occurs in the real text?!
188 if( line_count == lines.size() && caret_pos == -1 ) {
189 msg = msg+'\n'+line;
190 }
191 QString col = QString::number(caret_pos);
192 if( col == "-1" ) {
193 col = "";
194 }
195 err_count++;
196 list_count++;
197 QTreeWidgetItem *item = new QTreeWidgetItem();
198 item->setText(0, QString::number(list_count).rightJustified(4,' '));
199 item->setText(1, linenumber);
200 item->setTextAlignment(1, (item->textAlignment(1) & ~Qt::AlignHorizontal_Mask) | Qt::AlignRight);
201 item->setText(2, col);
202 item->setTextAlignment(2, (item->textAlignment(2) & ~Qt::AlignHorizontal_Mask) | Qt::AlignRight);
203 item->setText(3, msg);
204 listview->addTopLevelItem(item);
205 } else {
206 msg = msg+'\n'+line;
207 }
208 }
209 }
210 if( err_count == 0 ) {
211 QString msg;
212 if( m_validating ) {
213 msg = "No errors found, document is valid."; // no i18n here
214 } else {
215 msg = "No errors found, document is well-formed."; // no i18n here
216 }
217 QTreeWidgetItem *item = new QTreeWidgetItem();
218 item->setText(0, QString::number(list_count+1).rightJustified(4,' '));
219 item->setText(3, msg);
220 listview->addTopLevelItem(item);
221 }
222}
223
224
225void PluginKateXMLCheckView::slotClicked(QTreeWidgetItem *item, int column)
226{
227 Q_UNUSED(column);
228 kDebug() << "slotClicked";
229 if( item ) {
230 bool ok = true;
231 uint line = item->text(1).toUInt(&ok);
232 bool ok2 = true;
233 uint column = item->text(2).toUInt(&ok);
234 if( ok && ok2 ) {
235 KTextEditor::View *kv = win->activeView();
236 if( ! kv )
237 return;
238
239 kv->setCursorPosition(KTextEditor::Cursor (line-1, column));
240 }
241 }
242}
243
244
245void PluginKateXMLCheckView::slotUpdate()
246{
247 kDebug() << "slotUpdate() (not implemented yet)";
248}
249
250
251bool PluginKateXMLCheckView::slotValidate()
252{
253 kDebug() << "slotValidate()";
254
255 win->showToolView (dock);
256
257 m_proc->clearProgram();
258 m_validating = false;
259 m_dtdname = "";
260
261 KTextEditor::View *kv = win->activeView();
262 if( ! kv )
263 return false;
264 delete m_tmp_file;
265 m_tmp_file = new KTemporaryFile();
266 if( !m_tmp_file->open() ) {
267 kDebug() << "Error (slotValidate()): could not create '" << m_tmp_file->fileName() << "': " << m_tmp_file->errorString();
268 KMessageBox::error(0, i18n("<b>Error:</b> Could not create "
269 "temporary file '%1'.", m_tmp_file->fileName()));
270 delete m_tmp_file;
271 m_tmp_file=0L;
272 return false;
273 }
274 QTextStream s ( m_tmp_file );
275 s << kv->document()->text();
276 s.flush();
277
278 QString exe = KStandardDirs::findExe("xmllint");
279 if( exe.isEmpty() ) {
280 exe = KStandardDirs::locate("exe", "xmllint");
281 }
282
283 // use catalogs for KDE docbook:
284 if( ! getenv("XML_CATALOG_FILES") ) {
285 KComponentData ins("katexmlcheckplugin");
286 QString catalogs;
287 catalogs += ins.dirs()->findResource("data", "ksgmltools2/customization/catalog.xml");
288 kDebug() << "catalogs: " << catalogs;
289 setenv("XML_CATALOG_FILES", QFile::encodeName( catalogs ).data(), 1);
290 }
291 //kDebug() << "**catalogs: " << getenv("XML_CATALOG_FILES");
292
293 *m_proc << exe << "--noout";
294
295 // tell xmllint the working path of the document's file, if possible.
296 // otherweise it will not find relative DTDs
297 QString path = kv->document()->url().directory();
298 kDebug() << path;
299 if (!path.isEmpty()) {
300 *m_proc << "--path" << path;
301 }
302
303 // heuristic: assume that the doctype is in the first 10,000 bytes:
304 QString text_start = kv->document()->text().left(10000);
305 // remove comments before looking for doctype (as a doctype might be commented out
306 // and needs to be ignored then):
307 QRegExp re("<!--.*-->");
308 re.setMinimal(true);
309 text_start.remove(re);
310 QRegExp re_doctype("<!DOCTYPE\\s+(.*)\\s+(?:PUBLIC\\s+[\"'].*[\"']\\s+[\"'](.*)[\"']|SYSTEM\\s+[\"'](.*)[\"'])", Qt::CaseInsensitive);
311 re_doctype.setMinimal(true);
312
313 if( re_doctype.indexIn(text_start) != -1 ) {
314 QString dtdname;
315 if( ! re_doctype.cap(2).isEmpty() ) {
316 dtdname = re_doctype.cap(2);
317 } else {
318 dtdname = re_doctype.cap(3);
319 }
320 if( !dtdname.startsWith("http:") ) { // todo: u_dtd.isLocalFile() doesn't work :-(
321 // a local DTD is used
322 m_validating = true;
323 *m_proc << "--valid";
324 } else {
325 m_validating = true;
326 *m_proc << "--valid";
327 }
328 } else if( text_start.indexOf("<!DOCTYPE") != -1 ) {
329 // DTD is inside the XML file
330 m_validating = true;
331 *m_proc << "--valid";
332 }
333 *m_proc << m_tmp_file->fileName();
334
335 m_proc->start();
336 if( ! m_proc->waitForStarted(-1) ) {
337 KMessageBox::error(0, i18n("<b>Error:</b> Failed to execute xmllint. Please make "
338 "sure that xmllint is installed. It is part of libxml2."));
339 return false;
340 }
341 QApplication::setOverrideCursor(Qt::WaitCursor);
342 return true;
343}
344