1 /*
2 For general Scribus (>=1.3.2) copyright and licensing information please refer
3 to the COPYING file provided with the program. Following this notice may exist
4 a copyright and/or license notice that predates the release of Scribus 1.3.2
5 for which a new license (GPL+exception) is in place.
6 */
7 /***************************************************************************
8 latexhelpers.cpp - description
9 -------------------
10 copyright : Scribus Team
11 ***************************************************************************/
12
13 /***************************************************************************
14 * *
15 * This program is free software; you can redistribute it and/or modify *
16 * it under the terms of the GNU General Public License as published by *
17 * the Free Software Foundation; either version 2 of the License, or *
18 * (at your option) any later version. *
19 * *
20 ***************************************************************************/
21
22
23 #include "latexhelpers.h"
24
25 #include <QDebug>
26 #include <QFile>
27 #include <QFileInfo>
28 #include <QDir>
29 #include <QMessageBox>
30
31 #include "prefsmanager.h"
32 #include "scpaths.h"
33 #include "ui/scmessagebox.h"
34
LatexHighlighter(QTextDocument * document)35 LatexHighlighter::LatexHighlighter(QTextDocument *document)
36 : QSyntaxHighlighter(document)
37 {
38 m_rules = nullptr;
39 }
40
highlightBlock(const QString & text)41 void LatexHighlighter::highlightBlock(const QString &text)
42 {
43 //This is required to fix a Qt incompatibility. See error message below for details.
44 static bool disable_highlighting = false;
45 if (disable_highlighting) return;
46
47 if (!m_rules) return;
48 foreach (LatexHighlighterRule *rule, *m_rules)
49 {
50 int index = text.indexOf(rule->regex);
51 while (index >= 0) {
52 int length;
53 if (rule->regex.captureCount() == 0)
54 {
55 length = rule->regex.matchedLength();
56 }
57 else
58 {
59 length = rule->regex.cap(1).length();
60 index = rule->regex.pos(1);
61 }
62 if (length == 0)
63 {
64 qWarning() << "Highlighter pattern" << rule->regex.pattern() << "matched a zero length string. This would lead to an infinite loop. Aborting. Please fix this pattern!";
65 break;
66 }
67 setFormat(index, length, rule->format);
68
69 int oldindex = index;
70 int offset = index + length;
71 index = text.indexOf(rule->regex, offset);
72 if (index >= 0 && (index == oldindex || index <= offset)) {
73 qWarning() << QObject::tr("Highlighter error: Invalid index returned by Qt's QString.indexOf(). This is a incompatibility between different Qt versions and it can only be fixed by recompiling Scribus with the same Qt version that is running on this system. Syntax highlighting is disabled now, but render frames should continue to work without problems.") << "Additional debugging info: old index:" << oldindex << "new index:"<< index << "offset:" << offset;
74 disable_highlighting = true;
75 return;
76 }
77 }
78 }
79 }
80
configBase()81 QString LatexConfigParser::configBase()
82 {
83 return ScPaths::instance().shareDir() + "/editorconfig/";
84 }
85
absoluteFilename(QString fn)86 QString LatexConfigParser::absoluteFilename(QString fn)
87 {
88 QFileInfo fi(fn);
89 if (!fi.exists())
90 return configBase() + fn;
91 return fn;
92 }
93
94 //TODO: Pass this information to LatexEditor, so the second parser can be removed
parseConfigFile(QString fn)95 bool LatexConfigParser::parseConfigFile(QString fn)
96 {
97 fn = absoluteFilename(fn);
98 m_error = "";
99 m_filename = fn;
100 QFile f(fn);
101 if (!f.open(QIODevice::ReadOnly))
102 {
103 ScMessageBox::critical(nullptr, QObject::tr("Error"), "<qt>" +
104 QObject::tr("Opening the configfile %1 failed! %2").arg(
105 fn, f.errorString())
106 + "</qt>");
107 }
108 xml.setDevice(&f);
109
110 while (!xml.atEnd())
111 {
112 xml.readNext();
113 if (xml.isWhitespace() || xml.isComment() || xml.isStartDocument() || xml.isEndDocument())
114 continue;
115 if (xml.isStartElement() && xml.name() == "editorsettings")
116 {
117 m_description = xml.attributes().value("description").toString();
118 m_icon = xml.attributes().value("icon").toString();
119 if (m_description.isEmpty())
120 m_description = fn;
121 parseElements();
122 }
123 else
124 formatError("Unexpected element at root level"+xml.name().toString()+", Token String: "+ xml.tokenString());
125 }
126 if (xml.hasError())
127 formatError(xml.errorString());
128 f.close();
129 return m_error.isEmpty();
130 }
131
parseElements()132 void LatexConfigParser::parseElements()
133 {
134 while (!xml.atEnd())
135 {
136 xml.readNext();
137 if (xml.isEndElement() && xml.name() == "editorsettings") break;
138 if (xml.isWhitespace() || xml.isComment() || xml.isEndElement()) continue;
139 if (!xml.isStartElement())
140 {
141 formatError("Unexpected element in <editorsettings>"+xml.name().toString()+", Token String: "+
142 xml.tokenString());
143 continue;
144 }
145
146 if (xml.name() == "executable") {
147 m_executable = xml.attributes().value("command").toString();
148 } else if (xml.name() == "imagefile") {
149 m_imageExtension = xml.attributes().value("extension").toString();
150 } else if (xml.name() == "highlighter") {
151 parseHighlighter();
152 } else if (xml.name() == "empty-frame-text") {
153 m_emptyFrameText = xml.readI18nText(true);
154 } else if (xml.name() == "preamble") {
155 m_preamble = xml.readElementText();
156 } else if (xml.name() == "postamble") {
157 m_postamble = xml.readElementText();
158 } else if (xml.name() == "tab") {
159 parseTab();
160 } else {
161 formatError("Unknown tag in <editorsettings>: "+xml.name().toString());
162 }
163 }
164 }
165
formatError(const QString & message)166 void LatexConfigParser::formatError(const QString& message)
167 {
168 QString new_error = QString::number(xml.lineNumber()) + ":" +
169 QString::number(xml.columnNumber()) + ":" + message;
170 qWarning() << m_filename << new_error;
171 m_error += new_error + "\n";
172 }
173
StrRefToBool(const QStringRef & str) const174 bool LatexConfigParser::StrRefToBool(const QStringRef &str) const
175 {
176 if (str == "1" || str == "true")
177 return true;
178 if (str == "0" || str == "false" || str.isEmpty())
179 return false;
180 qWarning() << "Invalid bool string:" << str.toString();
181 return false;
182 }
183
parseHighlighter()184 void LatexConfigParser::parseHighlighter()
185 {
186 foreach (LatexHighlighterRule *rule, highlighterRules)
187 delete rule;
188 highlighterRules.clear();
189 while (!xml.atEnd()) {
190 xml.readNext();
191 if (xml.isWhitespace() || xml.isComment())
192 continue;
193 if (xml.isEndElement() && xml.name() == "highlighter")
194 break;
195 if (xml.isEndElement() && xml.name() == "rule")
196 continue;
197 if (!xml.isStartElement() || xml.name() != "rule")
198 {
199 formatError("Unexpected element in <highlighter>: "+
200 xml.name().toString()+", Token String: "+
201 xml.tokenString());
202 continue;
203 }
204 QString regex = xml.attributes().value("regex").toString();
205 bool bold = StrRefToBool(xml.attributes().value("bold"));
206 bool italic = StrRefToBool(xml.attributes().value("italic"));
207 bool underline = StrRefToBool(xml.attributes().value("underline"));
208 bool minimal = StrRefToBool(xml.attributes().value("minimal"));
209 QString colorStr = xml.attributes().value("color").toString();
210 QColor color(colorStr);
211 if (!color.isValid())
212 {
213 color.fromRgb(0, 0, 0); //Black
214 if (!colorStr.isEmpty())
215 qWarning() << "Invalid color:" << colorStr;
216 }
217 LatexHighlighterRule *newRule = new LatexHighlighterRule();
218 newRule->format.setForeground(color);
219 newRule->format.setFontItalic(italic);
220 if (bold)
221 newRule->format.setFontWeight(QFont::Bold);
222 newRule->format.setFontUnderline(underline);
223 newRule->regex.setPattern(regex);
224 newRule->regex.setMinimal(minimal);
225 highlighterRules.append(newRule);
226 }
227 }
228
229
parseTab()230 void LatexConfigParser::parseTab()
231 {
232 QString type = xml.attributes().value("type").toString();
233 bool itemstab = (type == "items");
234 QString title = "";
235 QString name, text, default_value;
236
237 while (!xml.atEnd())
238 {
239 xml.readNext();
240 if (xml.isWhitespace() || xml.isComment()) continue;
241 if (xml.isEndElement() && xml.name() == "tab") break;
242 if (!xml.isStartElement())
243 {
244 formatError("Unexpected element in <tab>: "+xml.name().toString()+", Token String: "+
245 xml.tokenString());
246 continue;
247 }
248 if (xml.name() == "title")
249 {
250 if (!title.isEmpty())
251 formatError("Second <title> tag in <tab>");
252 title = xml.readI18nText();
253 }
254 else if (xml.name() == "item")
255 {
256 if (!itemstab)
257 formatError("Found <item> in a 'settings'-tab!");
258 // QString value = xml.attributes().value("value").toString();
259 // QString img = xml.attributes().value("image").toString();
260 text = xml.readI18nText();
261 }
262 else if (xml.name() == "comment" || xml.name() == "font"
263 || xml.name() == "spinbox" || xml.name() == "color"
264 || xml.name() == "text" || xml.name() == "list")
265 {
266 //TODO: Store this + attributes in a list
267 // QString tagname = xml.name().toString();
268 name = xml.attributes().value("name").toString();
269 default_value = xml.attributes().value("default").toString();
270 if (xml.name() != "list")
271 text = xml.readI18nText();
272 else
273 ignoreList();
274 if (!name.isEmpty())
275 {
276 if (properties.contains(name))
277 formatError("Redeclared setting with name: " + name);
278 else
279 properties.insert(name, default_value);
280 }
281 //TODO: qDebug() << "For future use:" << tagname << name << text << default_value;
282 }
283 else
284 formatError("Unexpected element in <tab>: " + xml.name().toString());
285 }
286
287 if (title.isEmpty())
288 formatError("Tab ended here, but no title was found!");
289 }
290
ignoreList()291 void LatexConfigParser::ignoreList()
292 {
293 //TODO: Quick hack to avoid real parsing
294 while (!xml.atEnd())
295 {
296 xml.readNext();
297 if (xml.isEndElement() && xml.name() == "list") break;
298 }
299 }
300
executable() const301 QString LatexConfigParser::executable() const
302 {
303 QFileInfo f(m_filename);
304 QString fileName=f.fileName();
305 QString command = PrefsManager::instance().latexCommands()[fileName];
306 if (command.isEmpty())
307 return m_executable;
308 return command;
309 }
310
readI18nText(bool unindent)311 QString I18nXmlStreamReader::readI18nText(bool unindent)
312 {
313 QString language = PrefsManager::instance().uiLanguage();
314 QString result;
315 int matchquality = 0;
316 bool i18n = false;
317 if (!isStartElement()) raiseError("readI18nText called without startelement!");
318
319 QString startTag = name().toString();
320 while (!atEnd()) {
321 readNext();
322 if (isWhitespace() || isComment()) continue;
323 if (isStartElement() && name() == startTag)
324 {
325 raiseError("Invalid nested elements.");
326 return "Error";
327 }
328 if (isEndElement() && name() == startTag)
329 {
330 if (!unindent)
331 return result.trimmed();
332 QStringList splitted = result.split("\n");
333 int i;
334 int minspaces = 0xffff;
335 /* NOTE: First line contains no leading whitespace so we start at 1 */
336 for (i = 1; i < splitted.size(); i++) {
337 if (splitted[i].trimmed().isEmpty()) continue;
338 int spaces;
339 QString tmp = splitted[i];
340 for (spaces = 0; spaces < tmp.length(); spaces++) {
341 if (!tmp[spaces].isSpace()) break;
342 }
343 if (spaces < minspaces) minspaces = spaces;
344 }
345 for (i = 1; i < splitted.size(); i++) {
346 splitted[i] = splitted[i].mid(minspaces);
347 }
348 return splitted.join("\n").trimmed();
349 }
350 if (i18n)
351 {
352 if (isEndElement())
353 {
354 if (name() == "i18n")
355 {
356 i18n = false;
357 }
358 else
359 {
360 raiseError("Invalid end element "+ name().toString());
361 }
362 continue;
363 }
364 if (!isStartElement())
365 {
366 raiseError("Unexpected data!");
367 }
368 if (name() == language)
369 {
370 matchquality = 2; //Perfect match
371 result = readElementText();
372 }
373 else if (language.startsWith(name().toString()) && matchquality <= 1)
374 {
375 matchquality = 1; //Only beginning part matches
376 result = readElementText();
377 }
378 else if (result.isEmpty())
379 {
380 matchquality = 0;
381 result = readElementText();
382 }
383 else
384 {
385 readElementText(); //Ignore the text
386 }
387 }
388 else
389 {
390 if (isStartElement())
391 {
392 if (name() == "i18n")
393 {
394 i18n = true;
395 continue;
396 }
397 raiseError("Tag " + name().toString() + "found, but \"i18n\" or string data expected.");
398 continue;
399 }
400 if (isCharacters())
401 result = result + text().toString();
402 }
403 }
404 raiseError("Unexpected end of XML file");
405 return result;
406 }
407
408 LatexConfigCache* LatexConfigCache::m_instance = nullptr;
409
instance()410 LatexConfigCache* LatexConfigCache::instance()
411 {
412 if (!m_instance)
413 m_instance = new LatexConfigCache();
414 return m_instance;
415 }
416
parser(const QString & filename,bool warnOnError)417 LatexConfigParser* LatexConfigCache::parser(const QString& filename, bool warnOnError)
418 {
419 if (m_parsers.contains(filename))
420 {
421 if (warnOnError && m_error[filename])
422 {
423 //Recreate element as error might have been fixed.
424 delete m_parsers[filename];
425 createParser(filename, warnOnError);
426 }
427 }
428 else
429 createParser(filename, warnOnError);
430 return m_parsers[filename];
431 }
432
433
createParser(const QString & filename,bool warnOnError)434 void LatexConfigCache::createParser(const QString& filename, bool warnOnError)
435 {
436 LatexConfigParser *parser = new LatexConfigParser();
437 bool hasError = !parser->parseConfigFile(filename);
438 m_parsers[filename] = parser;
439 m_error[filename] = hasError;
440 if (hasError)
441 {
442 ScMessageBox::critical(nullptr, QObject::tr("Error"), "<qt>" +
443 QObject::tr("Parsing the configfile %1 failed! Depending on the type of the error "
444 "render frames might not work correctly!\n%2").arg(
445 filename, parser->error())
446 + "</qt>");
447 }
448 }
449
hasError(const QString & filename)450 bool LatexConfigCache::hasError(const QString& filename)
451 {
452 if (!m_error.contains(filename))
453 return true;
454 return m_error[filename];
455 }
456
defaultConfigs()457 QStringList LatexConfigCache::defaultConfigs()
458 {
459 QDir dir(LatexConfigParser::configBase());
460 QStringList files;
461 files = dir.entryList(QStringList("*.xml"));
462 files.sort();
463 int i;
464 for (i = 0; i < files.size(); i++)
465 {
466 if (files[i].compare("sample.xml",Qt::CaseInsensitive)==0)
467 {
468 files.removeAt(i);
469 i--;
470 }
471 }
472 return files;
473 }
474
defaultCommands()475 QMap<QString, QString> LatexConfigCache::defaultCommands()
476 {
477 QMap<QString, QString> configCmds;
478
479 const QStringList configFiles = PrefsManager::instance().latexConfigs();
480 for (const QString& configFile : configFiles)
481 {
482 LatexConfigParser *config = LatexConfigCache::instance()->parser(configFile);
483 configCmds.insert(configFile, config->executable());
484 }
485 return configCmds;
486 }
487