1 #include "help.h"
2 #include "texdocdialog.h"
3 #include "smallUsefulFunctions.h"
4 #include "utilsSystem.h"
5 #include "configmanager.h"
6 #include <QProcessEnvironment>
7 #include <QMutex>
8 
Help(QObject * parent)9 Help::Help(QObject *parent): QObject(parent),texDocSystem(0)
10 {
11 
12 }
13 
14 /*!
15  * \brief execute a dialog to let the user choose a package to show its documentation
16  * \param packages
17  * \param defaultPackage
18  */
execTexdocDialog(const QStringList & packages,const QString & defaultPackage)19 void Help::execTexdocDialog(const QStringList &packages, const QString &defaultPackage)
20 {
21     TexdocDialog dialog(nullptr,this);
22 	dialog.setPackageNames(packages);
23 	if (!defaultPackage.isEmpty()) {
24 		dialog.setPreferredPackage(defaultPackage);
25 	}
26 	if (dialog.exec()) {
27 		viewTexdoc(dialog.selectedPackage());
28 	}
29 }
30 /*!
31  * \brief run texdoc --view package
32  * \param package
33  */
viewTexdoc(QString package)34 void Help::viewTexdoc(QString package)
35 {
36 	if (package.isEmpty()) {
37 		QAction *act = qobject_cast<QAction *>(sender());
38 		if (!act) return;
39 		package = act->data().toString();
40 	}
41 	if (!package.isEmpty()) {
42         QString answer=runTexdoc("--view "+package);
43 	}
44 }
45 
46 /*!
47  * \brief check if system runs miktex
48  * Tries to run texdoc --veriosn and analyzes result.
49  * Miktex starts with MikTeX ...
50  * \return
51  */
isMiktexTexdoc()52 bool Help::isMiktexTexdoc()
53 {
54     if (!texDocSystem) {
55         QString answer=runTexdoc("--version");
56 		texDocSystem = answer.startsWith("MiKTeX") ? 1 : 2;
57 	}
58 	return (texDocSystem == 1);
59 }
60 
isTexdocExpectedToFinish()61 bool Help::isTexdocExpectedToFinish()
62 {
63 	if (!isMiktexTexdoc()) return true;
64 	QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
65     foreach (const QString &var, env.keys()) {
66 		if (var.startsWith("MIKTEX_VIEW_")) {
67 			// miktex texdoc will run as long as the viewer is opened when the MIKTEX_VIEW_* variables are set
68 			// http://docs.miktex.org/manual/mthelp.html
69 			return false;
70 		}
71 	}
72 	return true;
73 }
74 
75 /*!
76  * \brief search for documentation files for a given package
77  * It uses texdoc to access that information.
78  * \param package
79  * \param silent
80  * \return
81  */
packageDocFile(const QString & package,bool silent)82 QString Help::packageDocFile(const QString &package, bool silent)
83 {
84     QString cmd = BuildManager::CMD_TEXDOC;
85 	if (cmd.isEmpty()) {
86 		if (!silent) UtilsUi::txsWarning(tr("texdoc not found."));
87 		return QString();
88 	}
89 	QStringList args;
90     if (isMiktexTexdoc()) {
91 		args << "--list-only";
92 	} else {
93 		args << "--list" << "--machine";
94 	}
95 	args << package;
96 
97 
98     QString output =runTexdoc(args.join(" "));
99 
100 	QStringList allFiles;
101     if (isMiktexTexdoc()) {
102 		allFiles = output.split("\r\n");
103 	} else {
104 		foreach (const QString &line, output.split("\n")) {
105 			QStringList cols = line.simplified().split(" ");
106 			if (cols.count() > 2)
107 				allFiles << cols.at(2);
108 		}
109 	}
110 	foreach (const QString &file, allFiles) {
111 		if (file.endsWith(".pdf") || file.endsWith(".dvi"))
112 			return file;
113 	}
114 	return QString();
115 }
116 /*!
117  * \brief search for documentation files for a given package asynchrnously
118  * It uses texdoc to access that information.
119  * The results are processed in texdocAvailableRequestFinished
120  * \param package
121  * \param silent
122  * \return
123  */
texdocAvailableRequest(const QString & package)124 void Help::texdocAvailableRequest(const QString &package)
125 {
126 	if (package.isEmpty())
127 		return;
128     if (BuildManager::CMD_TEXDOC.isEmpty()) {
129 		emit texdocAvailableReply(package, false, tr("texdoc not found."));
130 		return;
131 	}
132 
133 	QStringList args;
134 	if (isMiktexTexdoc()) {
135 		args << "--print-only" << package;
136 	} else {
137         args << "--list" << "--machine" << package; // --print-only does not exist in texlive 2012, actual is response is not used either ...
138 		// TODO: not the right option: don't open the viewer here
139 		// There seems to be no option yielding only the would be called command
140 		// Alternative: texdoc --list -M and parse the first line for the package name
141 	}
142     runTexdocAsync(args.join(" "),SLOT(texdocAvailableRequestFinished(int,QProcess::ExitStatus)));
143 
144 }
texdocAvailableRequestFinished(int,QProcess::ExitStatus status)145 void Help::texdocAvailableRequestFinished(int,QProcess::ExitStatus status){
146 
147     if(status!=QProcess::NormalExit) return; // texdoc --list failed
148 
149     ProcessX *proc=qobject_cast<ProcessX *>(sender());
150     QString *buffer=proc->getStdoutBuffer();
151     QString cmdLine=proc->getCommandLine();
152     int i=cmdLine.lastIndexOf(" ");
153     QString package;
154     if(i>-1){
155         package=cmdLine.mid(i+1);
156     }
157 
158 
159     if(buffer==nullptr) return; // sanity check
160 
161     if(!isMiktexTexdoc() && !buffer->isEmpty()){
162         // analyze texdoc --list result in more detail, as it gives results even for partially matched names
163         QStringList lines=buffer->split("\n");
164         QString line=lines.first();
165         QStringList cols=line.split("\t");
166         if(cols.count()>4){
167             if(cols.value(1).startsWith("-")){
168                 buffer->clear(); // only partial, no real match
169             }
170         }
171     }
172 
173     emit texdocAvailableReply(package, !buffer->isEmpty(), QString());
174 
175     delete buffer;
176 }
177 
178 /*!
179  * \brief run texdoc command
180  * \param args
181  * \return
182  */
runTexdoc(QString args)183 QString Help::runTexdoc(QString args)
184 {
185     QString output;
186     emit statusMessage(QString(" texdoc "));
187     emit runCommand(BuildManager::CMD_TEXDOC+" "+args, &output);
188     return output;
189 }
190 /*!
191  * \brief run texdoc command asynchronously
192  * \param args
193  * \param finishedCMD SLOT for return path
194  * \return
195  */
runTexdocAsync(QString args,const char * finishedCMD)196 bool Help::runTexdocAsync(QString args,const char * finishedCMD)
197 {
198     emit statusMessage(QString(" texdoc (async)"));
199     emit runCommandAsync(BuildManager::CMD_TEXDOC+" "+args, finishedCMD);
200     return true;
201 }
202 
203 
204 
205 
LatexReference(QObject * parent)206 LatexReference::LatexReference(QObject *parent) : QObject(parent) {}
207 
setFile(QString filename)208 void LatexReference::setFile(QString filename)
209 {
210 	m_filename = filename;
211 	if (filename.isEmpty()) return;
212 
213 	QFile f(filename);
214 	if (!f.open(QFile::ReadOnly | QFile::Text)) return;
215 	QTextStream stream(&f);
216 #if QT_VERSION < QT_VERSION_CHECK(6,0,0)
217     stream.setCodec("UTF-8");
218 #endif
219 	m_htmltext = stream.readAll();
220 	makeIndex();
221 }
222 
contains(const QString & command)223 bool LatexReference::contains(const QString &command)
224 {
225 	return m_anchors.contains(command);
226 }
227 
228 /// tries to generate a text of suitable length for display as a tooltip
getTextForTooltip(const QString & command)229 QString LatexReference::getTextForTooltip(const QString &command)
230 {
231 	QString sectionText = getSectionText(command);
232 	QString partialText;
233     if (sectionText.count('\n') > 30) { // tooltip would be very large: try to get a reasonable smaller string
234         if (command.startsWith("\\begin{")) {
235 			return truncateLines(sectionText, 30);
236 		} else {
237 			partialText = getPartialText(command);
238             int nr=partialText.count('\n');
239             if(nr<10)
240                 return truncateLines(sectionText, 30);
241             if (!partialText.isEmpty()) return partialText;
242         }
243 	}
244 	return sectionText;
245 }
246 
247 /*! get all the text in the section describing the command
248  * it starts with the first heading after the section anchor and ranges down to the next <hr>
249  */
getSectionText(const QString & command)250 QString LatexReference::getSectionText(const QString &command)
251 {
252 	Anchor sAnchor(m_sectionAnchors[command]);
253 	if (sAnchor.name.isNull()) return QString();
254 	if (sAnchor.start_pos < 0) {
255 		sAnchor.start_pos = m_htmltext.indexOf(QString("<a name=\"%1\">").arg(sAnchor.name));
256 		sAnchor.start_pos = m_htmltext.indexOf("<h", sAnchor.start_pos); // skip header div by going to next headline
257 	}
258 	if (sAnchor.start_pos < 0) return QString();
259 	if (sAnchor.end_pos < 0) {
260 		QString endTag("<hr>");
261 		sAnchor.end_pos = m_htmltext.indexOf(endTag, sAnchor.start_pos);
262 		m_sectionAnchors.insert(command, sAnchor); // save positions for a faster lookup next time
263 	}
264 	return m_htmltext.mid(sAnchor.start_pos, sAnchor.end_pos - sAnchor.start_pos);
265 }
266 
267 /*! get only a partial description for the command
268  *  the serach looks for the following block types (sqare brackets mark the extrated text:
269  *    [<dt>(anchor-in-here)</dt><dd></dd>]
270  *    </div>[(anchor-in-here)]</a name=
271  */
getPartialText(const QString & command)272 QString LatexReference::getPartialText(const QString &command)
273 {
274     static QRegularExpression startTag("<(dt|/div)>");
275 	QString endTag;
276 	int endOffset = 0;
277 
278 	Anchor anchor(m_anchors[command]);
279 	if (anchor.name.isNull()) return QString();
280 	if (anchor.start_pos < 0) {
281 		anchor.start_pos = m_htmltext.indexOf(QString("<a name=\"%1\">").arg(anchor.name));
282         QRegularExpressionMatch match;
283         anchor.start_pos = m_htmltext.lastIndexOf(startTag, anchor.start_pos,&match);
284         if (match.captured(1) == "dt") {
285 			endTag = "</dd>";
286 			endOffset = endTag.length();
287 		} else {
288 			anchor.start_pos += QString("</div>").length();
289 			endTag = "</p>";
290 		}
291 	}
292 	if (anchor.start_pos < 0) return QString();
293 	if (anchor.end_pos < 0) {
294 		anchor.end_pos = m_htmltext.indexOf(endTag, anchor.start_pos + 1);
295 		if (anchor.end_pos >= 0) anchor.end_pos += endOffset;
296 		int hrEnd = m_htmltext.indexOf("<hr>", anchor.start_pos + 1);
297 		if (hrEnd >= 0 && hrEnd < anchor.end_pos) // don't go further than the next <hr>
298 			anchor.end_pos = hrEnd;
299 		m_anchors.insert(command, anchor); // save positions for a faster lookup next time
300 	}
301 	return m_htmltext.mid(anchor.start_pos, anchor.end_pos - anchor.start_pos);
302 }
303 
304 /* parses the index of the reference manual and extracts the anchor names for the commands */
makeIndex()305 void LatexReference::makeIndex()
306 {
307 	QString startTag("<table class=\"index-cp\"");
308 	QString endTag("</table>");
309 
310 	int start = m_htmltext.indexOf(startTag);
311 	if (start < 0) return;
312 	int end = m_htmltext.indexOf(endTag, start);
313 	int length = end < 0 ? -1 : end - start + endTag.length();
314 	QString indexText = m_htmltext.mid(start, length);
315 
316     QRegularExpression rx("<a href=\"#([^\"]+)\"><code>([^\\s<]+)[^\n]*<a href=\"#([^\"]+)\">([^<]+)</a>");
317 	int pos = 0;
318 	while (pos >= 0) {
319         QRegularExpressionMatch match;
320         pos = indexText.indexOf(rx, pos,&match);
321 		if (pos < 0) break;
322         QString anchorName(match.captured(1));
323         QString word(match.captured(2));
324         QString sectionAnchorName(match.captured(3));
325         //QString sectionTitle(match.captured(4));
326 		if (word.startsWith('\\')) { // a command
327 			if (word == "\\begin" || word == "\\") {
328 				// don't add these words to the index because they give a mess in the tooltips
329                 pos += match.capturedLength();
330 				continue;
331 			}
332 			m_anchors.insert(word, Anchor(anchorName));
333 			m_sectionAnchors.insert(word, Anchor(sectionAnchorName));
334 		} else if (anchorName.contains("environment")) { // an environment
335 			m_anchors.insert("\\begin{" + word, Anchor(anchorName));
336 			m_sectionAnchors.insert("\\begin{" + word, Anchor(sectionAnchorName));
337 		} else {
338 			// TODO: anything useful in the rest?
339 			//qDebug() << word << anchorName << sectionAnchorName << sectionTitle;
340 		}
341         pos += match.capturedLength();
342 	}
343 	//qDebug() << "Found entries in index:" << m_anchors.count();
344 }
345 
346