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 | |
10 | QT_BEGIN_NAMESPACE |
11 | |
12 | QHash<QString, QString> Quoter::; |
13 | |
14 | static 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 |
33 | QStringList 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 | */ |
55 | static 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 | |
84 | Quoter::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 | |
106 | void Quoter::reset() |
107 | { |
108 | m_silent = false; |
109 | m_plainLines.clear(); |
110 | m_markedLines.clear(); |
111 | m_codeLocation = Location(); |
112 | } |
113 | |
114 | void 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 | |
158 | QString 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 | |
182 | QString Quoter::quoteSnippet(const Location &docLocation, const QString &identifier) |
183 | { |
184 | QString = 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 | |
224 | QString Quoter::quoteTo(const Location &docLocation, const QString &command, const QString &pattern) |
225 | { |
226 | QString t; |
227 | QString = 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 | |
246 | QString 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 | |
254 | QString 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 | |
272 | bool 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 | |
294 | void 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 | |
307 | QString Quoter::() 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 | |
315 | QString Quoter::removeSpecialLines(const QString &line, const QString &, 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 | |
338 | QT_END_NAMESPACE |
339 | |