blob: fcdc6dee1b6ba688eb14f12f0674703744950aa3 [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2019 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the tools applications of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "quoter.h"
#include <QtCore/qdebug.h>
#include <QtCore/qfileinfo.h>
#include <QtCore/qregexp.h>
QT_BEGIN_NAMESPACE
QHash<QString, QString> Quoter::commentHash;
static void replaceMultipleNewlines(QString &s)
{
const int n = s.size();
bool slurping = false;
int j = -1;
const QChar newLine = QLatin1Char('\n');
QChar *d = s.data();
for (int i = 0; i != n; ++i) {
const QChar c = d[i];
bool hit = (c == newLine);
if (slurping && hit)
continue;
d[++j] = c;
slurping = hit;
}
s.resize(++j);
}
// This is equivalent to line.split( QRegExp("\n(?!\n|$)") ) but much faster
QStringList Quoter::splitLines(const QString &line)
{
QStringList result;
int i = line.size();
while (true) {
int j = i - 1;
while (j >= 0 && line.at(j) == QLatin1Char('\n'))
--j;
while (j >= 0 && line.at(j) != QLatin1Char('\n'))
--j;
result.prepend(line.mid(j + 1, i - j - 1));
if (j < 0)
break;
i = j;
}
return result;
}
/*
Transforms 'int x = 3 + 4' into 'int x=3+4'. A white space is kept
between 'int' and 'x' because it is meaningful in C++.
*/
static void trimWhiteSpace(QString &str)
{
enum { Normal, MetAlnum, MetSpace } state = Normal;
const int n = str.length();
int j = -1;
QChar *d = str.data();
for (int i = 0; i != n; ++i) {
const QChar c = d[i];
if (c.isLetterOrNumber()) {
if (state == Normal) {
state = MetAlnum;
} else {
if (state == MetSpace)
str[++j] = c;
state = Normal;
}
str[++j] = c;
} else if (c.isSpace()) {
if (state == MetAlnum)
state = MetSpace;
} else {
state = Normal;
str[++j] = c;
}
}
str.resize(++j);
}
Quoter::Quoter() : silent(false)
{
/* We're going to hard code these delimiters:
* C++, Qt, Qt Script, Java:
//! [<id>]
* .pro, .py, CMake files:
#! [<id>]
* .html, .qrc, .ui, .xq, .xml .dita files:
<!-- [<id>] -->
*/
if (!commentHash.size()) {
commentHash["pro"] = "#!";
commentHash["py"] = "#!";
commentHash["cmake"] = "#!";
commentHash["html"] = "<!--";
commentHash["qrc"] = "<!--";
commentHash["ui"] = "<!--";
commentHash["xml"] = "<!--";
commentHash["dita"] = "<!--";
commentHash["xq"] = "<!--";
}
}
void Quoter::reset()
{
silent = false;
plainLines.clear();
markedLines.clear();
codeLocation = Location();
}
void Quoter::quoteFromFile(const QString &userFriendlyFilePath, const QString &plainCode,
const QString &markedCode)
{
silent = false;
/*
Split the source code into logical lines. Empty lines are
treated specially. Before:
p->alpha();
p->beta();
p->gamma();
p->delta();
After:
p->alpha();
p->beta();\n
p->gamma();\n\n
p->delta();
Newlines are preserved because they affect codeLocation.
*/
codeLocation = Location(userFriendlyFilePath);
plainLines = splitLines(plainCode);
markedLines = splitLines(markedCode);
if (markedLines.count() != plainLines.count()) {
codeLocation.warning(tr("Something is wrong with qdoc's handling of marked code"));
markedLines = plainLines;
}
/*
Squeeze blanks (cat -s).
*/
for (auto &line : markedLines)
replaceMultipleNewlines(line);
codeLocation.start();
}
QString Quoter::quoteLine(const Location &docLocation, const QString &command,
const QString &pattern)
{
if (plainLines.isEmpty()) {
failedAtEnd(docLocation, command);
return QString();
}
if (pattern.isEmpty()) {
docLocation.warning(tr("Missing pattern after '\\%1'").arg(command));
return QString();
}
if (match(docLocation, pattern, plainLines.first()))
return getLine();
if (!silent) {
docLocation.warning(tr("Command '\\%1' failed").arg(command));
codeLocation.warning(tr("Pattern '%1' didn't match here").arg(pattern));
silent = true;
}
return QString();
}
QString Quoter::quoteSnippet(const Location &docLocation, const QString &identifier)
{
QString comment = commentForCode();
QString delimiter = comment + QString(" [%1]").arg(identifier);
QString t;
int indent = 0;
while (!plainLines.isEmpty()) {
if (match(docLocation, delimiter, plainLines.first())) {
QString startLine = getLine();
while (indent < startLine.length() && startLine[indent] == QLatin1Char(' '))
indent++;
break;
}
getLine();
}
while (!plainLines.isEmpty()) {
QString line = plainLines.first();
if (match(docLocation, delimiter, line)) {
QString lastLine = getLine(indent);
int dIndex = lastLine.indexOf(delimiter);
if (dIndex > 0) {
// The delimiter might be preceded on the line by other
// delimeters, so look for the first comment on the line.
QString leading = lastLine.left(dIndex);
dIndex = leading.indexOf(comment);
if (dIndex != -1)
leading = leading.left(dIndex);
if (leading.endsWith(QLatin1String("<@comment>")))
leading.chop(10);
if (!leading.trimmed().isEmpty())
t += leading;
}
return t;
}
t += removeSpecialLines(line, comment, indent);
}
failedAtEnd(docLocation, QString("snippet (%1)").arg(delimiter));
return t;
}
QString Quoter::quoteTo(const Location &docLocation, const QString &command, const QString &pattern)
{
QString t;
QString comment = commentForCode();
if (pattern.isEmpty()) {
while (!plainLines.isEmpty()) {
QString line = plainLines.first();
t += removeSpecialLines(line, comment);
}
} else {
while (!plainLines.isEmpty()) {
if (match(docLocation, pattern, plainLines.first())) {
return t;
}
t += getLine();
}
failedAtEnd(docLocation, command);
}
return t;
}
QString Quoter::quoteUntil(const Location &docLocation, const QString &command,
const QString &pattern)
{
QString t = quoteTo(docLocation, command, pattern);
t += getLine();
return t;
}
QString Quoter::getLine(int unindent)
{
if (plainLines.isEmpty())
return QString();
plainLines.removeFirst();
QString t = markedLines.takeFirst();
int i = 0;
while (i < unindent && i < t.length() && t[i] == QLatin1Char(' '))
i++;
t = t.mid(i);
t += QLatin1Char('\n');
codeLocation.advanceLines(t.count(QLatin1Char('\n')));
return t;
}
bool Quoter::match(const Location &docLocation, const QString &pattern0, const QString &line)
{
QString str = line;
while (str.endsWith(QLatin1Char('\n')))
str.truncate(str.length() - 1);
QString pattern = pattern0;
if (pattern.startsWith(QLatin1Char('/')) && pattern.endsWith(QLatin1Char('/'))
&& pattern.length() > 2) {
QRegExp rx(pattern.mid(1, pattern.length() - 2));
if (!silent && !rx.isValid()) {
docLocation.warning(tr("Invalid regular expression '%1'").arg(rx.pattern()));
silent = true;
}
return str.indexOf(rx) != -1;
}
trimWhiteSpace(str);
trimWhiteSpace(pattern);
return str.indexOf(pattern) != -1;
}
void Quoter::failedAtEnd(const Location &docLocation, const QString &command)
{
if (!silent && !command.isEmpty()) {
if (codeLocation.filePath().isEmpty()) {
docLocation.warning(tr("Unexpected '\\%1'").arg(command));
} else {
docLocation.warning(tr("Command '\\%1' failed at end of file '%2'")
.arg(command)
.arg(codeLocation.filePath()));
}
silent = true;
}
}
QString Quoter::commentForCode() const
{
QFileInfo fi = QFileInfo(codeLocation.fileName());
if (fi.fileName() == "CMakeLists.txt")
return "#!";
return commentHash.value(fi.suffix(), "//!");
}
QString Quoter::removeSpecialLines(const QString &line, const QString &comment, int unindent)
{
QString t;
// Remove special macros to support Qt namespacing.
QString trimmed = line.trimmed();
if (trimmed.startsWith("QT_BEGIN_NAMESPACE")) {
getLine();
} else if (trimmed.startsWith("QT_END_NAMESPACE")) {
getLine();
t += QLatin1Char('\n');
} else if (!trimmed.startsWith(comment)) {
// Ordinary code
t += getLine(unindent);
} else {
// Comments
if (line.contains(QLatin1Char('\n')))
t += QLatin1Char('\n');
getLine();
}
return t;
}
QT_END_NAMESPACE