#include "scan.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "logcat.hpp" QS_LOGGING_CATEGORY(logQmlScanner, "quickshell.qmlscanner", QtWarningMsg); void QmlScanner::scanDir(const QString& path) { if (this->scannedDirs.contains(path)) return; this->scannedDirs.push_back(path); qCDebug(logQmlScanner) << "Scanning directory" << path; auto dir = QDir(path); bool seenQmldir = false; auto singletons = QVector(); auto entries = QVector(); for (auto& entry: dir.entryList(QDir::Files | QDir::NoDotAndDotDot)) { if (entry == "qmldir") { qCDebug(logQmlScanner ) << "Found qmldir file, qmldir synthesization will be disabled for directory" << path; seenQmldir = true; } else if (entry.at(0).isUpper() && entry.endsWith(".qml")) { if (this->scanQmlFile(dir.filePath(entry))) { singletons.push_back(entry); } else { entries.push_back(entry); } } else if (entry.at(0).isUpper() && entry.endsWith(".qml.json")) { this->scanQmlJson(dir.filePath(entry)); singletons.push_back(entry.first(entry.length() - 5)); } } if (!seenQmldir) { qCDebug(logQmlScanner) << "Synthesizing qmldir for directory" << path << "singletons" << singletons; QString qmldir; auto stream = QTextStream(&qmldir); // cant derive a module name if not in shell path if (path.startsWith(this->rootPath.path())) { auto end = path.sliced(this->rootPath.path().length()); // verify we have a valid module name. for (auto& c: end) { if (c == '/') c = '.'; else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_') { } else { qCWarning(logQmlScanner) << "Module path contains invalid characters for a module name: " << path.sliced(this->rootPath.path().length()); goto skipadd; } } stream << "module qs" << end << '\n'; skipadd:; } else { qCWarning(logQmlScanner) << "Module path" << path << "is outside of the config folder."; } for (auto& singleton: singletons) { stream << "singleton " << singleton.sliced(0, singleton.length() - 4) << " 1.0 " << singleton << "\n"; } for (auto& entry: entries) { stream << entry.sliced(0, entry.length() - 4) << " 1.0 " << entry << "\n"; } qCDebug(logQmlScanner) << "Synthesized qmldir for" << path << qPrintable("\n" + qmldir); this->fileIntercepts.insert(QDir(path).filePath("qmldir"), qmldir); } } bool QmlScanner::scanQmlFile(const QString& path) { if (this->scannedFiles.contains(path)) return false; this->scannedFiles.push_back(path); qCDebug(logQmlScanner) << "Scanning qml file" << path; auto file = QFile(path); if (!file.open(QFile::ReadOnly | QFile::Text)) { qCWarning(logQmlScanner) << "Failed to open file" << path; return false; } auto stream = QTextStream(&file); auto imports = QVector(); bool singleton = false; while (!stream.atEnd()) { auto line = stream.readLine().trimmed(); if (!singleton && line == "pragma Singleton") { qCDebug(logQmlScanner) << "Discovered singleton" << path; singleton = true; } else if (line.startsWith("import")) { // we dont care about "import qs" as we always load the root folder if (auto importCursor = line.indexOf(" qs."); importCursor != -1) { importCursor += 4; QString path; while (importCursor != line.length()) { auto c = line.at(importCursor); if (c == '.') c = '/'; else if (c == ' ') break; else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_') { } else { qCWarning(logQmlScanner) << "Import line contains invalid characters: " << line; goto next; } path.append(c); importCursor += 1; } imports.append(this->rootPath.filePath(path)); } else if (auto startQuot = line.indexOf('"'); startQuot != -1 && line.length() >= startQuot + 3) { auto endQuot = line.indexOf('"', startQuot + 1); if (endQuot == -1) continue; auto name = line.sliced(startQuot + 1, endQuot - startQuot - 1); imports.push_back(name); } } else if (line.contains('{')) break; next:; } file.close(); if (logQmlScanner().isDebugEnabled() && !imports.isEmpty()) { qCDebug(logQmlScanner) << "Found imports" << imports; } auto currentdir = QDir(QFileInfo(path).canonicalPath()); // the root can never be a singleton so it dosent matter if we skip it this->scanDir(currentdir.path()); for (auto& import: imports) { QString ipath; if (import.startsWith("root:")) { auto path = import.sliced(5); if (path.startsWith('/')) path = path.sliced(1); ipath = this->rootPath.filePath(path); } else { ipath = currentdir.filePath(import); } auto pathInfo = QFileInfo(ipath); auto cpath = pathInfo.canonicalFilePath(); if (cpath.isEmpty()) { qCWarning(logQmlScanner) << "Ignoring unresolvable import" << ipath << "from" << path; continue; } if (!pathInfo.isDir()) { qCDebug(logQmlScanner) << "Ignoring non-directory import" << ipath << "from" << path; continue; } if (import.endsWith(".js")) this->scannedFiles.push_back(cpath); else this->scanDir(cpath); } return singleton; } void QmlScanner::scanQmlJson(const QString& path) { qCDebug(logQmlScanner) << "Scanning qml.json file" << path; auto file = QFile(path); if (!file.open(QFile::ReadOnly | QFile::Text)) { qCWarning(logQmlScanner) << "Failed to open file" << path; return; } auto data = file.readAll(); // Importing this makes CI builds fail for some reason. QJsonParseError error; // NOLINT (misc-include-cleaner) auto json = QJsonDocument::fromJson(data, &error); if (error.error != QJsonParseError::NoError) { qCCritical(logQmlScanner).nospace() << "Failed to parse qml.json file at " << path << ": " << error.errorString(); return; } const QString body = "pragma Singleton\nimport QtQuick as Q\n\n" % QmlScanner::jsonToQml(json.object()).second; qCDebug(logQmlScanner) << "Synthesized qml file for" << path << qPrintable("\n" + body); this->fileIntercepts.insert(path.first(path.length() - 5), body); this->scannedFiles.push_back(path); } QPair QmlScanner::jsonToQml(const QJsonValue& value, int indent) { if (value.isObject()) { const auto& object = value.toObject(); auto valIter = object.constBegin(); QString accum = "Q.QtObject {\n"; for (const auto& key: object.keys()) { const auto& val = *valIter++; auto [type, repr] = QmlScanner::jsonToQml(val, indent + 2); accum += QString(' ').repeated(indent + 2) % "readonly property " % type % ' ' % key % ": " % repr % ";\n"; } accum += QString(' ').repeated(indent) % '}'; return qMakePair(QStringLiteral("Q.QtObject"), accum); } else if (value.isArray()) { return qMakePair( QStringLiteral("var"), QJsonDocument(value.toArray()).toJson(QJsonDocument::Compact) ); } else if (value.isString()) { const auto& str = value.toString(); if (str.startsWith('#') && (str.length() == 4 || str.length() == 7 || str.length() == 9)) { for (auto c: str.sliced(1)) { if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { goto noncolor; } } return qMakePair(QStringLiteral("Q.color"), '"' % str % '"'); } noncolor: return qMakePair(QStringLiteral("string"), '"' % QString(str).replace("\"", "\\\"") % '"'); } else if (value.isDouble()) { auto num = value.toDouble(); double whole = 0; if (std::modf(num, &whole) == 0.0) { return qMakePair(QStringLiteral("int"), QString::number(static_cast(whole))); } else { return qMakePair(QStringLiteral("real"), QString::number(num)); } } else if (value.isBool()) { return qMakePair(QStringLiteral("bool"), value.toBool() ? "true" : "false"); } else { return qMakePair(QStringLiteral("var"), "null"); } }