1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3
4#include "quoter.h"
5
6#include <QtCore/qdebug.h>
7#include <QtCore/qfileinfo.h>
8#include <QtCore/qregularexpression.h>
9
10QT_BEGIN_NAMESPACE
11
12QHash<QString, QString> Quoter::s_commentHash;
13
14static void replaceMultipleNewlines(QString &s)
15{
16 const qsizetype n = s.size();
17 bool slurping = false;
18 int j = -1;
19 const QChar newLine = QLatin1Char('\n');
20 QChar *d = s.data();
21 for (int i = 0; i != n; ++i) {
22 const QChar c = d[i];
23 bool hit = (c == newLine);
24 if (slurping && hit)
25 continue;
26 d[++j] = c;
27 slurping = hit;
28 }
29 s.resize(size: ++j);
30}
31
32// This is equivalent to line.split( QRegularExpression("\n(?!\n|$)") ) but much faster
33QStringList Quoter::splitLines(const QString &line)
34{
35 QStringList result;
36 qsizetype i = line.size();
37 while (true) {
38 qsizetype j = i - 1;
39 while (j >= 0 && line.at(i: j) == QLatin1Char('\n'))
40 --j;
41 while (j >= 0 && line.at(i: j) != QLatin1Char('\n'))
42 --j;
43 result.prepend(t: line.mid(position: j + 1, n: i - j - 1));
44 if (j < 0)
45 break;
46 i = j;
47 }
48 return result;
49}
50
51/*
52 Transforms 'int x = 3 + 4' into 'int x=3+4'. A white space is kept
53 between 'int' and 'x' because it is meaningful in C++.
54*/
55static void trimWhiteSpace(QString &str)
56{
57 enum { Normal, MetAlnum, MetSpace } state = Normal;
58 const qsizetype n = str.size();
59
60 int j = -1;
61 QChar *d = str.data();
62 for (int i = 0; i != n; ++i) {
63 const QChar c = d[i];
64 if (c.isLetterOrNumber()) {
65 if (state == Normal) {
66 state = MetAlnum;
67 } else {
68 if (state == MetSpace)
69 str[++j] = c;
70 state = Normal;
71 }
72 str[++j] = c;
73 } else if (c.isSpace()) {
74 if (state == MetAlnum)
75 state = MetSpace;
76 } else {
77 state = Normal;
78 str[++j] = c;
79 }
80 }
81 str.resize(size: ++j);
82}
83
84Quoter::Quoter() : m_silent(false)
85{
86 /* We're going to hard code these delimiters:
87 * C++, Qt, Qt Script, Java:
88 //! [<id>]
89 * .pro, .py, CMake files:
90 #! [<id>]
91 * .html, .qrc, .ui, .xq, .xml files:
92 <!-- [<id>] -->
93 */
94 if (s_commentHash.empty()) {
95 s_commentHash["pro"] = "#!";
96 s_commentHash["py"] = "#!";
97 s_commentHash["cmake"] = "#!";
98 s_commentHash["html"] = "<!--";
99 s_commentHash["qrc"] = "<!--";
100 s_commentHash["ui"] = "<!--";
101 s_commentHash["xml"] = "<!--";
102 s_commentHash["xq"] = "<!--";
103 }
104}
105
106void Quoter::reset()
107{
108 m_silent = false;
109 m_plainLines.clear();
110 m_markedLines.clear();
111 m_codeLocation = Location();
112}
113
114void Quoter::quoteFromFile(const QString &userFriendlyFilePath, const QString &plainCode,
115 const QString &markedCode)
116{
117 m_silent = false;
118
119 /*
120 Split the source code into logical lines. Empty lines are
121 treated specially. Before:
122
123 p->alpha();
124 p->beta();
125
126 p->gamma();
127
128
129 p->delta();
130
131 After:
132
133 p->alpha();
134 p->beta();\n
135 p->gamma();\n\n
136 p->delta();
137
138 Newlines are preserved because they affect codeLocation.
139 */
140 m_codeLocation = Location(userFriendlyFilePath);
141
142 m_plainLines = splitLines(line: plainCode);
143 m_markedLines = splitLines(line: markedCode);
144 if (m_markedLines.size() != m_plainLines.size()) {
145 m_codeLocation.warning(
146 QStringLiteral("Something is wrong with qdoc's handling of marked code"));
147 m_markedLines = m_plainLines;
148 }
149
150 /*
151 Squeeze blanks (cat -s).
152 */
153 for (auto &line : m_markedLines)
154 replaceMultipleNewlines(s&: line);
155 m_codeLocation.start();
156}
157
158QString Quoter::quoteLine(const Location &docLocation, const QString &command,
159 const QString &pattern)
160{
161 if (m_plainLines.isEmpty()) {
162 failedAtEnd(docLocation, command);
163 return QString();
164 }
165
166 if (pattern.isEmpty()) {
167 docLocation.warning(QStringLiteral("Missing pattern after '\\%1'").arg(a: command));
168 return QString();
169 }
170
171 if (match(docLocation, pattern, line: m_plainLines.first()))
172 return getLine();
173
174 if (!m_silent) {
175 docLocation.warning(QStringLiteral("Command '\\%1' failed").arg(a: command));
176 m_codeLocation.warning(QStringLiteral("Pattern '%1' didn't match here").arg(a: pattern));
177 m_silent = true;
178 }
179 return QString();
180}
181
182QString Quoter::quoteSnippet(const Location &docLocation, const QString &identifier)
183{
184 QString comment = commentForCode();
185 QString delimiter = comment + QString(" [%1]").arg(a: identifier);
186 QString t;
187 int indent = 0;
188
189 while (!m_plainLines.isEmpty()) {
190 if (match(docLocation, pattern: delimiter, line: m_plainLines.first())) {
191 QString startLine = getLine();
192 while (indent < startLine.size() && startLine[indent] == QLatin1Char(' '))
193 indent++;
194 break;
195 }
196 getLine();
197 }
198 while (!m_plainLines.isEmpty()) {
199 QString line = m_plainLines.first();
200 if (match(docLocation, pattern: delimiter, line)) {
201 QString lastLine = getLine(unindent: indent);
202 qsizetype dIndex = lastLine.indexOf(s: delimiter);
203 if (dIndex > 0) {
204 // The delimiter might be preceded on the line by other
205 // delimeters, so look for the first comment on the line.
206 QString leading = lastLine.left(n: dIndex);
207 dIndex = leading.indexOf(s: comment);
208 if (dIndex != -1)
209 leading = leading.left(n: dIndex);
210 if (leading.endsWith(s: QLatin1String("<@comment>")))
211 leading.chop(n: 10);
212 if (!leading.trimmed().isEmpty())
213 t += leading;
214 }
215 return t;
216 }
217
218 t += removeSpecialLines(line, comment, unindent: indent);
219 }
220 failedAtEnd(docLocation, command: QString("snippet (%1)").arg(a: delimiter));
221 return t;
222}
223
224QString Quoter::quoteTo(const Location &docLocation, const QString &command, const QString &pattern)
225{
226 QString t;
227 QString comment = commentForCode();
228
229 if (pattern.isEmpty()) {
230 while (!m_plainLines.isEmpty()) {
231 QString line = m_plainLines.first();
232 t += removeSpecialLines(line, comment);
233 }
234 } else {
235 while (!m_plainLines.isEmpty()) {
236 if (match(docLocation, pattern, line: m_plainLines.first())) {
237 return t;
238 }
239 t += getLine();
240 }
241 failedAtEnd(docLocation, command);
242 }
243 return t;
244}
245
246QString Quoter::quoteUntil(const Location &docLocation, const QString &command,
247 const QString &pattern)
248{
249 QString t = quoteTo(docLocation, command, pattern);
250 t += getLine();
251 return t;
252}
253
254QString Quoter::getLine(int unindent)
255{
256 if (m_plainLines.isEmpty())
257 return QString();
258
259 m_plainLines.removeFirst();
260
261 QString t = m_markedLines.takeFirst();
262 int i = 0;
263 while (i < unindent && i < t.size() && t[i] == QLatin1Char(' '))
264 i++;
265
266 t = t.mid(position: i);
267 t += QLatin1Char('\n');
268 m_codeLocation.advanceLines(n: t.count(c: QLatin1Char('\n')));
269 return t;
270}
271
272bool Quoter::match(const Location &docLocation, const QString &pattern0, const QString &line)
273{
274 QString str = line;
275 while (str.endsWith(c: QLatin1Char('\n')))
276 str.truncate(pos: str.size() - 1);
277
278 QString pattern = pattern0;
279 if (pattern.startsWith(c: QLatin1Char('/')) && pattern.endsWith(c: QLatin1Char('/'))
280 && pattern.size() > 2) {
281 QRegularExpression rx(pattern.mid(position: 1, n: pattern.size() - 2));
282 if (!m_silent && !rx.isValid()) {
283 docLocation.warning(
284 QStringLiteral("Invalid regular expression '%1'").arg(a: rx.pattern()));
285 m_silent = true;
286 }
287 return str.indexOf(re: rx) != -1;
288 }
289 trimWhiteSpace(str);
290 trimWhiteSpace(str&: pattern);
291 return str.indexOf(s: pattern) != -1;
292}
293
294void Quoter::failedAtEnd(const Location &docLocation, const QString &command)
295{
296 if (!m_silent && !command.isEmpty()) {
297 if (m_codeLocation.filePath().isEmpty()) {
298 docLocation.warning(QStringLiteral("Unexpected '\\%1'").arg(a: command));
299 } else {
300 docLocation.warning(QStringLiteral("Command '\\%1' failed at end of file '%2'")
301 .arg(args: command, args: m_codeLocation.filePath()));
302 }
303 m_silent = true;
304 }
305}
306
307QString Quoter::commentForCode() const
308{
309 QFileInfo fi = QFileInfo(m_codeLocation.fileName());
310 if (fi.fileName() == "CMakeLists.txt")
311 return "#!";
312 return s_commentHash.value(key: fi.suffix(), defaultValue: "//!");
313}
314
315QString Quoter::removeSpecialLines(const QString &line, const QString &comment, int unindent)
316{
317 QString t;
318
319 // Remove special macros to support Qt namespacing.
320 QString trimmed = line.trimmed();
321 if (trimmed.startsWith(s: "QT_BEGIN_NAMESPACE")) {
322 getLine();
323 } else if (trimmed.startsWith(s: "QT_END_NAMESPACE")) {
324 getLine();
325 t += QLatin1Char('\n');
326 } else if (!trimmed.startsWith(s: comment)) {
327 // Ordinary code
328 t += getLine(unindent);
329 } else {
330 // Comments
331 if (line.contains(c: QLatin1Char('\n')))
332 t += QLatin1Char('\n');
333 getLine();
334 }
335 return t;
336}
337
338QT_END_NAMESPACE
339

source code of qttools/src/qdoc/qdoc/quoter.cpp