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