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