File indexing completed on 2024-05-12 04:02:19

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 "abstracthighlighter_p.h"
0010 #include "definition.h"
0011 #include "definition_p.h"
0012 #include "format.h"
0013 #include "ksyntaxhighlighting_logging.h"
0014 #include "state.h"
0015 #include "theme.h"
0016 
0017 #include <QFile>
0018 #include <QFileInfo>
0019 #include <QIODevice>
0020 #include <QTextStream>
0021 
0022 using namespace KSyntaxHighlighting;
0023 
0024 class KSyntaxHighlighting::HtmlHighlighterPrivate : public AbstractHighlighterPrivate
0025 {
0026 public:
0027     std::unique_ptr<QTextStream> out;
0028     std::unique_ptr<QFile> file;
0029     QString currentLine;
0030     std::vector<QString> htmlStyles;
0031 };
0032 
0033 HtmlHighlighter::HtmlHighlighter()
0034     : AbstractHighlighter(new HtmlHighlighterPrivate())
0035 {
0036 }
0037 
0038 HtmlHighlighter::~HtmlHighlighter()
0039 {
0040 }
0041 
0042 void HtmlHighlighter::setOutputFile(const QString &fileName)
0043 {
0044     Q_D(HtmlHighlighter);
0045     d->file.reset(new QFile(fileName));
0046     if (!d->file->open(QFile::WriteOnly | QFile::Truncate)) {
0047         qCWarning(Log) << "Failed to open output file" << fileName << ":" << d->file->errorString();
0048         return;
0049     }
0050     d->out.reset(new QTextStream(d->file.get()));
0051     d->out->setEncoding(QStringConverter::Utf8);
0052 }
0053 
0054 void HtmlHighlighter::setOutputFile(FILE *fileHandle)
0055 {
0056     Q_D(HtmlHighlighter);
0057     d->out.reset(new QTextStream(fileHandle, QIODevice::WriteOnly));
0058     d->out->setEncoding(QStringConverter::Utf8);
0059 }
0060 
0061 void HtmlHighlighter::highlightFile(const QString &fileName, const QString &title)
0062 {
0063     QFileInfo fi(fileName);
0064     QFile f(fileName);
0065     if (!f.open(QFile::ReadOnly)) {
0066         qCWarning(Log) << "Failed to open input file" << fileName << ":" << f.errorString();
0067         return;
0068     }
0069 
0070     if (title.isEmpty()) {
0071         highlightData(&f, fi.fileName());
0072     } else {
0073         highlightData(&f, title);
0074     }
0075 }
0076 
0077 /**
0078  * @brief toHtmlRgba
0079  * Converts QColor -> #RRGGBBAA if there is an alpha channel
0080  * otherwise it will just return the hexcode. This is because QColor
0081  * outputs #AARRGGBB, whereas browser support #RRGGBBAA.
0082  *
0083  * @param color
0084  * @return
0085  */
0086 static QString toHtmlRgbaString(const QColor &color)
0087 {
0088     if (color.alpha() == 0xFF) {
0089         return color.name();
0090     }
0091     static const char16_t digits[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
0092     QChar hexcode[9];
0093     hexcode[0] = QLatin1Char('#');
0094     hexcode[1] = digits[color.red() >> 4];
0095     hexcode[2] = digits[color.red() & 0xf];
0096     hexcode[3] = digits[color.green() >> 4];
0097     hexcode[4] = digits[color.green() & 0xf];
0098     hexcode[5] = digits[color.blue() >> 4];
0099     hexcode[6] = digits[color.blue() & 0xf];
0100     hexcode[7] = digits[color.alpha() >> 4];
0101     hexcode[8] = digits[color.alpha() & 0xf];
0102     return QString(hexcode, 9);
0103 }
0104 
0105 void HtmlHighlighter::highlightData(QIODevice *dev, const QString &title)
0106 {
0107     Q_D(HtmlHighlighter);
0108 
0109     if (!d->out) {
0110         qCWarning(Log) << "No output stream defined!";
0111         return;
0112     }
0113 
0114     QString htmlTitle;
0115     if (title.isEmpty()) {
0116         htmlTitle = QStringLiteral("KSyntaxHighlighter");
0117     } else {
0118         htmlTitle = title.toHtmlEscaped();
0119     }
0120 
0121     const auto &theme = d->m_theme;
0122     const auto &definition = d->m_definition;
0123 
0124     auto definitions = definition.includedDefinitions();
0125     definitions.append(definition);
0126 
0127     int maxId = 0;
0128     for (const auto &definition : std::as_const(definitions)) {
0129         for (const auto &format : std::as_const(DefinitionData::get(definition)->formats)) {
0130             maxId = qMax(maxId, format.id());
0131         }
0132     }
0133     d->htmlStyles.clear();
0134     // htmlStyles must not be empty for applyFormat to work even with a definition without any context
0135     d->htmlStyles.resize(maxId + 1);
0136 
0137     // initialize htmlStyles
0138     for (const auto &definition : std::as_const(definitions)) {
0139         for (const auto &format : std::as_const(DefinitionData::get(definition)->formats)) {
0140             auto &buffer = d->htmlStyles[format.id()];
0141             if (format.hasTextColor(theme)) {
0142                 buffer += QStringLiteral("color:") + toHtmlRgbaString(format.textColor(theme)) + QStringLiteral(";");
0143             }
0144             if (format.hasBackgroundColor(theme)) {
0145                 buffer += QStringLiteral("background-color:") + toHtmlRgbaString(format.backgroundColor(theme)) + QStringLiteral(";");
0146             }
0147             if (format.isBold(theme)) {
0148                 buffer += QStringLiteral("font-weight:bold;");
0149             }
0150             if (format.isItalic(theme)) {
0151                 buffer += QStringLiteral("font-style:italic;");
0152             }
0153             if (format.isUnderline(theme)) {
0154                 buffer += QStringLiteral("text-decoration:underline;");
0155             }
0156             if (format.isStrikeThrough(theme)) {
0157                 buffer += QStringLiteral("text-decoration:line-through;");
0158             }
0159 
0160             if (!buffer.isEmpty()) {
0161                 buffer.insert(0, QStringLiteral("<span style=\""));
0162                 // replace last ';'
0163                 buffer.back() = u'"';
0164                 buffer += u'>';
0165             }
0166         }
0167     }
0168 
0169     State state;
0170     *d->out << "<!DOCTYPE html>\n";
0171     *d->out << "<html><head>\n";
0172     *d->out << "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"/>\n";
0173     *d->out << "<title>" << htmlTitle << "</title>\n";
0174     *d->out << "<meta name=\"generator\" content=\"KF5::SyntaxHighlighting - Definition (" << definition.name() << ") - Theme (" << theme.name() << ")\"/>\n";
0175     *d->out << "</head><body";
0176     *d->out << " style=\"background-color:" << toHtmlRgbaString(QColor::fromRgba(theme.editorColor(Theme::BackgroundColor)));
0177     if (theme.textColor(Theme::Normal)) {
0178         *d->out << ";color:" << toHtmlRgbaString(QColor::fromRgba(theme.textColor(Theme::Normal)));
0179     }
0180     *d->out << "\"><pre>\n";
0181 
0182     QTextStream in(dev);
0183     while (in.readLineInto(&d->currentLine)) {
0184         state = highlightLine(d->currentLine, state);
0185         *d->out << "\n";
0186     }
0187 
0188     *d->out << "</pre></body></html>\n";
0189     d->out->flush();
0190 
0191     d->out.reset();
0192     d->file.reset();
0193 }
0194 
0195 void HtmlHighlighter::applyFormat(int offset, int length, const Format &format)
0196 {
0197     if (length == 0) {
0198         return;
0199     }
0200 
0201     Q_D(HtmlHighlighter);
0202 
0203     auto const &htmlStyle = d->htmlStyles[format.id()];
0204 
0205     if (!htmlStyle.isEmpty()) {
0206         *d->out << htmlStyle;
0207     }
0208 
0209     for (QChar ch : QStringView(d->currentLine).mid(offset, length)) {
0210         if (ch == u'<')
0211             *d->out << QStringLiteral("&lt;");
0212         else if (ch == u'&')
0213             *d->out << QStringLiteral("&amp;");
0214         else
0215             *d->out << ch;
0216     }
0217 
0218     if (!htmlStyle.isEmpty()) {
0219         *d->out << QStringLiteral("</span>");
0220     }
0221 }