1/****************************************************************************
2**
3** Copyright (C) 2016 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the QtQml module of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:GPL-EXCEPT$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU
19** General Public License version 3 as published by the Free Software
20** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
21** included in the packaging of this file. Please review the following
22** information to ensure the GNU General Public License requirements will
23** be met: https://www.gnu.org/licenses/gpl-3.0.html.
24**
25** $QT_END_LICENSE$
26**
27****************************************************************************/
28
29#include <private/qqmljsengine_p.h>
30#include <private/qqmljslexer_p.h>
31#include <private/qqmljsparser_p.h>
32#include <QtCore/QCoreApplication>
33#include <QtCore/QStringList>
34#include <QtCore/QFile>
35#include <QtCore/QFileInfo>
36#include <QtCore/QDir>
37#include <iostream>
38#include <cstdlib>
39
40QT_BEGIN_NAMESPACE
41
42//
43// QML/JS minifier
44//
45namespace QQmlJS {
46
47enum RegExpFlag {
48 Global = 0x01,
49 IgnoreCase = 0x02,
50 Multiline = 0x04
51};
52
53
54class QmlminLexer: protected Lexer, public Directives
55{
56 QQmlJS::Engine _engine;
57 QString _fileName;
58 QString _directives;
59
60protected:
61 QVector<int> _stateStack;
62 QList<int> _tokens;
63 QList<QString> _tokenStrings;
64 int yytoken = -1;
65 QString yytokentext;
66
67 void lex() {
68 if (_tokens.isEmpty()) {
69 _tokens.append(t: Lexer::lex());
70 _tokenStrings.append(t: tokenText());
71 }
72
73 yytoken = _tokens.takeFirst();
74 yytokentext = _tokenStrings.takeFirst();
75 }
76
77 int lookaheadToken()
78 {
79 if (yytoken < 0)
80 lex();
81 return yytoken;
82 }
83
84 void pushToken(int token)
85 {
86 _tokens.prepend(t: yytoken);
87 _tokenStrings.prepend(t: yytokentext);
88 yytoken = token;
89 yytokentext = QString();
90 }
91
92public:
93 QmlminLexer()
94 : Lexer(&_engine), _stateStack(128) {}
95 virtual ~QmlminLexer() {}
96
97 QString fileName() const { return _fileName; }
98
99 bool operator()(const QString &fileName, const QString &code)
100 {
101 int startToken = T_FEED_JS_SCRIPT;
102 const QFileInfo fileInfo(fileName);
103 if (fileInfo.suffix().toLower() == QLatin1String("qml"))
104 startToken = T_FEED_UI_PROGRAM;
105 setCode(code, /*line = */ lineno: 1, /*qmlMode = */ startToken == T_FEED_UI_PROGRAM);
106 _fileName = fileName;
107 _directives.clear();
108 return parse(startToken);
109 }
110
111 QString directives()
112 {
113 return _directives;
114 }
115
116 //
117 // Handle the .pragma/.import directives
118 //
119 void pragmaLibrary() override
120 {
121 _directives += QLatin1String(".pragma library\n");
122 }
123
124 void importFile(const QString &jsfile, const QString &module, int line, int column) override
125 {
126 _directives += QLatin1String(".import");
127 _directives += QLatin1Char('"');
128 _directives += quote(string: jsfile);
129 _directives += QLatin1Char('"');
130 _directives += QLatin1String("as ");
131 _directives += module;
132 _directives += QLatin1Char('\n');
133 Q_UNUSED(line);
134 Q_UNUSED(column);
135 }
136
137 void importModule(const QString &uri, const QString &version, const QString &module, int line, int column) override
138 {
139 _directives += QLatin1String(".import ");
140 _directives += uri;
141 _directives += QLatin1Char(' ');
142 _directives += version;
143 _directives += QLatin1String(" as ");
144 _directives += module;
145 _directives += QLatin1Char('\n');
146 Q_UNUSED(line);
147 Q_UNUSED(column);
148 }
149
150protected:
151 virtual bool parse(int startToken) = 0;
152
153 static QString quote(const QString &string)
154 {
155 QString quotedString;
156 for (const QChar &ch : string) {
157 if (ch == QLatin1Char('"'))
158 quotedString += QLatin1String("\\\"");
159 else {
160 if (ch == QLatin1Char('\\')) quotedString += QLatin1String("\\\\");
161 else if (ch == QLatin1Char('\"')) quotedString += QLatin1String("\\\"");
162 else if (ch == QLatin1Char('\b')) quotedString += QLatin1String("\\b");
163 else if (ch == QLatin1Char('\f')) quotedString += QLatin1String("\\f");
164 else if (ch == QLatin1Char('\n')) quotedString += QLatin1String("\\n");
165 else if (ch == QLatin1Char('\r')) quotedString += QLatin1String("\\r");
166 else if (ch == QLatin1Char('\t')) quotedString += QLatin1String("\\t");
167 else if (ch == QLatin1Char('\v')) quotedString += QLatin1String("\\v");
168 else if (ch == QLatin1Char('\0')) quotedString += QLatin1String("\\0");
169 else quotedString += ch;
170 }
171 }
172 return quotedString;
173 }
174
175 bool isIdentChar(const QChar &ch) const
176 {
177 if (ch.isLetterOrNumber())
178 return true;
179 else if (ch == QLatin1Char('_') || ch == QLatin1Char('$'))
180 return true;
181 return false;
182 }
183
184 bool isRegExpRule(int ruleno) const
185 {
186 return ruleno == J_SCRIPT_REGEXPLITERAL_RULE1 ||
187 ruleno == J_SCRIPT_REGEXPLITERAL_RULE2;
188 }
189
190 void handleLookaheads(int ruleno) {
191 if (ruleno == J_SCRIPT_EXPRESSIONSTATEMENTLOOKAHEAD_RULE) {
192 int token = lookaheadToken();
193 if (token == T_LBRACE)
194 pushToken(token: T_FORCE_BLOCK);
195 else if (token == T_FUNCTION || token == T_CLASS || token == T_LET || token == T_CONST)
196 pushToken(token: T_FORCE_DECLARATION);
197 } else if (ruleno == J_SCRIPT_CONCISEBODYLOOKAHEAD_RULE) {
198 int token = lookaheadToken();
199 if (token == T_LBRACE)
200 pushToken(token: T_FORCE_BLOCK);
201 } else if (ruleno == J_SCRIPT_EXPORTDECLARATIONLOOKAHEAD_RULE) {
202 int token = lookaheadToken();
203 if (token == T_FUNCTION || token == T_CLASS)
204 pushToken(token: T_FORCE_DECLARATION);
205 }
206 }
207
208 bool scanRestOfRegExp(int ruleno, QString *restOfRegExp)
209 {
210 if (! scanRegExp(prefix: ruleno == J_SCRIPT_REGEXPLITERAL_RULE1 ? Lexer::NoPrefix : Lexer::EqualPrefix))
211 return false;
212
213 *restOfRegExp = regExpPattern();
214 if (ruleno == J_SCRIPT_REGEXPLITERAL_RULE2) {
215 Q_ASSERT(! restOfRegExp->isEmpty());
216 Q_ASSERT(restOfRegExp->at(0) == QLatin1Char('='));
217 *restOfRegExp = restOfRegExp->mid(position: 1); // strip the prefix
218 }
219 *restOfRegExp += QLatin1Char('/');
220 const RegExpFlag flags = (RegExpFlag) regExpFlags();
221 if (flags & Global)
222 *restOfRegExp += QLatin1Char('g');
223 if (flags & IgnoreCase)
224 *restOfRegExp += QLatin1Char('i');
225 if (flags & Multiline)
226 *restOfRegExp += QLatin1Char('m');
227
228 if (regExpFlags() == 0) {
229 // Add an extra space after the regexp literal delimiter (aka '/').
230 // This will avoid possible problems when pasting tokens like `instanceof'
231 // after the regexp literal.
232 *restOfRegExp += QLatin1Char(' ');
233 }
234 return true;
235 }
236};
237
238
239class Minify: public QmlminLexer
240{
241 QString _minifiedCode;
242 int _maxWidth;
243 int _width;
244
245public:
246 Minify(int maxWidth);
247
248 QString minifiedCode() const;
249
250protected:
251 void append(const QString &s);
252 bool parse(int startToken) override;
253 void escape(const QChar &ch, QString *out);
254};
255
256Minify::Minify(int maxWidth)
257 : _maxWidth(maxWidth), _width(0)
258{
259}
260
261QString Minify::minifiedCode() const
262{
263 return _minifiedCode;
264}
265
266void Minify::append(const QString &s)
267{
268 if (!s.isEmpty()) {
269 if (_maxWidth) {
270 // Prefer not to exceed the maximum chars per line (but don't break up segments)
271 int segmentLength = s.count();
272 if (_width && ((_width + segmentLength) > _maxWidth)) {
273 _minifiedCode.append(c: QLatin1Char('\n'));
274 _width = 0;
275 }
276
277 _width += segmentLength;
278 }
279
280 _minifiedCode.append(s);
281 }
282}
283
284void Minify::escape(const QChar &ch, QString *out)
285{
286 out->append(s: QLatin1String("\\u"));
287 const QString hx = QString::number(ch.unicode(), base: 16);
288 switch (hx.length()) {
289 case 1: out->append(s: QLatin1String("000")); break;
290 case 2: out->append(s: QLatin1String("00")); break;
291 case 3: out->append(c: QLatin1Char('0')); break;
292 case 4: break;
293 default: Q_ASSERT(!"unreachable");
294 }
295 out->append(s: hx);
296}
297
298bool Minify::parse(int startToken)
299{
300 int yyaction = 0;
301 int yytos = -1;
302 QString assembled;
303
304 _minifiedCode.clear();
305 _tokens.append(t: startToken);
306 _tokenStrings.append(t: QString());
307
308 if (startToken == T_FEED_JS_SCRIPT) {
309 // parse optional pragma directive
310 DiagnosticMessage error;
311 if (scanDirectives(directives: this, error: &error)) {
312 // append the scanned directives to the minifier code.
313 append(s: directives());
314
315 _tokens.append(t: tokenKind());
316 _tokenStrings.append(t: tokenText());
317 } else {
318 std::cerr << qPrintable(fileName()) << ':' << tokenStartLine() << ':'
319 << tokenStartColumn() << ": syntax error" << std::endl;
320 return false;
321 }
322 }
323
324 do {
325 if (++yytos == _stateStack.size())
326 _stateStack.resize(asize: _stateStack.size() * 2);
327
328 _stateStack[yytos] = yyaction;
329
330 again:
331 if (yytoken == -1 && action_index[yyaction] != -TERMINAL_COUNT)
332 lex();
333
334 yyaction = t_action(yyaction, yytoken);
335 if (yyaction > 0) {
336 if (yyaction == ACCEPT_STATE) {
337 --yytos;
338 if (!assembled.isEmpty())
339 append(s: assembled);
340 return true;
341 }
342
343 const QChar lastChar = assembled.isEmpty() ? (_minifiedCode.isEmpty() ? QChar()
344 : _minifiedCode.at(i: _minifiedCode.length() - 1))
345 : assembled.at(i: assembled.length() - 1);
346
347 if (yytoken == T_SEMICOLON) {
348 assembled += QLatin1Char(';');
349
350 append(s: assembled);
351 assembled.clear();
352
353 } else if (yytoken == T_PLUS || yytoken == T_MINUS || yytoken == T_PLUS_PLUS || yytoken == T_MINUS_MINUS) {
354 if (lastChar == QLatin1Char(spell[yytoken][0])) {
355 // don't merge unary signs, additive expressions and postfix/prefix increments.
356 assembled += QLatin1Char(' ');
357 }
358
359 assembled += QLatin1String(spell[yytoken]);
360
361 } else if (yytoken == T_NUMERIC_LITERAL) {
362 if (isIdentChar(ch: lastChar))
363 assembled += QLatin1Char(' ');
364
365 if (yytokentext.startsWith(c: '.'))
366 assembled += QLatin1Char('0');
367
368 assembled += yytokentext;
369
370 if (assembled.endsWith(c: QLatin1Char('.')))
371 assembled += QLatin1Char('0');
372
373 } else if (yytoken == T_IDENTIFIER) {
374 QString identifier = yytokentext;
375
376 if (classify(identifier.constData(), identifier.size(), qmlMode()) != T_IDENTIFIER) {
377 // the unescaped identifier is a keyword. In this case just replace
378 // the last character of the identifier with it escape sequence.
379 const QChar ch = identifier.at(i: identifier.length() - 1);
380 identifier.chop(n: 1);
381 escape(ch, out: &identifier);
382 }
383
384 if (isIdentChar(ch: lastChar))
385 assembled += QLatin1Char(' ');
386
387 assembled += identifier;
388
389 } else if (yytoken == T_STRING_LITERAL || yytoken == T_MULTILINE_STRING_LITERAL) {
390 assembled += QLatin1Char('"');
391 assembled += quote(string: yytokentext);
392 assembled += QLatin1Char('"');
393 } else {
394 if (isIdentChar(ch: lastChar)) {
395 if (! yytokentext.isEmpty()) {
396 const QChar ch = yytokentext.at(i: 0);
397 if (isIdentChar(ch))
398 assembled += QLatin1Char(' ');
399 }
400 }
401 assembled += yytokentext;
402 }
403 yytoken = -1;
404 } else if (yyaction < 0) {
405 const int ruleno = -yyaction - 1;
406 yytos -= rhs[ruleno];
407
408 handleLookaheads(ruleno);
409
410 if (isRegExpRule(ruleno)) {
411 QString restOfRegExp;
412
413 if (! scanRestOfRegExp(ruleno, restOfRegExp: &restOfRegExp))
414 break; // break the loop, it wil report a syntax error
415
416 assembled += restOfRegExp;
417 }
418 yyaction = nt_action(_stateStack[yytos], lhs[ruleno] - TERMINAL_COUNT);
419 }
420 } while (yyaction);
421
422 const int yyerrorstate = _stateStack[yytos];
423
424 // automatic insertion of `;'
425 if (yytoken != -1 && ((t_action(yyerrorstate, T_AUTOMATIC_SEMICOLON) && canInsertAutomaticSemicolon(yytoken))
426 || t_action(yyerrorstate, T_COMPATIBILITY_SEMICOLON))) {
427 _tokens.prepend(t: yytoken);
428 _tokenStrings.prepend(t: yytokentext);
429 yyaction = yyerrorstate;
430 yytoken = T_SEMICOLON;
431 goto again;
432 }
433
434 std::cerr << qPrintable(fileName()) << ':' << tokenStartLine() << ':' << tokenStartColumn()
435 << ": syntax error" << std::endl;
436 return false;
437}
438
439
440class Tokenize: public QmlminLexer
441{
442 QStringList _minifiedCode;
443
444public:
445 Tokenize() {}
446
447 QStringList tokenStream() const;
448
449protected:
450 bool parse(int startToken) override;
451};
452
453QStringList Tokenize::tokenStream() const
454{
455 return _minifiedCode;
456}
457
458bool Tokenize::parse(int startToken)
459{
460 int yyaction = 0;
461 int yytos = -1;
462
463 _minifiedCode.clear();
464 _tokens.append(t: startToken);
465 _tokenStrings.append(t: QString());
466
467 if (startToken == T_FEED_JS_SCRIPT) {
468 // parse optional pragma directive
469 DiagnosticMessage error;
470 if (scanDirectives(directives: this, error: &error)) {
471 // append the scanned directives as one token to
472 // the token stream.
473 _minifiedCode.append(t: directives());
474
475 _tokens.append(t: tokenKind());
476 _tokenStrings.append(t: tokenText());
477 } else {
478 std::cerr << qPrintable(fileName()) << ':' << tokenStartLine() << ':'
479 << tokenStartColumn() << ": syntax error" << std::endl;
480 return false;
481 }
482 }
483
484 do {
485 if (++yytos == _stateStack.size())
486 _stateStack.resize(asize: _stateStack.size() * 2);
487
488 _stateStack[yytos] = yyaction;
489
490 again:
491 if (yytoken == -1 && action_index[yyaction] != -TERMINAL_COUNT)
492 lex();
493
494 yyaction = t_action(yyaction, yytoken);
495 if (yyaction > 0) {
496 if (yyaction == ACCEPT_STATE) {
497 --yytos;
498 return true;
499 }
500
501 if (yytoken == T_SEMICOLON)
502 _minifiedCode += QLatin1String(";");
503 else
504 _minifiedCode += yytokentext;
505
506 yytoken = -1;
507 } else if (yyaction < 0) {
508 const int ruleno = -yyaction - 1;
509 yytos -= rhs[ruleno];
510
511 handleLookaheads(ruleno);
512
513 if (isRegExpRule(ruleno)) {
514 QString restOfRegExp;
515
516 if (! scanRestOfRegExp(ruleno, restOfRegExp: &restOfRegExp))
517 break; // break the loop, it wil report a syntax error
518
519 _minifiedCode.last().append(s: restOfRegExp);
520 }
521
522 yyaction = nt_action(_stateStack[yytos], lhs[ruleno] - TERMINAL_COUNT);
523 }
524 } while (yyaction);
525
526 const int yyerrorstate = _stateStack[yytos];
527
528 // automatic insertion of `;'
529 if (yytoken != -1 && ((t_action(yyerrorstate, T_AUTOMATIC_SEMICOLON) && canInsertAutomaticSemicolon(yytoken))
530 || t_action(yyerrorstate, T_COMPATIBILITY_SEMICOLON))) {
531 _tokens.prepend(t: yytoken);
532 _tokenStrings.prepend(t: yytokentext);
533 yyaction = yyerrorstate;
534 yytoken = T_SEMICOLON;
535 goto again;
536 }
537
538 std::cerr << qPrintable(fileName()) << ':' << tokenStartLine() << ':'
539 << tokenStartColumn() << ": syntax error" << std::endl;
540 return false;
541}
542
543} // end of QQmlJS namespace
544
545static void usage(bool showHelp = false)
546{
547 std::cerr << "Usage: qmlmin [options] file" << std::endl;
548
549 if (showHelp) {
550 std::cerr << " Removes comments and layout characters" << std::endl
551 << " The options are:" << std::endl
552 << " -o<file> write output to file rather than stdout" << std::endl
553 << " -v --verify-only just run the verifier, no output" << std::endl
554 << " -w<width> restrict line characters to width" << std::endl
555 << " -h display this output" << std::endl;
556 }
557}
558
559int runQmlmin(int argc, char *argv[])
560{
561 QCoreApplication app(argc, argv);
562 QCoreApplication::setApplicationVersion(QLatin1String(QT_VERSION_STR));
563
564 const QStringList args = app.arguments();
565
566 QString fileName;
567 QString outputFile;
568 bool verifyOnly = false;
569
570 // By default ensure the output character width is less than 16-bits (pass 0 to disable)
571 int width = USHRT_MAX;
572
573 int index = 1;
574 while (index < args.size()) {
575 const QString arg = args.at(i: index++);
576 const QString next = index < args.size() ? args.at(i: index) : QString();
577
578 if (arg == QLatin1String("-h") || arg == QLatin1String("--help")) {
579 usage(/*showHelp*/ true);
580 return 0;
581 } else if (arg == QLatin1String("-v") || arg == QLatin1String("--verify-only")) {
582 verifyOnly = true;
583 } else if (arg == QLatin1String("-o")) {
584 if (next.isEmpty()) {
585 std::cerr << "qmlmin: argument to '-o' is missing" << std::endl;
586 return EXIT_FAILURE;
587 } else {
588 outputFile = next;
589 ++index; // consume the next argument
590 }
591 } else if (arg.startsWith(s: QLatin1String("-o"))) {
592 outputFile = arg.mid(position: 2);
593
594 if (outputFile.isEmpty()) {
595 std::cerr << "qmlmin: argument to '-o' is missing" << std::endl;
596 return EXIT_FAILURE;
597 }
598 } else if (arg == QLatin1String("-w")) {
599 if (next.isEmpty()) {
600 std::cerr << "qmlmin: argument to '-w' is missing" << std::endl;
601 return EXIT_FAILURE;
602 } else {
603 bool ok;
604 width = next.toInt(ok: &ok);
605
606 if (!ok) {
607 std::cerr << "qmlmin: argument to '-w' is invalid" << std::endl;
608 return EXIT_FAILURE;
609 }
610
611 ++index; // consume the next argument
612 }
613 } else if (arg.startsWith(s: QLatin1String("-w"))) {
614 bool ok;
615 width = arg.midRef(position: 2).toInt(ok: &ok);
616
617 if (!ok) {
618 std::cerr << "qmlmin: argument to '-w' is invalid" << std::endl;
619 return EXIT_FAILURE;
620 }
621 } else {
622 const bool isInvalidOpt = arg.startsWith(c: QLatin1Char('-'));
623 if (! isInvalidOpt && fileName.isEmpty())
624 fileName = arg;
625 else {
626 usage(/*show help*/ showHelp: isInvalidOpt);
627 if (isInvalidOpt)
628 std::cerr << "qmlmin: invalid option '" << qPrintable(arg) << '\'' << std::endl;
629 else
630 std::cerr << "qmlmin: too many input files specified" << std::endl;
631 return EXIT_FAILURE;
632 }
633 }
634 }
635
636 if (fileName.isEmpty()) {
637 usage();
638 return 0;
639 }
640
641 std::cerr << "qmlmin: This tool is deprecated and will be removed in Qt 6. It is not needed anymore due to QtQml's built-in caching." << std::endl;
642
643 QFile file(fileName);
644 if (! file.open(flags: QFile::ReadOnly)) {
645 std::cerr << "qmlmin: '" << qPrintable(fileName) << "' no such file or directory" << std::endl;
646 return EXIT_FAILURE;
647 }
648
649 const QString code = QString::fromUtf8(str: file.readAll()); // QML files are UTF-8 encoded.
650 file.close();
651
652 QQmlJS::Minify minify(width);
653 if (! minify(fileName, code)) {
654 std::cerr << "qmlmin: cannot minify '" << qPrintable(fileName) << "' (not a valid QML/JS file)" << std::endl;
655 return EXIT_FAILURE;
656 }
657
658 //
659 // verify the output
660 //
661 QQmlJS::Minify secondMinify(width);
662 if (! secondMinify(fileName, minify.minifiedCode()) || secondMinify.minifiedCode() != minify.minifiedCode()) {
663 std::cerr << "qmlmin: cannot minify '" << qPrintable(fileName) << '\'' << std::endl;
664 return EXIT_FAILURE;
665 }
666
667 QQmlJS::Tokenize originalTokens, minimizedTokens;
668 originalTokens(fileName, code);
669 minimizedTokens(fileName, minify.minifiedCode());
670
671 if (originalTokens.tokenStream().size() != minimizedTokens.tokenStream().size()) {
672 std::cerr << "qmlmin: cannot minify '" << qPrintable(fileName) << '\'' << std::endl;
673 return EXIT_FAILURE;
674 }
675
676 if (! verifyOnly) {
677 if (outputFile.isEmpty()) {
678 const QByteArray chars = minify.minifiedCode().toUtf8();
679 std::cout << chars.constData();
680 } else {
681 QFile file(outputFile);
682 if (! file.open(flags: QFile::WriteOnly)) {
683 std::cerr << "qmlmin: cannot minify '" << qPrintable(fileName) << "' (permission denied)" << std::endl;
684 return EXIT_FAILURE;
685 }
686
687 file.write(data: minify.minifiedCode().toUtf8());
688 file.close();
689 }
690 }
691
692 return 0;
693}
694
695QT_END_NAMESPACE
696
697int main(int argc, char **argv)
698{
699 return QT_PREPEND_NAMESPACE(runQmlmin(argc, argv));
700}
701

source code of qtdeclarative/tools/qmlmin/main.cpp