Changeset View
Changeset View
Standalone View
Standalone View
tools/uni2characterwidth/template.cpp
- This file was added.
1 | /* | ||||
---|---|---|---|---|---|
2 | This file is part of Konsole, a terminal emulator for KDE. | ||||
3 | | ||||
4 | Copyright 2018 by Mariusz Glebocki <mglb@arccos-1.net> | ||||
5 | | ||||
6 | This program is free software; you can redistribute it and/or modify | ||||
7 | it under the terms of the GNU General Public License as published by | ||||
8 | the Free Software Foundation; either version 2 of the License, or | ||||
9 | (at your option) any later version. | ||||
10 | | ||||
11 | This program is distributed in the hope that it will be useful, | ||||
12 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
14 | GNU General Public License for more details. | ||||
15 | | ||||
16 | You should have received a copy of the GNU General Public License | ||||
17 | along with this program; if not, write to the Free Software | ||||
18 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA | ||||
19 | 02110-1301 USA. | ||||
20 | */ | ||||
21 | | ||||
22 | #include <QDebug> | ||||
23 | #include <QMap> | ||||
24 | #include <QString> | ||||
25 | #include <QVector> | ||||
26 | #include <QRegularExpression> | ||||
27 | #include <QRegularExpressionMatch> | ||||
28 | #include "template.h" | ||||
29 | | ||||
30 | static const QString unescape(const QStringRef &str) { | ||||
31 | QString result; | ||||
32 | result.reserve(str.length()); | ||||
33 | for(int i = 0; i < str.length(); ++i) { | ||||
34 | if(str[i] == QLatin1Char('\\') && i < str.length() - 1) | ||||
35 | result += str[++i]; | ||||
36 | else | ||||
37 | result += str[i]; | ||||
38 | } | ||||
39 | return result; | ||||
40 | } | ||||
41 | | ||||
42 | // | ||||
43 | // Template::Element | ||||
44 | // | ||||
45 | const QString Template::Element::findFmt(Var::DataType type) const { | ||||
46 | const Template::Element *element; | ||||
47 | for(element = this; element != nullptr; element = element->parent) { | ||||
48 | if(!element->fmt.isEmpty() && isValidFmt(element->fmt, type)) { | ||||
49 | return element->fmt; | ||||
50 | } | ||||
51 | } | ||||
52 | return defaultFmt(type); | ||||
53 | } | ||||
54 | | ||||
55 | QString Template::Element::path() const { | ||||
56 | QStringList namesList; | ||||
57 | const Template::Element *element; | ||||
58 | for(element = this; element != nullptr; element = element->parent) { | ||||
59 | if(!element->hasName() && element->parent != nullptr) { | ||||
60 | QString anonName = QStringLiteral("[anon]"); | ||||
61 | for(int i = 0; i < element->parent->children.size(); ++i) { | ||||
62 | if(&element->parent->children[i] == element) { | ||||
63 | anonName = QStringLiteral("[%1]").arg(i); | ||||
64 | break; | ||||
65 | } | ||||
66 | } | ||||
67 | namesList.prepend(anonName); | ||||
68 | } else { | ||||
69 | namesList.prepend(element->name); | ||||
70 | } | ||||
71 | } | ||||
72 | return namesList.join(QLatin1Char('.')); | ||||
73 | } | ||||
74 | | ||||
75 | const QString Template::Element::defaultFmt(Var::DataType type) { | ||||
76 | switch(type) { | ||||
77 | case Var::DataType::Number: return QStringLiteral("%d"); | ||||
78 | case Var::DataType::String: return QStringLiteral("%s"); | ||||
79 | default: Q_UNREACHABLE(); | ||||
80 | } | ||||
81 | } | ||||
82 | | ||||
83 | bool Template::Element::isValidFmt(const QString &fmt, Var::DataType type) { | ||||
84 | switch(type) { | ||||
85 | case Var::DataType::String: return fmt.endsWith(QLatin1Char('s')); | ||||
86 | case Var::DataType::Number: return true; // regexp in parser takes care of it | ||||
87 | default: return false; | ||||
88 | } | ||||
89 | } | ||||
90 | | ||||
91 | // | ||||
92 | // Template | ||||
93 | // | ||||
94 | | ||||
95 | Template::Template(const QString &text): _text(text) { | ||||
96 | _root.name = QStringLiteral("[root]"); | ||||
97 | _root.outer = QStringRef(&_text); | ||||
98 | _root.inner = QStringRef(&_text); | ||||
99 | _root.parent = nullptr; | ||||
100 | _root.line = 1; | ||||
101 | _root.column = 1; | ||||
102 | } | ||||
103 | | ||||
104 | void Template::parse() { | ||||
105 | _root.children.clear(); | ||||
106 | _root.outer = QStringRef(&_text); | ||||
107 | _root.inner = QStringRef(&_text); | ||||
108 | parseRecursively(_root); | ||||
109 | // dbgDumpTree(_root); | ||||
110 | } | ||||
111 | | ||||
112 | QString Template::generate(const Var &data) { | ||||
113 | QString result; | ||||
114 | result.reserve(_text.size()); | ||||
115 | generateRecursively(result, _root, data); | ||||
116 | return result; | ||||
117 | } | ||||
118 | | ||||
119 | static inline void warn(const Template::Element &element, const QString &id, const QString &msg) { | ||||
120 | const QString path = id.isEmpty() ? element.path() : Template::Element(&element, id).path(); | ||||
121 | qWarning() << QStringLiteral("Warning: %1:%2: %3: %4").arg(element.line).arg(element.column).arg(path, msg); | ||||
122 | } | ||||
123 | static inline void warn(const Template::Element &element, const QString &msg) { | ||||
124 | warn(element, QString(), msg); | ||||
125 | } | ||||
126 | | ||||
127 | void Template::executeCommand(Element &element, const Template::Element &childStub, const QStringList &argv) { | ||||
128 | // Insert content N times | ||||
129 | if(argv[0] == QStringLiteral("repeat")) { | ||||
130 | bool ok; | ||||
131 | unsigned count = argv.value(1).toInt(&ok); | ||||
132 | if(!ok || count < 1) { | ||||
133 | warn(element, QStringLiteral("!") + argv[0], QStringLiteral("invalid repeat count (%1), assuming 0.").arg(argv[1])); | ||||
134 | return; | ||||
135 | } | ||||
136 | | ||||
137 | element.children.append(childStub); | ||||
138 | Template::Element &cmdElement = element.children.last(); | ||||
139 | if(!cmdElement.inner.isEmpty()) { | ||||
140 | // Parse children | ||||
141 | parseRecursively(cmdElement); | ||||
142 | // Remember how many children was there before replication | ||||
143 | int originalChildrenCount = cmdElement.children.size(); | ||||
144 | // Replicate children | ||||
145 | for(unsigned i = 1; i < count; ++i) { | ||||
146 | for(int chId = 0; chId < originalChildrenCount; ++chId) { | ||||
147 | cmdElement.children.append(cmdElement.children[chId]); | ||||
148 | } | ||||
149 | } | ||||
150 | } | ||||
151 | // Set printf-like format (with leading %) applied for strings and numbers | ||||
152 | // inside the group | ||||
153 | } else if(argv[0] == QStringLiteral("fmt")) { | ||||
154 | static const QRegularExpression FMT_RE(QStringLiteral(R":(^%[-0 +#]?(?:[1-9][0-9]*)?\.?[0-9]*[diouxXs]$):"), | ||||
155 | QRegularExpression::OptimizeOnFirstUsageOption); | ||||
156 | const auto match = FMT_RE.match(argv.value(1)); | ||||
157 | QString fmt = QStringLiteral(""); | ||||
158 | if(!match.hasMatch()) | ||||
159 | warn(element, QStringLiteral("!") + argv[0], QStringLiteral("invalid format (%1), assuming default").arg(argv[1])); | ||||
160 | else | ||||
161 | fmt = match.captured(); | ||||
162 | | ||||
163 | element.children.append(childStub); | ||||
164 | Template::Element &cmdElement = element.children.last(); | ||||
165 | cmdElement.fmt = fmt; | ||||
166 | parseRecursively(cmdElement); | ||||
167 | } | ||||
168 | } | ||||
169 | | ||||
170 | void Template::parseRecursively(Element &element) { | ||||
171 | static const QRegularExpression RE(QStringLiteral(R":((?'comment'«\*(([^:]*):)?.*?(?(-2):\g{-1})\*»)|):" | ||||
172 | R":(«(?:(?'name'[-_a-zA-Z0-9]*)|(?:!(?'cmd'[-_a-zA-Z0-9]+(?: +(?:[^\\:]+|(?:\\.)+)+)?)))):" | ||||
173 | R":((?::(?:~[ \t]*\n)?(?'inner'(?:[^«]*?|(?R))*))?(?:\n[ \t]*~)?»):"), | ||||
174 | QRegularExpression::DotMatchesEverythingOption | QRegularExpression::MultilineOption | | ||||
175 | QRegularExpression::OptimizeOnFirstUsageOption); | ||||
176 | static const QRegularExpression CMD_SPLIT_RE(QStringLiteral(R":((?:"((?:(?:\\.)*|[^"]*)*)"|(?:[^\\ "]+|(?:\\.)+)+)):"), | ||||
177 | QRegularExpression::DotMatchesEverythingOption | QRegularExpression::MultilineOption | | ||||
178 | QRegularExpression::OptimizeOnFirstUsageOption); | ||||
179 | static const QRegularExpression UNESCAPE_RE(QStringLiteral(R":(\\(.)):"), | ||||
180 | QRegularExpression::DotMatchesEverythingOption | QRegularExpression::MultilineOption | | ||||
181 | QRegularExpression::OptimizeOnFirstUsageOption); | ||||
182 | static const QString nameGroupName = QStringLiteral("name"); | ||||
183 | static const QString innerGroupName = QStringLiteral("inner"); | ||||
184 | static const QString cmdGroupName = QStringLiteral("cmd"); | ||||
185 | static const QString commentGroupName = QStringLiteral("comment"); | ||||
186 | | ||||
187 | int posOffset = element.outer.position(); | ||||
188 | uint posLine = element.line; | ||||
189 | uint posColumn = element.column; | ||||
190 | | ||||
191 | auto matchIter = RE.globalMatch(element.inner); | ||||
192 | while(matchIter.hasNext()) { | ||||
193 | auto match = matchIter.next(); | ||||
194 | auto cmd = match.captured(cmdGroupName); | ||||
195 | auto comment = match.captured(commentGroupName); | ||||
196 | | ||||
197 | const auto localOuterRef = match.capturedRef(0); | ||||
198 | const auto localInnerRef = match.capturedRef(innerGroupName); | ||||
199 | | ||||
200 | auto outerRef = QStringRef(&_text, localOuterRef.position(), localOuterRef.length()); | ||||
201 | auto innerRef = QStringRef(&_text, localInnerRef.position(), localInnerRef.length()); | ||||
202 | | ||||
203 | while(posOffset < outerRef.position() && posOffset < _text.size()) { | ||||
204 | if(_text[posOffset++] == QLatin1Char('\n')) { | ||||
205 | ++posLine; | ||||
206 | posColumn = 1; | ||||
207 | } else { | ||||
208 | ++posColumn; | ||||
209 | } | ||||
210 | } | ||||
211 | | ||||
212 | if(!cmd.isEmpty()) { | ||||
213 | QStringList cmdArgv; | ||||
214 | auto cmdArgIter = CMD_SPLIT_RE.globalMatch(cmd); | ||||
215 | while(cmdArgIter.hasNext()) { | ||||
216 | auto cmdArg = cmdArgIter.next(); | ||||
217 | cmdArgv += cmdArg.captured(cmdArg.captured(1).isEmpty() ? 0 : 1); | ||||
218 | cmdArgv.last().replace(UNESCAPE_RE, QStringLiteral("\1")); | ||||
219 | } | ||||
220 | | ||||
221 | Template::Element childStub = Template::Element(&element); | ||||
222 | childStub.outer = outerRef; | ||||
223 | childStub.name = QLatin1Char('!') + cmd; | ||||
224 | childStub.inner = innerRef; | ||||
225 | childStub.line = posLine; | ||||
226 | childStub.column = posColumn; | ||||
227 | executeCommand(element, childStub, cmdArgv); | ||||
228 | } else if (!comment.isEmpty()) { | ||||
229 | element.children.append(Element(&element)); | ||||
230 | Template::Element &child = element.children.last(); | ||||
231 | child.outer = outerRef; | ||||
232 | child.name = QString(); | ||||
233 | child.inner = QStringRef(); | ||||
234 | child.line = posLine; | ||||
235 | child.column = posColumn; | ||||
236 | child.isComment = true; | ||||
237 | } else { | ||||
238 | element.children.append(Element(&element)); | ||||
239 | Template::Element &child = element.children.last(); | ||||
240 | child.outer = outerRef; | ||||
241 | child.name = match.captured(nameGroupName); | ||||
242 | child.inner = innerRef; | ||||
243 | child.line = posLine; | ||||
244 | child.column = posColumn; | ||||
245 | if(!child.inner.isEmpty()) | ||||
246 | parseRecursively(child); | ||||
247 | } | ||||
248 | } | ||||
249 | } | ||||
250 | | ||||
251 | int Template::generateRecursively(QString &result, const Template::Element &element, const Var &data, int consumed) { | ||||
252 | int consumedDataItems = consumed; | ||||
253 | | ||||
254 | if(!element.children.isEmpty()) { | ||||
255 | int totalDataItems; | ||||
256 | switch(data.dataType()) { | ||||
257 | case Var::DataType::Number: | ||||
258 | case Var::DataType::String: | ||||
259 | case Var::DataType::Map: | ||||
260 | totalDataItems = 1; | ||||
261 | break; | ||||
262 | case Var::DataType::Vector: | ||||
263 | totalDataItems = data.vec.size(); | ||||
264 | break; | ||||
265 | case Var::DataType::Invalid: | ||||
266 | default: | ||||
267 | Q_UNREACHABLE(); | ||||
268 | } | ||||
269 | | ||||
270 | while(consumedDataItems < totalDataItems) { | ||||
271 | int prevChildEndPosition = element.inner.position(); | ||||
272 | for(const auto &child: element.children) { | ||||
273 | const int characterCountBetweenChildren = child.outer.position() - prevChildEndPosition; | ||||
274 | if(characterCountBetweenChildren > 0) { | ||||
275 | // Add text between previous child (or inner beginning) and this child. | ||||
276 | result += unescape(_text.midRef(prevChildEndPosition, characterCountBetweenChildren)); | ||||
277 | } else if(characterCountBetweenChildren < 0) { | ||||
278 | // Repeated item; they overlap and end1 > start2 | ||||
279 | result += unescape(element.inner.mid(prevChildEndPosition - element.inner.position())); | ||||
280 | result += unescape(element.inner.left(child.outer.position() - element.inner.position())); | ||||
281 | } | ||||
282 | | ||||
283 | switch(data.dataType()) { | ||||
284 | case Var::DataType::Number: | ||||
285 | case Var::DataType::String: | ||||
286 | generateRecursively(result, child, data); | ||||
287 | consumedDataItems = 1; // Deepest child always consumes number/string | ||||
288 | break; | ||||
289 | case Var::DataType::Vector: | ||||
290 | if(!data.vec.isEmpty()) { | ||||
291 | if(!child.hasName() && !child.isCommand() && consumedDataItems < data.vec.size()) { | ||||
292 | consumedDataItems += generateRecursively(result, child, data[consumedDataItems]); | ||||
293 | } else { | ||||
294 | consumedDataItems += generateRecursively(result, child, data.vec.mid(consumedDataItems)); | ||||
295 | } | ||||
296 | } else { | ||||
297 | warn(child, QStringLiteral("no more items available in parent's list.")); | ||||
298 | } | ||||
299 | break; | ||||
300 | case Var::DataType::Map: | ||||
301 | if(!child.hasName()) { | ||||
302 | consumedDataItems = generateRecursively(result, child, data); | ||||
303 | } else if(data.map.contains(child.name)) { | ||||
304 | generateRecursively(result, child, data.map[child.name]); | ||||
305 | // Always consume, repeating doesn't change anything | ||||
306 | consumedDataItems = 1; | ||||
307 | } else { | ||||
308 | warn(child, QStringLiteral("missing value for the element in parent's map.")); | ||||
309 | } | ||||
310 | break; | ||||
311 | default: | ||||
312 | break; | ||||
313 | } | ||||
314 | prevChildEndPosition = child.outer.position() + child.outer.length(); | ||||
315 | } | ||||
316 | | ||||
317 | result += unescape(element.inner.mid(prevChildEndPosition - element.inner.position(), -1)); | ||||
318 | | ||||
319 | if(element.isCommand()) { | ||||
320 | break; | ||||
321 | } | ||||
322 | | ||||
323 | const bool isLast = consumedDataItems >= totalDataItems; | ||||
324 | if(!isLast) { | ||||
325 | // Collapse empty lines between elements | ||||
326 | int nlNum = 0; | ||||
327 | for(int i = 0; i < element.inner.size() / 2; ++i) { | ||||
328 | if(element.inner.at(i) == QLatin1Char('\n') && | ||||
329 | element.inner.at(i) == element.inner.at(element.inner.size() - i - 1)) | ||||
330 | nlNum++; | ||||
331 | else | ||||
332 | break; | ||||
333 | } | ||||
334 | if(nlNum > 0) | ||||
335 | result.chop(nlNum); | ||||
336 | } | ||||
337 | } | ||||
338 | } else if (!element.isComment) { | ||||
339 | // Handle leaf element | ||||
340 | switch(data.dataType()) { | ||||
341 | case Var::DataType::Number: { | ||||
342 | const QString fmt = element.findFmt(Var::DataType::Number); | ||||
343 | result += QString::asprintf(qUtf8Printable(fmt), data.num); | ||||
344 | break; | ||||
345 | } | ||||
346 | case Var::DataType::String: { | ||||
347 | const QString fmt = element.findFmt(Var::DataType::String); | ||||
348 | result += QString::asprintf(qUtf8Printable(fmt), qUtf8Printable(data.str)); | ||||
349 | break; | ||||
350 | } | ||||
351 | case Var::DataType::Vector: | ||||
352 | if(data.vec.isEmpty()) { | ||||
353 | warn(element, QStringLiteral("got empty list.")); | ||||
354 | } else if(data.vec.at(0).dataType() == Var::DataType::Number) { | ||||
355 | const QString fmt = element.findFmt(Var::DataType::Number); | ||||
356 | result += QString::asprintf(qUtf8Printable(fmt), data.num); | ||||
357 | } else if(data.vec.at(0).dataType() == Var::DataType::String) { | ||||
358 | const QString fmt = element.findFmt(Var::DataType::String); | ||||
359 | result += QString::asprintf(qUtf8Printable(fmt), qUtf8Printable(data.str)); | ||||
360 | } else { | ||||
361 | warn(element, QStringLiteral("the list entry data type (%1) is not supported in childrenless elements."). | ||||
362 | arg(data.vec.at(0).dataTypeAsString())); | ||||
363 | } | ||||
364 | break; | ||||
365 | case Var::DataType::Map: | ||||
366 | warn(element, QStringLiteral("map type is not supported in childrenless elements.")); | ||||
367 | break; | ||||
368 | case Var::DataType::Invalid: | ||||
369 | break; | ||||
370 | } | ||||
371 | consumedDataItems = 1; | ||||
372 | } | ||||
373 | | ||||
374 | return consumedDataItems; | ||||
375 | } | ||||
376 | | ||||
377 | /* | ||||
378 | void dbgDumpTree(const Template::Element &element) { | ||||
379 | static int indent = 0; | ||||
380 | QString type; | ||||
381 | if(element.isCommand()) | ||||
382 | type = QStringLiteral("command"); | ||||
383 | else if(element.isComment) | ||||
384 | type = QStringLiteral("comment"); | ||||
385 | else if(element.hasName() && element.inner.isEmpty()) | ||||
386 | type = QStringLiteral("empty named"); | ||||
387 | else if(element.hasName()) | ||||
388 | type = QStringLiteral("named"); | ||||
389 | else if(element.inner.isEmpty()) | ||||
390 | type = QStringLiteral("empty anonymous"); | ||||
391 | else | ||||
392 | type = QStringLiteral("anonymous"); | ||||
393 | | ||||
394 | qDebug().noquote() << QStringLiteral("%1[%2] \"%3\" %4:%5") | ||||
395 | .arg(QStringLiteral("· ").repeated(indent), type, element.name) | ||||
396 | .arg(element.line) | ||||
397 | .arg(element.column); | ||||
398 | indent++; | ||||
399 | for(const auto &child: element.children) { | ||||
400 | dbgDumpTree(child); | ||||
401 | } | ||||
402 | indent--; | ||||
403 | } | ||||
404 | */ |