1/*
2 SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
3 SPDX-FileCopyrightText: 2018 Christoph Cullmann <cullmann@kde.org>
4
5 SPDX-License-Identifier: MIT
6*/
7
8#include "htmlhighlighter.h"
9#include "abstracthighlighter_p.h"
10#include "definition.h"
11#include "definition_p.h"
12#include "format.h"
13#include "ksyntaxhighlighting_logging.h"
14#include "state.h"
15#include "theme.h"
16
17#include <QFile>
18#include <QFileInfo>
19#include <QIODevice>
20#include <QTextStream>
21
22using namespace KSyntaxHighlighting;
23
24class KSyntaxHighlighting::HtmlHighlighterPrivate : public AbstractHighlighterPrivate
25{
26public:
27 std::unique_ptr<QTextStream> out;
28 std::unique_ptr<QFile> file;
29 QString currentLine;
30 std::vector<QString> htmlStyles;
31};
32
33HtmlHighlighter::HtmlHighlighter()
34 : AbstractHighlighter(new HtmlHighlighterPrivate())
35{
36}
37
38HtmlHighlighter::~HtmlHighlighter()
39{
40}
41
42void HtmlHighlighter::setOutputFile(const QString &fileName)
43{
44 Q_D(HtmlHighlighter);
45 d->file.reset(new QFile(fileName));
46 if (!d->file->open(QFile::WriteOnly | QFile::Truncate)) {
47 qCWarning(Log) << "Failed to open output file" << fileName << ":" << d->file->errorString();
48 return;
49 }
50 d->out.reset(new QTextStream(d->file.get()));
51 d->out->setEncoding(QStringConverter::Utf8);
52}
53
54void HtmlHighlighter::setOutputFile(FILE *fileHandle)
55{
56 Q_D(HtmlHighlighter);
57 d->out.reset(new QTextStream(fileHandle, QIODevice::WriteOnly));
58 d->out->setEncoding(QStringConverter::Utf8);
59}
60
61void HtmlHighlighter::highlightFile(const QString &fileName, const QString &title)
62{
63 QFileInfo fi(fileName);
64 QFile f(fileName);
65 if (!f.open(QFile::ReadOnly)) {
66 qCWarning(Log) << "Failed to open input file" << fileName << ":" << f.errorString();
67 return;
68 }
69
70 if (title.isEmpty()) {
71 highlightData(&f, fi.fileName());
72 } else {
73 highlightData(&f, title);
74 }
75}
76
77/**
78 * @brief toHtmlRgba
79 * Converts QColor -> #RRGGBBAA if there is an alpha channel
80 * otherwise it will just return the hexcode. This is because QColor
81 * outputs #AARRGGBB, whereas browser support #RRGGBBAA.
82 *
83 * @param color
84 * @return
85 */
86static QString toHtmlRgbaString(const QColor &color)
87{
88 if (color.alpha() == 0xFF) {
89 return color.name();
90 }
91 static const char16_t digits[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
92 QChar hexcode[9];
93 hexcode[0] = QLatin1Char('#');
94 hexcode[1] = digits[color.red() >> 4];
95 hexcode[2] = digits[color.red() & 0xf];
96 hexcode[3] = digits[color.green() >> 4];
97 hexcode[4] = digits[color.green() & 0xf];
98 hexcode[5] = digits[color.blue() >> 4];
99 hexcode[6] = digits[color.blue() & 0xf];
100 hexcode[7] = digits[color.alpha() >> 4];
101 hexcode[8] = digits[color.alpha() & 0xf];
102 return QString(hexcode, 9);
103}
104
105void HtmlHighlighter::highlightData(QIODevice *dev, const QString &title)
106{
107 Q_D(HtmlHighlighter);
108
109 if (!d->out) {
110 qCWarning(Log) << "No output stream defined!";
111 return;
112 }
113
114 QString htmlTitle;
115 if (title.isEmpty()) {
116 htmlTitle = QStringLiteral("KSyntaxHighlighter");
117 } else {
118 htmlTitle = title.toHtmlEscaped();
119 }
120
121 const auto &theme = d->m_theme;
122 const auto &definition = d->m_definition;
123
124 auto definitions = definition.includedDefinitions();
125 definitions.append(definition);
126
127 int maxId = 0;
128 for (const auto &definition : std::as_const(definitions)) {
129 for (const auto &format : std::as_const(DefinitionData::get(definition)->formats)) {
130 maxId = qMax(maxId, format.id());
131 }
132 }
133 d->htmlStyles.clear();
134 // htmlStyles must not be empty for applyFormat to work even with a definition without any context
135 d->htmlStyles.resize(maxId + 1);
136
137 // initialize htmlStyles
138 for (const auto &definition : std::as_const(definitions)) {
139 for (const auto &format : std::as_const(DefinitionData::get(definition)->formats)) {
140 auto &buffer = d->htmlStyles[format.id()];
141 if (format.hasTextColor(theme)) {
142 buffer += QStringLiteral("color:") + toHtmlRgbaString(format.textColor(theme)) + QStringLiteral(";");
143 }
144 if (format.hasBackgroundColor(theme)) {
145 buffer += QStringLiteral("background-color:") + toHtmlRgbaString(format.backgroundColor(theme)) + QStringLiteral(";");
146 }
147 if (format.isBold(theme)) {
148 buffer += QStringLiteral("font-weight:bold;");
149 }
150 if (format.isItalic(theme)) {
151 buffer += QStringLiteral("font-style:italic;");
152 }
153 if (format.isUnderline(theme)) {
154 buffer += QStringLiteral("text-decoration:underline;");
155 }
156 if (format.isStrikeThrough(theme)) {
157 buffer += QStringLiteral("text-decoration:line-through;");
158 }
159
160 if (!buffer.isEmpty()) {
161 buffer.insert(0, QStringLiteral("<span style=\""));
162 // replace last ';'
163 buffer.back() = u'"';
164 buffer += u'>';
165 }
166 }
167 }
168
169 State state;
170 *d->out << "<!DOCTYPE html>\n";
171 *d->out << "<html><head>\n";
172 *d->out << "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"/>\n";
173 *d->out << "<title>" << htmlTitle << "</title>\n";
174 *d->out << "<meta name=\"generator\" content=\"KF5::SyntaxHighlighting - Definition (" << definition.name() << ") - Theme (" << theme.name() << ")\"/>\n";
175 *d->out << "</head><body";
176 *d->out << " style=\"background-color:" << toHtmlRgbaString(QColor::fromRgba(theme.editorColor(Theme::BackgroundColor)));
177 if (theme.textColor(Theme::Normal)) {
178 *d->out << ";color:" << toHtmlRgbaString(QColor::fromRgba(theme.textColor(Theme::Normal)));
179 }
180 *d->out << "\"><pre>\n";
181
182 QTextStream in(dev);
183 while (in.readLineInto(&d->currentLine)) {
184 state = highlightLine(d->currentLine, state);
185 *d->out << "\n";
186 }
187
188 *d->out << "</pre></body></html>\n";
189 d->out->flush();
190
191 d->out.reset();
192 d->file.reset();
193}
194
195void HtmlHighlighter::applyFormat(int offset, int length, const Format &format)
196{
197 if (length == 0) {
198 return;
199 }
200
201 Q_D(HtmlHighlighter);
202
203 auto const &htmlStyle = d->htmlStyles[format.id()];
204
205 if (!htmlStyle.isEmpty()) {
206 *d->out << htmlStyle;
207 }
208
209 for (QChar ch : QStringView(d->currentLine).mid(offset, length)) {
210 if (ch == u'<')
211 *d->out << QStringLiteral("&lt;");
212 else if (ch == u'&')
213 *d->out << QStringLiteral("&amp;");
214 else
215 *d->out << ch;
216 }
217
218 if (!htmlStyle.isEmpty()) {
219 *d->out << QStringLiteral("</span>");
220 }
221}
222

source code of syntax-highlighting/src/lib/htmlhighlighter.cpp