mirror of
https://github.com/zealdocs/zeal.git
synced 2026-08-29 08:34:50 +08:00
924 lines
33 KiB
C++
924 lines
33 KiB
C++
/****************************************************************************
|
|
**
|
|
** Copyright (C) 2015-2016 Oleg Shparber
|
|
** Copyright (C) 2013-2014 Jerzy Kozera
|
|
** Contact: https://go.zealdocs.org/l/contact
|
|
**
|
|
** This file is part of Zeal.
|
|
**
|
|
** Zeal is free software: you can redistribute it and/or modify
|
|
** it under the terms of the GNU General Public License as published by
|
|
** the Free Software Foundation, either version 3 of the License, or
|
|
** (at your option) any later version.
|
|
**
|
|
** Zeal is distributed in the hope that it will be useful,
|
|
** but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
** GNU General Public License for more details.
|
|
**
|
|
** You should have received a copy of the GNU General Public License
|
|
** along with Zeal. If not, see <https://www.gnu.org/licenses/>.
|
|
**
|
|
****************************************************************************/
|
|
|
|
#include "docset.h"
|
|
|
|
#include "cancellationtoken.h"
|
|
#include "searchresult.h"
|
|
|
|
#include <util/plist.h>
|
|
#include <util/sqlitedatabase.h>
|
|
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QJsonArray>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QRegularExpression>
|
|
#include <QVariant>
|
|
#include <QVarLengthArray>
|
|
|
|
#include <sqlite3.h>
|
|
|
|
#include <cstring>
|
|
|
|
using namespace Zeal::Registry;
|
|
|
|
namespace {
|
|
const char IndexNamePrefix[] = "__zi_name"; // zi - Zeal index
|
|
const char IndexNameVersion[] = "0001"; // Current index version
|
|
|
|
namespace InfoPlist {
|
|
const char CFBundleName[] = "CFBundleName";
|
|
//const char CFBundleIdentifier[] = "CFBundleIdentifier";
|
|
const char DashDocSetFamily[] = "DashDocSetFamily";
|
|
const char DashDocSetKeyword[] = "DashDocSetKeyword";
|
|
const char DashDocSetPluginKeyword[] = "DashDocSetPluginKeyword";
|
|
const char DashIndexFilePath[] = "dashIndexFilePath";
|
|
const char DocSetPlatformFamily[] = "DocSetPlatformFamily";
|
|
//const char IsDashDocset[] = "isDashDocset";
|
|
//const char IsJavaScriptEnabled[] = "isJavaScriptEnabled";
|
|
}
|
|
}
|
|
|
|
static void sqliteScoreFunction(sqlite3_context *context, int argc, sqlite3_value **argv);
|
|
|
|
Docset::Docset(const QString &path) :
|
|
m_path(path)
|
|
{
|
|
QDir dir(m_path);
|
|
if (!dir.exists())
|
|
return;
|
|
|
|
loadMetadata();
|
|
|
|
// Attempt to find the icon in any supported format
|
|
for (const QString &iconFile : dir.entryList({QStringLiteral("icon.*")}, QDir::Files)) {
|
|
m_icon = QIcon(dir.filePath(iconFile));
|
|
if (!m_icon.availableSizes().isEmpty())
|
|
break;
|
|
}
|
|
|
|
// TODO: Report errors here and below
|
|
if (!dir.cd(QStringLiteral("Contents")))
|
|
return;
|
|
|
|
// TODO: 'info.plist' is invalid according to Apple, and must alsways be 'Info.plist'
|
|
// https://developer.apple.com/library/mac/documentation/MacOSX/Conceptual/BPRuntimeConfig
|
|
// /Articles/ConfigFiles.html
|
|
Util::Plist plist;
|
|
if (dir.exists(QStringLiteral("Info.plist")))
|
|
plist.read(dir.filePath(QStringLiteral("Info.plist")));
|
|
else if (dir.exists(QStringLiteral("info.plist")))
|
|
plist.read(dir.filePath(QStringLiteral("info.plist")));
|
|
else
|
|
return;
|
|
|
|
if (plist.hasError())
|
|
return;
|
|
|
|
if (m_name.isEmpty()) {
|
|
// Fallback if meta.json is absent
|
|
if (plist.contains(InfoPlist::CFBundleName)) {
|
|
m_name = m_title = plist[InfoPlist::CFBundleName].toString();
|
|
// TODO: Remove when MainWindow::docsetName() will not use directory name
|
|
m_name.replace(QLatin1Char(' '), QLatin1Char('_'));
|
|
} else {
|
|
m_name = QFileInfo(m_path).fileName().remove(QStringLiteral(".docset"));
|
|
}
|
|
}
|
|
|
|
if (m_title.isEmpty()) {
|
|
m_title = m_name;
|
|
m_title.replace(QLatin1Char('_'), QLatin1Char(' '));
|
|
}
|
|
|
|
// TODO: Verify if this is needed
|
|
if (plist.contains(InfoPlist::DashDocSetFamily)
|
|
&& plist[InfoPlist::DashDocSetFamily].toString() == QLatin1String("cheatsheet")) {
|
|
m_name = m_name + QLatin1String("cheats");
|
|
}
|
|
|
|
if (!dir.cd(QStringLiteral("Resources")) || !dir.exists(QStringLiteral("docSet.dsidx")))
|
|
return;
|
|
|
|
m_db = new Util::SQLiteDatabase(dir.filePath(QStringLiteral("docSet.dsidx")));
|
|
|
|
if (!m_db->isOpen()) {
|
|
qWarning("SQL Error: %s", qPrintable(m_db->lastError()));
|
|
return;
|
|
}
|
|
|
|
sqlite3_create_function(m_db->handle(), "zealScore", 2, SQLITE_UTF8, nullptr,
|
|
sqliteScoreFunction, nullptr, nullptr);
|
|
|
|
m_type = m_db->tables().contains(QStringLiteral("searchIndex"), Qt::CaseInsensitive)
|
|
? Type::Dash : Type::ZDash;
|
|
|
|
createIndex();
|
|
|
|
if (m_type == Docset::Type::ZDash) {
|
|
createView();
|
|
}
|
|
|
|
if (!dir.cd(QStringLiteral("Documents"))) {
|
|
m_type = Type::Invalid;
|
|
return;
|
|
}
|
|
|
|
// Setup keywords
|
|
if (plist.contains(InfoPlist::DocSetPlatformFamily))
|
|
m_keywords << plist[InfoPlist::DocSetPlatformFamily].toString();
|
|
|
|
if (plist.contains(InfoPlist::DashDocSetPluginKeyword))
|
|
m_keywords << plist[InfoPlist::DashDocSetPluginKeyword].toString();
|
|
|
|
if (plist.contains(InfoPlist::DashDocSetKeyword))
|
|
m_keywords << plist[InfoPlist::DashDocSetKeyword].toString();
|
|
|
|
if (plist.contains(InfoPlist::DashDocSetFamily)) {
|
|
const QString kw = plist[InfoPlist::DashDocSetFamily].toString();
|
|
if (!kw.contains(QLatin1String("dashtoc"))) {
|
|
m_keywords << kw;
|
|
}
|
|
}
|
|
|
|
m_keywords.removeDuplicates();
|
|
|
|
// Prefer index path provided by the docset over metadata.
|
|
if (plist.contains(InfoPlist::DashIndexFilePath)) {
|
|
m_indexFileUrl = createPageUrl(plist[InfoPlist::DashIndexFilePath].toString());
|
|
} else if (m_indexFileUrl.isEmpty()) {
|
|
if (dir.exists(QStringLiteral("index.html")))
|
|
m_indexFileUrl = createPageUrl(QStringLiteral("index.html"));
|
|
else
|
|
qWarning("Cannot determine index file for docset %s", qPrintable(m_name));
|
|
}
|
|
|
|
countSymbols();
|
|
}
|
|
|
|
Docset::~Docset()
|
|
{
|
|
delete m_db;
|
|
}
|
|
|
|
bool Docset::isValid() const
|
|
{
|
|
return m_type != Type::Invalid;
|
|
}
|
|
|
|
QString Docset::name() const
|
|
{
|
|
return m_name;
|
|
}
|
|
|
|
QString Docset::title() const
|
|
{
|
|
return m_title;
|
|
}
|
|
|
|
QStringList Docset::keywords() const
|
|
{
|
|
return m_keywords;
|
|
}
|
|
|
|
QString Docset::version() const
|
|
{
|
|
return m_version;
|
|
}
|
|
|
|
QString Docset::revision() const
|
|
{
|
|
return m_revision;
|
|
}
|
|
|
|
QString Docset::feedUrl() const
|
|
{
|
|
return m_feedUrl;
|
|
}
|
|
|
|
QString Docset::path() const
|
|
{
|
|
return m_path;
|
|
}
|
|
|
|
QString Docset::documentPath() const
|
|
{
|
|
return QDir(m_path).filePath(QStringLiteral("Contents/Resources/Documents"));
|
|
}
|
|
|
|
QIcon Docset::icon() const
|
|
{
|
|
return m_icon;
|
|
}
|
|
|
|
QIcon Docset::symbolTypeIcon(const QString &symbolType) const
|
|
{
|
|
static const QIcon unknownIcon(QStringLiteral("typeIcon:Unknown.png"));
|
|
|
|
const QIcon icon(QStringLiteral("typeIcon:%1.png").arg(symbolType));
|
|
return icon.availableSizes().isEmpty() ? unknownIcon : icon;
|
|
}
|
|
|
|
QUrl Docset::indexFileUrl() const
|
|
{
|
|
return m_indexFileUrl;
|
|
}
|
|
|
|
QMap<QString, int> Docset::symbolCounts() const
|
|
{
|
|
return m_symbolCounts;
|
|
}
|
|
|
|
int Docset::symbolCount(const QString &symbolType) const
|
|
{
|
|
return m_symbolCounts.value(symbolType);
|
|
}
|
|
|
|
const QMap<QString, QUrl> &Docset::symbols(const QString &symbolType) const
|
|
{
|
|
if (!m_symbols.contains(symbolType))
|
|
loadSymbols(symbolType);
|
|
return m_symbols[symbolType];
|
|
}
|
|
|
|
QList<SearchResult> Docset::search(const QString &query, const CancellationToken &token) const
|
|
{
|
|
QString sql;
|
|
if (m_type == Docset::Type::Dash) {
|
|
if (m_fuzzySearchEnabled) {
|
|
sql = QStringLiteral("SELECT name, type, path, '', zealScore('%1', name) as score"
|
|
" FROM searchIndex"
|
|
" WHERE score > 0");
|
|
} else {
|
|
sql = QStringLiteral("SELECT name, type, path, '', -length(name) as score"
|
|
" FROM searchIndex"
|
|
" WHERE (name LIKE '%%1%' ESCAPE '\\')");
|
|
}
|
|
} else {
|
|
if (m_fuzzySearchEnabled) {
|
|
sql = QStringLiteral("SELECT name, type, path, fragment, zealScore('%1', name) as score"
|
|
" FROM searchIndex"
|
|
" WHERE score > 0");
|
|
} else {
|
|
sql = QStringLiteral("SELECT name, type, path, fragment, -length(name) as score"
|
|
" FROM searchIndex"
|
|
" WHERE (name LIKE '%%1%' ESCAPE '\\')");
|
|
}
|
|
}
|
|
|
|
// Limit for very short queries.
|
|
// TODO: Show a notification about the reduced result set.
|
|
if (query.size() < 3) {
|
|
sql += QLatin1String(" LIMIT 1000");
|
|
}
|
|
|
|
// Make it safe to use in a SQL query.
|
|
QString sanitizedQuery = query;
|
|
sanitizedQuery.replace(QLatin1Char('\''), QLatin1String("''"));
|
|
m_db->prepare(sql.arg(sanitizedQuery));
|
|
|
|
QList<SearchResult> results;
|
|
while (m_db->next() && !token.isCanceled()) {
|
|
results.append({m_db->value(0).toString(),
|
|
parseSymbolType(m_db->value(1).toString()),
|
|
m_db->value(2).toString(), m_db->value(3).toString(),
|
|
const_cast<Docset *>(this), m_db->value(4).toInt()});
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
QList<SearchResult> Docset::relatedLinks(const QUrl &url) const
|
|
{
|
|
QList<SearchResult> results;
|
|
|
|
// Strip docset path and anchor from url
|
|
const QString dir = documentPath();
|
|
QString urlPath = url.path();
|
|
int dirPosition = urlPath.indexOf(dir);
|
|
QString path = url.path().mid(dirPosition + dir.size() + 1);
|
|
|
|
// Get the url without the #anchor.
|
|
QUrl cleanUrl(path);
|
|
cleanUrl.setFragment(QString());
|
|
|
|
// Prepare the query to look up all pages with the same url.
|
|
QString sql;
|
|
if (m_type == Docset::Type::Dash) {
|
|
sql = QStringLiteral("SELECT name, type, path"
|
|
" FROM searchIndex"
|
|
" WHERE path LIKE \"%1%%\" AND path <> \"%1\"");
|
|
} else if (m_type == Docset::Type::ZDash) {
|
|
sql = QStringLiteral("SELECT name, type, path, fragment"
|
|
" FROM searchIndex"
|
|
" WHERE path = \"%1\" AND fragment IS NOT NULL");
|
|
}
|
|
|
|
m_db->prepare(sql.arg(cleanUrl.toString()));
|
|
while (m_db->next()) {
|
|
results.append({m_db->value(0).toString(),
|
|
parseSymbolType(m_db->value(1).toString()),
|
|
m_db->value(2).toString(), m_db->value(3).toString(),
|
|
const_cast<Docset *>(this), 0});
|
|
}
|
|
|
|
if (results.size() == 1)
|
|
results.clear();
|
|
|
|
return results;
|
|
}
|
|
|
|
QUrl Docset::searchResultUrl(const SearchResult &result) const
|
|
{
|
|
return createPageUrl(result.urlPath, result.urlFragment);
|
|
}
|
|
|
|
void Docset::loadMetadata()
|
|
{
|
|
const QDir dir(m_path);
|
|
|
|
// Fallback if meta.json is absent
|
|
if (!dir.exists(QStringLiteral("meta.json")))
|
|
return;
|
|
|
|
QScopedPointer<QFile> file(new QFile(dir.filePath(QStringLiteral("meta.json"))));
|
|
if (!file->open(QIODevice::ReadOnly))
|
|
return;
|
|
|
|
QJsonParseError jsonError;
|
|
const QJsonObject jsonObject = QJsonDocument::fromJson(file->readAll(), &jsonError).object();
|
|
|
|
if (jsonError.error != QJsonParseError::NoError)
|
|
return;
|
|
|
|
m_name = jsonObject[QStringLiteral("name")].toString();
|
|
m_title = jsonObject[QStringLiteral("title")].toString();
|
|
m_version = jsonObject[QStringLiteral("version")].toString();
|
|
m_revision = jsonObject[QStringLiteral("revision")].toString();
|
|
|
|
if (jsonObject.contains(QStringLiteral("feed_url"))) {
|
|
m_feedUrl = jsonObject[QStringLiteral("feed_url")].toString();
|
|
}
|
|
|
|
if (jsonObject.contains(QStringLiteral("extra"))) {
|
|
const QJsonObject extra = jsonObject[QStringLiteral("extra")].toObject();
|
|
|
|
if (extra.contains(QStringLiteral("indexFilePath"))) {
|
|
m_indexFileUrl = createPageUrl(extra[QStringLiteral("indexFilePath")].toString());
|
|
}
|
|
|
|
if (extra.contains(QStringLiteral("keywords"))) {
|
|
for (const QJsonValueRef kw : extra[QStringLiteral("keywords")].toArray())
|
|
m_keywords << kw.toString();
|
|
}
|
|
}
|
|
}
|
|
|
|
void Docset::countSymbols()
|
|
{
|
|
static const QString sql = QStringLiteral("SELECT type, COUNT(*)"
|
|
" FROM searchIndex"
|
|
" GROUP BY type");
|
|
if (!m_db->prepare(sql)) {
|
|
qWarning("SQL Error: %s", qPrintable(m_db->lastError()));
|
|
return;
|
|
}
|
|
|
|
while (m_db->next()) {
|
|
const QString symbolTypeStr = m_db->value(0).toString();
|
|
const QString symbolType = parseSymbolType(symbolTypeStr);
|
|
m_symbolStrings.insertMulti(symbolType, symbolTypeStr);
|
|
m_symbolCounts[symbolType] += m_db->value(1).toInt();
|
|
}
|
|
}
|
|
|
|
// TODO: Fetch and cache only portions of symbols
|
|
void Docset::loadSymbols(const QString &symbolType) const
|
|
{
|
|
for (const QString &symbol : m_symbolStrings.values(symbolType))
|
|
loadSymbols(symbolType, symbol);
|
|
}
|
|
|
|
void Docset::loadSymbols(const QString &symbolType, const QString &symbolString) const
|
|
{
|
|
QString sql;
|
|
if (m_type == Docset::Type::Dash) {
|
|
sql = QStringLiteral("SELECT name, path"
|
|
" FROM searchIndex"
|
|
" WHERE type='%1'"
|
|
" ORDER BY name");
|
|
} else {
|
|
sql = QStringLiteral("SELECT name, path, fragment"
|
|
" FROM searchIndex"
|
|
" WHERE type='%1'"
|
|
" ORDER BY name");
|
|
}
|
|
|
|
if (!m_db->prepare(sql.arg(symbolString))) {
|
|
qWarning("SQL Error: %s", qPrintable(m_db->lastError()));
|
|
return;
|
|
}
|
|
|
|
QMap<QString, QUrl> &symbols = m_symbols[symbolType];
|
|
while (m_db->next())
|
|
symbols.insertMulti(m_db->value(0).toString(),
|
|
createPageUrl(m_db->value(1).toString(), m_db->value(2).toString()));
|
|
}
|
|
|
|
void Docset::createIndex()
|
|
{
|
|
static const QString indexListQuery = QStringLiteral("PRAGMA INDEX_LIST('%1')");
|
|
static const QString indexDropQuery = QStringLiteral("DROP INDEX '%1'");
|
|
static const QString indexCreateQuery = QStringLiteral("CREATE INDEX IF NOT EXISTS %1%2"
|
|
" ON %3 (%4 COLLATE NOCASE)");
|
|
|
|
const QString tableName = m_type == Type::Dash ? QStringLiteral("searchIndex")
|
|
: QStringLiteral("ztoken");
|
|
const QString columnName = m_type == Type::Dash ? QStringLiteral("name")
|
|
: QStringLiteral("ztokenname");
|
|
|
|
m_db->prepare(indexListQuery.arg(tableName));
|
|
|
|
QStringList oldIndexes;
|
|
|
|
while (m_db->next()) {
|
|
const QString indexName = m_db->value(1).toString();
|
|
if (!indexName.startsWith(IndexNamePrefix))
|
|
continue;
|
|
|
|
if (indexName.endsWith(IndexNameVersion))
|
|
return;
|
|
|
|
oldIndexes << indexName;
|
|
}
|
|
|
|
// Drop old indexes
|
|
for (const QString &oldIndexName : oldIndexes)
|
|
m_db->execute(indexDropQuery.arg(oldIndexName));
|
|
|
|
m_db->execute(indexCreateQuery.arg(IndexNamePrefix, IndexNameVersion, tableName, columnName));
|
|
}
|
|
|
|
void Docset::createView()
|
|
{
|
|
static const QString viewCreateQuery
|
|
= QStringLiteral("CREATE VIEW IF NOT EXISTS searchIndex AS"
|
|
" SELECT"
|
|
" ztokenname AS name,"
|
|
" ztypename AS type,"
|
|
" zpath AS path,"
|
|
" zanchor AS fragment"
|
|
" FROM ztoken"
|
|
" INNER JOIN ztokenmetainformation"
|
|
" ON ztoken.zmetainformation = ztokenmetainformation.z_pk"
|
|
" INNER JOIN zfilepath"
|
|
" ON ztokenmetainformation.zfile = zfilepath.z_pk"
|
|
" INNER JOIN ztokentype"
|
|
" ON ztoken.ztokentype = ztokentype.z_pk");
|
|
|
|
m_db->execute(viewCreateQuery);
|
|
}
|
|
|
|
QUrl Docset::createPageUrl(const QString &path, const QString &fragment) const
|
|
{
|
|
QString realPath;
|
|
QString realFragment;
|
|
|
|
if (fragment.isEmpty()) {
|
|
const QStringList urlParts = path.split(QLatin1Char('#'));
|
|
realPath = urlParts[0];
|
|
if (urlParts.size() > 1)
|
|
realFragment = urlParts[1];
|
|
} else {
|
|
realPath = path;
|
|
realFragment = fragment;
|
|
}
|
|
|
|
static const QRegularExpression dashEntryRegExp(QLatin1String("<dash_entry_.*>"));
|
|
realPath.remove(dashEntryRegExp);
|
|
realFragment.remove(dashEntryRegExp);
|
|
|
|
// Absolute file path is required here to handle relative path to the docset storage (see #806).
|
|
QUrl url = QUrl::fromLocalFile(QDir(documentPath()).absoluteFilePath(realPath));
|
|
if (!realFragment.isEmpty()) {
|
|
if (realFragment.startsWith(QLatin1String("//apple_ref"))
|
|
|| realFragment.startsWith(QLatin1String("//dash_ref"))) {
|
|
url.setFragment(realFragment, QUrl::DecodedMode);
|
|
} else {
|
|
url.setFragment(realFragment);
|
|
}
|
|
}
|
|
|
|
return url;
|
|
}
|
|
|
|
QString Docset::parseSymbolType(const QString &str)
|
|
{
|
|
// Dash symbol aliases
|
|
const static QHash<QString, QString> aliases = {
|
|
// Attribute
|
|
{QStringLiteral("Package Attributes"), QStringLiteral("Attribute")},
|
|
{QStringLiteral("Private Attributes"), QStringLiteral("Attribute")},
|
|
{QStringLiteral("Protected Attributes"), QStringLiteral("Attribute")},
|
|
{QStringLiteral("Public Attributes"), QStringLiteral("Attribute")},
|
|
{QStringLiteral("Static Package Attributes"), QStringLiteral("Attribute")},
|
|
{QStringLiteral("Static Private Attributes"), QStringLiteral("Attribute")},
|
|
{QStringLiteral("Static Protected Attributes"), QStringLiteral("Attribute")},
|
|
{QStringLiteral("Static Public Attributes"), QStringLiteral("Attribute")},
|
|
{QStringLiteral("XML Attributes"), QStringLiteral("Attribute")},
|
|
// Binding
|
|
{QStringLiteral("binding"), QStringLiteral("Binding")},
|
|
// Category
|
|
{QStringLiteral("cat"), QStringLiteral("Category")},
|
|
{QStringLiteral("Groups"), QStringLiteral("Category")},
|
|
{QStringLiteral("Pages"), QStringLiteral("Category")},
|
|
// Class
|
|
{QStringLiteral("cl"), QStringLiteral("Class")},
|
|
{QStringLiteral("specialization"), QStringLiteral("Class")},
|
|
{QStringLiteral("tmplt"), QStringLiteral("Class")},
|
|
// Constant
|
|
{QStringLiteral("data"), QStringLiteral("Constant")},
|
|
{QStringLiteral("econst"), QStringLiteral("Constant")},
|
|
{QStringLiteral("enumdata"), QStringLiteral("Constant")},
|
|
{QStringLiteral("enumelt"), QStringLiteral("Constant")},
|
|
{QStringLiteral("clconst"), QStringLiteral("Constant")},
|
|
{QStringLiteral("structdata"), QStringLiteral("Constant")},
|
|
{QStringLiteral("writerid"), QStringLiteral("Constant")},
|
|
{QStringLiteral("Notifications"), QStringLiteral("Constant")},
|
|
// Constructor
|
|
{QStringLiteral("structctr"), QStringLiteral("Constructor")},
|
|
{QStringLiteral("Public Constructors"), QStringLiteral("Constructor")},
|
|
// Enumeration
|
|
{QStringLiteral("enum"), QStringLiteral("Enumeration")},
|
|
{QStringLiteral("Enum"), QStringLiteral("Enumeration")},
|
|
{QStringLiteral("Enumerations"), QStringLiteral("Enumeration")},
|
|
// Event
|
|
{QStringLiteral("event"), QStringLiteral("Event")},
|
|
{QStringLiteral("Public Events"), QStringLiteral("Event")},
|
|
{QStringLiteral("Inherited Events"), QStringLiteral("Event")},
|
|
{QStringLiteral("Private Events"), QStringLiteral("Event")},
|
|
// Field
|
|
{QStringLiteral("Data Fields"), QStringLiteral("Field")},
|
|
// Function
|
|
{QStringLiteral("dcop"), QStringLiteral("Function")},
|
|
{QStringLiteral("func"), QStringLiteral("Function")},
|
|
{QStringLiteral("ffunc"), QStringLiteral("Function")},
|
|
{QStringLiteral("signal"), QStringLiteral("Function")},
|
|
{QStringLiteral("slot"), QStringLiteral("Function")},
|
|
{QStringLiteral("grammar"), QStringLiteral("Function")},
|
|
{QStringLiteral("Function Prototypes"), QStringLiteral("Function")},
|
|
{QStringLiteral("Functions/Subroutines"), QStringLiteral("Function")},
|
|
{QStringLiteral("Members"), QStringLiteral("Function")},
|
|
{QStringLiteral("Package Functions"), QStringLiteral("Function")},
|
|
{QStringLiteral("Private Member Functions"), QStringLiteral("Function")},
|
|
{QStringLiteral("Private Slots"), QStringLiteral("Function")},
|
|
{QStringLiteral("Protected Member Functions"), QStringLiteral("Function")},
|
|
{QStringLiteral("Protected Slots"), QStringLiteral("Function")},
|
|
{QStringLiteral("Public Member Functions"), QStringLiteral("Function")},
|
|
{QStringLiteral("Public Slots"), QStringLiteral("Function")},
|
|
{QStringLiteral("Signals"), QStringLiteral("Function")},
|
|
{QStringLiteral("Static Package Functions"), QStringLiteral("Function")},
|
|
{QStringLiteral("Static Private Member Functions"), QStringLiteral("Function")},
|
|
{QStringLiteral("Static Protected Member Functions"), QStringLiteral("Function")},
|
|
{QStringLiteral("Static Public Member Functions"), QStringLiteral("Function")},
|
|
// Guide
|
|
{QStringLiteral("doc"), QStringLiteral("Guide")},
|
|
// Namespace
|
|
{QStringLiteral("ns"), QStringLiteral("Namespace")},
|
|
// Macro
|
|
{QStringLiteral("macro"), QStringLiteral("Macro")},
|
|
// Method
|
|
{QStringLiteral("clm"), QStringLiteral("Method")},
|
|
{QStringLiteral("enumcm"), QStringLiteral("Method")},
|
|
{QStringLiteral("enumctr"), QStringLiteral("Method")},
|
|
{QStringLiteral("enumm"), QStringLiteral("Method")},
|
|
{QStringLiteral("intfctr"), QStringLiteral("Method")},
|
|
{QStringLiteral("intfcm"), QStringLiteral("Method")},
|
|
{QStringLiteral("intfm"), QStringLiteral("Method")},
|
|
{QStringLiteral("intfsub"), QStringLiteral("Method")},
|
|
{QStringLiteral("instsub"), QStringLiteral("Method")},
|
|
{QStringLiteral("instctr"), QStringLiteral("Method")},
|
|
{QStringLiteral("instm"), QStringLiteral("Method")},
|
|
{QStringLiteral("structcm"), QStringLiteral("Method")},
|
|
{QStringLiteral("structm"), QStringLiteral("Method")},
|
|
{QStringLiteral("structsub"), QStringLiteral("Method")},
|
|
{QStringLiteral("Class Methods"), QStringLiteral("Method")},
|
|
{QStringLiteral("Inherited Methods"), QStringLiteral("Method")},
|
|
{QStringLiteral("Instance Methods"), QStringLiteral("Method")},
|
|
{QStringLiteral("Private Methods"), QStringLiteral("Method")},
|
|
{QStringLiteral("Protected Methods"), QStringLiteral("Method")},
|
|
{QStringLiteral("Public Methods"), QStringLiteral("Method")},
|
|
// Operator
|
|
{QStringLiteral("intfopfunc"), QStringLiteral("Operator")},
|
|
{QStringLiteral("opfunc"), QStringLiteral("Operator")},
|
|
// Property
|
|
{QStringLiteral("enump"), QStringLiteral("Property")},
|
|
{QStringLiteral("intfdata"), QStringLiteral("Property")},
|
|
{QStringLiteral("intfp"), QStringLiteral("Property")},
|
|
{QStringLiteral("instp"), QStringLiteral("Property")},
|
|
{QStringLiteral("structp"), QStringLiteral("Property")},
|
|
{QStringLiteral("Inherited Properties"), QStringLiteral("Property")},
|
|
{QStringLiteral("Private Properties"), QStringLiteral("Property")},
|
|
{QStringLiteral("Protected Properties"), QStringLiteral("Property")},
|
|
{QStringLiteral("Public Properties"), QStringLiteral("Property")},
|
|
// Protocol
|
|
{QStringLiteral("intf"), QStringLiteral("Protocol")},
|
|
// Structure
|
|
{QStringLiteral("struct"), QStringLiteral("Structure")},
|
|
{QStringLiteral("Data Structures"), QStringLiteral("Structure")},
|
|
{QStringLiteral("Struct"), QStringLiteral("Structure")},
|
|
// Type
|
|
{QStringLiteral("tag"), QStringLiteral("Type")},
|
|
{QStringLiteral("tdef"), QStringLiteral("Type")},
|
|
{QStringLiteral("Data Types"), QStringLiteral("Type")},
|
|
{QStringLiteral("Package Types"), QStringLiteral("Type")},
|
|
{QStringLiteral("Private Types"), QStringLiteral("Type")},
|
|
{QStringLiteral("Protected Types"), QStringLiteral("Type")},
|
|
{QStringLiteral("Public Types"), QStringLiteral("Type")},
|
|
{QStringLiteral("Typedefs"), QStringLiteral("Type")},
|
|
// Variable
|
|
{QStringLiteral("var"), QStringLiteral("Variable")}
|
|
};
|
|
|
|
return aliases.value(str, str);
|
|
}
|
|
|
|
bool Docset::isFuzzySearchEnabled() const
|
|
{
|
|
return m_fuzzySearchEnabled;
|
|
}
|
|
|
|
void Docset::setFuzzySearchEnabled(bool enabled)
|
|
{
|
|
m_fuzzySearchEnabled = enabled;
|
|
}
|
|
|
|
/**
|
|
* \brief Returns score based on a substring position in a string.
|
|
* \param str Original string.
|
|
* \param index Index of the substring within \a str.
|
|
* \param length Substring length.
|
|
* \return Score value between 1 and 100.
|
|
*/
|
|
static int scoreFuzzy(const char *str, int index, int length)
|
|
{
|
|
if (index == 0 || str[index - 1] == '.') {
|
|
// score between 66..99, if the match follows a dot, or starts the string
|
|
return qMax(66, 100 - length);
|
|
} else if (str[index + length] == 0) {
|
|
// score between 33..66, if the match is at the end of the string
|
|
return qMax(33, 67 - length);
|
|
} else {
|
|
// score between 1..33 otherwise (match in the middle of the string)
|
|
return qMax(1, 34 - length);
|
|
}
|
|
}
|
|
|
|
// Based on https://github.com/bevacqua/fuzzysearch
|
|
static void matchFuzzy(const char *needle, int needleLength,
|
|
const char *haystack, int haystackLength,
|
|
int *start, int *length)
|
|
{
|
|
static const int MaxDistance = 8;
|
|
static const int MaxGroupCount = 3;
|
|
|
|
*start = -1;
|
|
|
|
int groupCount = 0;
|
|
int bestRecursiveScore = -1;
|
|
int bestRecursiveStart = -1;
|
|
int bestRecursiveLength = -1;
|
|
|
|
for (int i = 0, j = 0; i < needleLength; ++i) {
|
|
bool found = false;
|
|
bool first = true;
|
|
int distance = 0;
|
|
|
|
while (j < haystackLength) {
|
|
if (needle[i] == haystack[j++]) {
|
|
if (*start == -1) {
|
|
*start = j; // first matched char
|
|
|
|
// try starting the search later in case the first character occurs again later
|
|
int recursiveStart;
|
|
int recursiveLength;
|
|
matchFuzzy(needle, needleLength, haystack + j,
|
|
haystackLength - j,
|
|
&recursiveStart, &recursiveLength);
|
|
if (recursiveStart != -1) {
|
|
int recursiveScore = scoreFuzzy(haystack,
|
|
recursiveStart,
|
|
recursiveLength);
|
|
if (recursiveScore > bestRecursiveScore) {
|
|
bestRecursiveScore = recursiveScore;
|
|
bestRecursiveStart = recursiveStart;
|
|
bestRecursiveLength = recursiveLength;
|
|
}
|
|
}
|
|
}
|
|
|
|
*length = j - *start + 1;
|
|
found = true;
|
|
break;
|
|
}
|
|
|
|
// Optimizations to reduce returned number of results
|
|
// (search was returning too many irrelevant results with large docsets)
|
|
// Optimization #1: too many mismatches.
|
|
if (first) {
|
|
if (++groupCount >= MaxGroupCount) {
|
|
break;
|
|
}
|
|
|
|
first = false;
|
|
}
|
|
|
|
// Optimization #2: too large distance between found chars.
|
|
if (i != 0 && ++distance >= MaxDistance) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found) {
|
|
// End of haystack, char not found.
|
|
if (bestRecursiveScore != -1) {
|
|
// Can still match with the same constraints if matching started later
|
|
// (smaller distance from first char to 2nd char)
|
|
*start = bestRecursiveStart;
|
|
*length = bestRecursiveLength;
|
|
} else {
|
|
*start = -1;
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
int score = scoreFuzzy(haystack, *start, *length);
|
|
if (bestRecursiveScore > score) {
|
|
*start = bestRecursiveStart;
|
|
*length = bestRecursiveLength;
|
|
}
|
|
}
|
|
|
|
// Ported from DevDocs (https://github.com/Thibaut/devdocs), see app/searcher.coffee.
|
|
static int scoreExact(int matchIndex, int matchLen, const char *value, int valueLen)
|
|
{
|
|
static const char DOT = '.';
|
|
|
|
int score = 100;
|
|
|
|
// Remove one point for each unmatched character.
|
|
score -= (valueLen - matchLen);
|
|
|
|
if (matchIndex > 0) {
|
|
if (value[matchIndex - 1] == DOT) {
|
|
// If the character preceding the query is a dot, assign the same
|
|
// score as if the query was found at the beginning of the string,
|
|
// minus one.
|
|
score += matchIndex - 1;
|
|
} else if (matchLen == 1) {
|
|
// Don't match a single-character query unless it's found at the
|
|
// beginning of the string or is preceded by a dot.
|
|
return 0;
|
|
} else {
|
|
// (1) Remove one point for each unmatched character up to
|
|
// the nearest preceding dot or the beginning of the
|
|
// string.
|
|
// (2) Remove one point for each unmatched character
|
|
// following the query.
|
|
int i = matchIndex - 2;
|
|
while (i >= 0 && value[i] != DOT)
|
|
--i;
|
|
|
|
score -= (matchIndex - i) // (1)
|
|
+ (valueLen - matchLen - matchIndex); // (2)
|
|
}
|
|
|
|
// Remove one point for each dot preceding the query, except for the
|
|
// one immediately before the query.
|
|
for (int i = matchIndex - 2; i >= 0; --i) {
|
|
if (value[i] == DOT)
|
|
--score;
|
|
}
|
|
}
|
|
|
|
// Remove five points for each dot following the query.
|
|
for (int i = valueLen - matchLen - matchIndex - 1; i >= 0; --i) {
|
|
if (value[matchIndex + matchLen + i] == DOT)
|
|
score -= 5;
|
|
}
|
|
|
|
return qMax(1, score);
|
|
}
|
|
|
|
static inline int scoreFunction(const char *needleOrig, const char *haystackOrig)
|
|
{
|
|
const int needleLength = static_cast<int>(qstrlen(needleOrig));
|
|
const int haystackLength = static_cast<int>(qstrlen(haystackOrig));
|
|
|
|
QVarLengthArray<char, 1024> needle(needleLength + 1);
|
|
QVarLengthArray<char, 1024> haystack(haystackLength + 1);
|
|
|
|
for (int i = 0, j = 0; i <= needleLength; ++i, ++j) {
|
|
const char c = needleOrig[i];
|
|
if ((i > 0 && needleOrig[i - 1] == ':' && c == ':') // C++ (::)
|
|
|| c == '/' || c == '_' || c == ' ') { // Go, some Guides
|
|
needle[j] = '.';
|
|
} else if (c >= 'A' && c <= 'Z') {
|
|
needle[j] = c + 32;
|
|
} else {
|
|
needle[j] = c;
|
|
}
|
|
}
|
|
|
|
for (int i = 0, j = 0; i <= haystackLength; ++i, ++j) {
|
|
const char c = haystackOrig[i];
|
|
if ((i > 0 && haystackOrig[i - 1] == ':' && c == ':') // C++ (::)
|
|
|| c == '/' || c == '_' || c == ' ') { // Go, some Guides
|
|
haystack[j] = '.';
|
|
} else if (c >= 'A' && c <= 'Z') {
|
|
haystack[j] = c + 32;
|
|
} else {
|
|
haystack[j] = c;
|
|
}
|
|
}
|
|
|
|
int score = 0;
|
|
int matchIndex = -1;
|
|
int matchLength = 0;
|
|
int exactIndex = -1;
|
|
const char *exactMatch = std::strstr(haystack.data(), needle.data());
|
|
|
|
if (exactMatch != nullptr) {
|
|
exactIndex = exactMatch - haystack.data();
|
|
}
|
|
|
|
if (exactIndex == -1) {
|
|
matchFuzzy(needle.data(), needleLength,
|
|
haystack.data(), haystackLength,
|
|
&matchIndex, &matchLength);
|
|
}
|
|
|
|
if (matchIndex == -1 && exactIndex == -1) { // no match
|
|
// simply return 0
|
|
return 0;
|
|
} else if (exactIndex != -1) {
|
|
// +100 to make sure exact matches are always on top.
|
|
score = scoreExact(exactIndex, needleLength, haystack.data(), haystackLength) + 100;
|
|
} else {
|
|
score = scoreFuzzy(haystack.data(), matchIndex, matchLength);
|
|
|
|
int indexOfLastDot;
|
|
for (indexOfLastDot = haystackLength - 1; indexOfLastDot >= 0; --indexOfLastDot) {
|
|
if (haystack[indexOfLastDot] == '.')
|
|
break;
|
|
}
|
|
|
|
if (indexOfLastDot != -1) {
|
|
matchIndex = -1;
|
|
matchFuzzy(needle.data(), needleLength,
|
|
haystack.data() + indexOfLastDot + 1, haystackLength - (indexOfLastDot + 1),
|
|
&matchIndex, &matchLength);
|
|
|
|
if (matchIndex != -1) {
|
|
score = qMax(score, scoreFuzzy(haystack.data() + indexOfLastDot + 1,
|
|
matchIndex, matchLength));
|
|
}
|
|
}
|
|
}
|
|
|
|
return score;
|
|
}
|
|
|
|
static void sqliteScoreFunction(sqlite3_context *context, int argc, sqlite3_value **argv)
|
|
{
|
|
Q_UNUSED(argc);
|
|
|
|
const char *needle = reinterpret_cast<const char *>(sqlite3_value_text(argv[0]));
|
|
const char *haystack = reinterpret_cast<const char *>(sqlite3_value_text(argv[1]));
|
|
|
|
sqlite3_result_int(context, scoreFunction(needle, haystack));
|
|
}
|