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 | |
57 | K_PLUGIN_FACTORY(PluginKateXMLCheckFactory, registerPlugin<PluginKateXMLCheck>();) |
58 | K_EXPORT_PLUGIN(PluginKateXMLCheckFactory("katexmlcheck" )) |
59 | |
60 | PluginKateXMLCheck::PluginKateXMLCheck( QObject* parent, const QVariantList& ) |
61 | : Kate::Plugin ( (Kate::Application *)parent ) |
62 | { |
63 | } |
64 | |
65 | |
66 | PluginKateXMLCheck::~PluginKateXMLCheck() |
67 | { |
68 | } |
69 | |
70 | |
71 | Kate::PluginView *PluginKateXMLCheck::createView(Kate::MainWindow *mainWindow) |
72 | { |
73 | return new PluginKateXMLCheckView(mainWindow); |
74 | } |
75 | |
76 | |
77 | //--------------------------------- |
78 | PluginKateXMLCheckView::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 ; |
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 * = 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 | |
124 | PluginKateXMLCheckView::~PluginKateXMLCheckView() |
125 | { |
126 | mainWindow()->guiFactory()->removeClient( this ); |
127 | delete m_proc; |
128 | delete m_tmp_file; |
129 | delete dock; |
130 | } |
131 | |
132 | void 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 | |
225 | void 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 | |
245 | void PluginKateXMLCheckView::slotUpdate() |
246 | { |
247 | kDebug() << "slotUpdate() (not implemented yet)" ; |
248 | } |
249 | |
250 | |
251 | bool 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 | |