File indexing completed on 2024-05-12 15:50:05

0001 /*
0002     SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
0003     SPDX-FileCopyrightText: 2018 Christoph Cullmann <cullmann@kde.org>
0004 
0005     SPDX-License-Identifier: MIT
0006 */
0007 
0008 #include "htmlhighlighter.h"
0009 #include "definition.h"
0010 #include "format.h"
0011 #include "ksyntaxhighlighting_logging.h"
0012 #include "state.h"
0013 #include "theme.h"
0014 
0015 #include <QFile>
0016 #include <QFileInfo>
0017 #include <QTextStream>
0018 #include <QVarLengthArray>
0019 
0020 using namespace KSyntaxHighlighting;
0021 
0022 class KSyntaxHighlighting::HtmlHighlighterPrivate
0023 {
0024 public:
0025     std::unique_ptr<QTextStream> out;
0026     std::unique_ptr<QFile> file;
0027     QString currentLine;
0028 };
0029 
0030 HtmlHighlighter::HtmlHighlighter()
0031     : d(new HtmlHighlighterPrivate())
0032 {
0033 }
0034 
0035 HtmlHighlighter::~HtmlHighlighter()
0036 {
0037 }
0038 
0039 void HtmlHighlighter::setOutputFile(const QString &fileName)
0040 {
0041     d->file.reset(new QFile(fileName));
0042     if (!d->file->open(QFile::WriteOnly | QFile::Truncate)) {
0043         qCWarning(Log) << "Failed to open output file" << fileName << ":" << d->file->errorString();
0044         return;
0045     }
0046     d->out.reset(new QTextStream(d->file.get()));
0047 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0048     d->out->setEncoding(QStringConverter::Utf8);
0049 #else
0050     d->out->setCodec("UTF-8");
0051 #endif
0052 }
0053 
0054 void HtmlHighlighter::setOutputFile(FILE *fileHandle)
0055 {
0056     d->out.reset(new QTextStream(fileHandle, QIODevice::WriteOnly));
0057 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0058     d->out->setEncoding(QStringConverter::Utf8);
0059 #else
0060     d->out->setCodec("UTF-8");
0061 #endif
0062 }
0063 
0064 void HtmlHighlighter::highlightFile(const QString &fileName, const QString &title)
0065 {
0066     QFileInfo fi(fileName);
0067     QFile f(fileName);
0068     if (!f.open(QFile::ReadOnly)) {
0069         qCWarning(Log) << "Failed to open input file" << fileName << ":" << f.errorString();
0070         return;
0071     }
0072 
0073     if (title.isEmpty()) {
0074         highlightData(&f, fi.fileName());
0075     } else {
0076         highlightData(&f, title);
0077     }
0078 }
0079 
0080 /**
0081  * @brief toHtmlRgba
0082  * Converts QColor -> rgba(r, g, b, a) if there is an alpha channel
0083  * otherwise it will just return the hexcode. This is because QColor
0084  * outputs #AARRGGBB, whereas browser support #RRGGBBAA.
0085  *
0086  * @param color
0087  * @return
0088  */
0089 static QString toHtmlRgbaString(const QColor &color)
0090 {
0091     if (color.alpha() == 0xFF) {
0092         return color.name();
0093     }
0094 
0095     QString rgba = QStringLiteral("rgba(");
0096     rgba.append(QString::number(color.red()));
0097     rgba.append(QLatin1Char(','));
0098     rgba.append(QString::number(color.green()));
0099     rgba.append(QLatin1Char(','));
0100     rgba.append(QString::number(color.blue()));
0101     rgba.append(QLatin1Char(','));
0102     // this must be alphaF
0103     rgba.append(QString::number(color.alphaF()));
0104     rgba.append(QLatin1Char(')'));
0105     return rgba;
0106 }
0107 
0108 void HtmlHighlighter::highlightData(QIODevice *dev, const QString &title)
0109 {
0110     if (!d->out) {
0111         qCWarning(Log) << "No output stream defined!";
0112         return;
0113     }
0114 
0115     QString htmlTitle;
0116     if (title.isEmpty()) {
0117         htmlTitle = QStringLiteral("Kate Syntax Highlighter");
0118     } else {
0119         htmlTitle = title.toHtmlEscaped();
0120     }
0121 
0122     State state;
0123     *d->out << "<!DOCTYPE html>\n";
0124     *d->out << "<html><head>\n";
0125     *d->out << "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"/>\n";
0126     *d->out << "<title>" << htmlTitle << "</title>\n";
0127     *d->out << "<meta name=\"generator\" content=\"KF5::SyntaxHighlighting - Definition (" << definition().name() << ") - Theme (" << theme().name()
0128             << ")\"/>\n";
0129     *d->out << "</head><body";
0130     *d->out << " style=\"background-color:" << toHtmlRgbaString(QColor::fromRgba(theme().editorColor(Theme::BackgroundColor)));
0131     if (theme().textColor(Theme::Normal)) {
0132         *d->out << ";color:" << toHtmlRgbaString(QColor::fromRgba(theme().textColor(Theme::Normal)));
0133     }
0134     *d->out << "\"><pre>\n";
0135 
0136     QTextStream in(dev);
0137 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0138     in.setCodec("UTF-8");
0139 #endif
0140     while (in.readLineInto(&d->currentLine)) {
0141         state = highlightLine(d->currentLine, state);
0142         *d->out << "\n";
0143     }
0144 
0145     *d->out << "</pre></body></html>\n";
0146     d->out->flush();
0147 
0148     d->out.reset();
0149     d->file.reset();
0150 }
0151 
0152 void HtmlHighlighter::applyFormat(int offset, int length, const Format &format)
0153 {
0154     if (length == 0) {
0155         return;
0156     }
0157 
0158     // collect potential output, cheaper than thinking about "is there any?"
0159     QVarLengthArray<QString, 16> formatOutput;
0160     if (format.hasTextColor(theme())) {
0161         formatOutput << QStringLiteral("color:") << toHtmlRgbaString(format.textColor(theme())) << QStringLiteral(";");
0162     }
0163     if (format.hasBackgroundColor(theme())) {
0164         formatOutput << QStringLiteral("background-color:") << toHtmlRgbaString(format.backgroundColor(theme())) << QStringLiteral(";");
0165     }
0166     if (format.isBold(theme())) {
0167         formatOutput << QStringLiteral("font-weight:bold;");
0168     }
0169     if (format.isItalic(theme())) {
0170         formatOutput << QStringLiteral("font-style:italic;");
0171     }
0172     if (format.isUnderline(theme())) {
0173         formatOutput << QStringLiteral("text-decoration:underline;");
0174     }
0175     if (format.isStrikeThrough(theme())) {
0176         formatOutput << QStringLiteral("text-decoration:line-through;");
0177     }
0178 
0179     if (!formatOutput.isEmpty()) {
0180         *d->out << "<span style=\"";
0181         for (const auto &out : std::as_const(formatOutput)) {
0182             *d->out << out;
0183         }
0184         *d->out << "\">";
0185     }
0186 
0187     *d->out << d->currentLine.mid(offset, length).toHtmlEscaped();
0188 
0189     if (!formatOutput.isEmpty()) {
0190         *d->out << "</span>";
0191     }
0192 }