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
16#include <QFile>
17#include <QFileInfo>
18#include <QIODevice>
19#include <QTextStream>
20
21using namespace KSyntaxHighlighting;
22
23class KSyntaxHighlighting::HtmlHighlighterPrivate : public AbstractHighlighterPrivate
24{
25public:
26 std::unique_ptr<QTextStream> out;
27 std::unique_ptr<QFile> file;
28 QString currentLine;
29 std::vector<QString> htmlStyles;
30 Theme::EditorColorRole bgRole = Theme::BackgroundColor;
31};
32
33HtmlHighlighter::HtmlHighlighter()
34 : AbstractHighlighter(new HtmlHighlighterPrivate())
35{
36}
37
38HtmlHighlighter::~HtmlHighlighter()
39{
40}
41
42void HtmlHighlighter::setBackgroundRole(Theme::EditorColorRole bgRole)
43{
44 Q_D(HtmlHighlighter);
45 d->bgRole = bgRole;
46}
47
48void HtmlHighlighter::setOutputFile(const QString &fileName)
49{
50 Q_D(HtmlHighlighter);
51 d->file.reset(p: new QFile(fileName));
52 if (!d->file->open(flags: QFile::WriteOnly | QFile::Truncate)) {
53 qCWarning(Log) << "Failed to open output file" << fileName << ":" << d->file->errorString();
54 return;
55 }
56 d->out.reset(p: new QTextStream(d->file.get()));
57 d->out->setEncoding(QStringConverter::Utf8);
58}
59
60void HtmlHighlighter::setOutputFile(FILE *fileHandle)
61{
62 Q_D(HtmlHighlighter);
63 d->out.reset(p: new QTextStream(fileHandle, QIODevice::WriteOnly));
64 d->out->setEncoding(QStringConverter::Utf8);
65}
66
67void HtmlHighlighter::highlightFile(const QString &fileName, const QString &title)
68{
69 QFile f(fileName);
70 if (!f.open(flags: QFile::ReadOnly)) {
71 qCWarning(Log) << "Failed to open input file" << fileName << ":" << f.errorString();
72 return;
73 }
74
75 if (title.isEmpty()) {
76 QFileInfo fi(fileName);
77 highlightData(device: &f, title: fi.fileName());
78 } else {
79 highlightData(device: &f, title);
80 }
81}
82
83namespace
84{
85/**
86 * @brief toHtmlRgba
87 * Converts QRgb -> #RRGGBBAA if there is an alpha channel
88 * otherwise it will just return the hexcode. This is because QColor
89 * outputs #AARRGGBB, whereas browser support #RRGGBBAA.
90 */
91struct HtmlColor {
92 HtmlColor(QRgb argb)
93 {
94 static const char16_t *digits = u"0123456789abcdef";
95
96 hexcode[0] = u'#';
97 hexcode[1] = digits[qRed(rgb: argb) >> 4];
98 hexcode[2] = digits[qRed(rgb: argb) & 0xf];
99 hexcode[3] = digits[qGreen(rgb: argb) >> 4];
100 hexcode[4] = digits[qGreen(rgb: argb) & 0xf];
101 hexcode[5] = digits[qBlue(rgb: argb) >> 4];
102 hexcode[6] = digits[qBlue(rgb: argb) & 0xf];
103 if (qAlpha(rgb: argb) == 0xff) {
104 len = 7;
105 } else {
106 hexcode[7] = digits[qAlpha(rgb: argb) >> 4];
107 hexcode[8] = digits[qAlpha(rgb: argb) & 0xf];
108 len = 9;
109 }
110 }
111
112 QStringView sv() const
113 {
114 return QStringView(hexcode, len);
115 }
116
117private:
118 QChar hexcode[9];
119 qsizetype len;
120};
121}
122
123void HtmlHighlighter::highlightData(QIODevice *dev, const QString &title)
124{
125 Q_D(HtmlHighlighter);
126
127 if (!d->out) {
128 qCWarning(Log) << "No output stream defined!";
129 return;
130 }
131
132 QString htmlTitle;
133 if (title.isEmpty()) {
134 htmlTitle = QStringLiteral("KSyntaxHighlighter");
135 } else {
136 htmlTitle = title.toHtmlEscaped();
137 }
138
139 const auto &theme = d->m_theme;
140 const auto &definition = d->m_definition;
141 const bool useSelectedText = d->bgRole == Theme::TextSelection;
142
143 auto definitions = definition.includedDefinitions();
144 definitions.append(t: definition);
145
146 const auto mainTextColor = [&] {
147 if (useSelectedText) {
148 const auto fg = theme.selectedTextColor(style: Theme::Normal);
149 if (fg) {
150 return fg;
151 }
152 }
153 return theme.textColor(style: Theme::Normal);
154 }();
155 const auto mainBgColor = theme.editorColor(role: d->bgRole);
156
157 int maxId = 0;
158 for (const auto &definition : std::as_const(t&: definitions)) {
159 for (const auto &format : std::as_const(t&: DefinitionData::get(def: definition)->formats)) {
160 maxId = qMax(a: maxId, b: format.id());
161 }
162 }
163 d->htmlStyles.clear();
164 // htmlStyles must not be empty for applyFormat to work even with a definition without any context
165 d->htmlStyles.resize(new_size: maxId + 1);
166
167 // initialize htmlStyles
168 for (const auto &definition : std::as_const(t&: definitions)) {
169 for (const auto &format : std::as_const(t&: DefinitionData::get(def: definition)->formats)) {
170 auto &buffer = d->htmlStyles[format.id()];
171
172 const auto textColor = useSelectedText ? format.selectedTextColor(theme).rgba() : format.textColor(theme).rgba();
173 if (textColor && textColor != mainTextColor) {
174 buffer += QStringLiteral("color:") + HtmlColor(textColor).sv() + u';';
175 }
176 const auto bgColor = useSelectedText ? format.selectedBackgroundColor(theme).rgba() : format.backgroundColor(theme).rgba();
177 if (bgColor && bgColor != mainBgColor) {
178 buffer += QStringLiteral("background-color:") + HtmlColor(bgColor).sv() + u';';
179 }
180 if (format.isBold(theme)) {
181 buffer += QStringLiteral("font-weight:bold;");
182 }
183 if (format.isItalic(theme)) {
184 buffer += QStringLiteral("font-style:italic;");
185 }
186 if (format.isUnderline(theme)) {
187 buffer += QStringLiteral("text-decoration:underline;");
188 }
189 if (format.isStrikeThrough(theme)) {
190 buffer += QStringLiteral("text-decoration:line-through;");
191 }
192
193 if (!buffer.isEmpty()) {
194 buffer.insert(i: 0, QStringLiteral("<span style=\""));
195 // replace last ';'
196 buffer.back() = u'"';
197 buffer += u'>';
198 }
199 }
200 }
201
202 State state;
203 *d->out << "<!DOCTYPE html>\n";
204 *d->out << "<html><head>\n";
205 *d->out << "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"/>\n";
206 *d->out << "<title>" << htmlTitle << "</title>\n";
207 *d->out << "<meta name=\"generator\" content=\"KF5::SyntaxHighlighting - Definition (" << definition.name() << ") - Theme (" << theme.name() << ")\"/>\n";
208 *d->out << "</head><body";
209 *d->out << " style=\"background-color:" << HtmlColor(mainBgColor).sv();
210 *d->out << ";color:" << HtmlColor(mainTextColor).sv();
211 *d->out << "\"><pre>\n";
212
213 QTextStream in(dev);
214 while (in.readLineInto(line: &d->currentLine)) {
215 state = highlightLine(text: d->currentLine, state);
216 *d->out << "\n";
217 }
218
219 *d->out << "</pre></body></html>\n";
220 d->out->flush();
221
222 d->out.reset();
223 d->file.reset();
224}
225
226void HtmlHighlighter::applyFormat(int offset, int length, const Format &format)
227{
228 if (length == 0) {
229 return;
230 }
231
232 Q_D(HtmlHighlighter);
233
234 auto const &htmlStyle = d->htmlStyles[format.id()];
235
236 if (!htmlStyle.isEmpty()) {
237 *d->out << htmlStyle;
238 }
239
240 for (QChar ch : QStringView(d->currentLine).sliced(pos: offset, n: length)) {
241 if (ch == u'<')
242 *d->out << QStringLiteral("&lt;");
243 else if (ch == u'&')
244 *d->out << QStringLiteral("&amp;");
245 else
246 *d->out << ch;
247 }
248
249 if (!htmlStyle.isEmpty()) {
250 *d->out << QStringLiteral("</span>");
251 }
252}
253

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