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 | }; |
32 | |
33 | HtmlHighlighter::HtmlHighlighter() |
34 | : AbstractHighlighter(new HtmlHighlighterPrivate()) |
35 | { |
36 | } |
37 | |
38 | HtmlHighlighter::~HtmlHighlighter() |
39 | { |
40 | } |
41 | |
42 | void HtmlHighlighter::setOutputFile(const QString &fileName) |
43 | { |
44 | Q_D(HtmlHighlighter); |
45 | d->file.reset(p: new QFile(fileName)); |
46 | if (!d->file->open(flags: QFile::WriteOnly | QFile::Truncate)) { |
47 | qCWarning(Log) << "Failed to open output file" << fileName << ":" << d->file->errorString(); |
48 | return; |
49 | } |
50 | d->out.reset(p: new QTextStream(d->file.get())); |
51 | d->out->setEncoding(QStringConverter::Utf8); |
52 | } |
53 | |
54 | void HtmlHighlighter::setOutputFile(FILE *fileHandle) |
55 | { |
56 | Q_D(HtmlHighlighter); |
57 | d->out.reset(p: new QTextStream(fileHandle, QIODevice::WriteOnly)); |
58 | d->out->setEncoding(QStringConverter::Utf8); |
59 | } |
60 | |
61 | void HtmlHighlighter::highlightFile(const QString &fileName, const QString &title) |
62 | { |
63 | QFileInfo fi(fileName); |
64 | QFile f(fileName); |
65 | if (!f.open(flags: QFile::ReadOnly)) { |
66 | qCWarning(Log) << "Failed to open input file" << fileName << ":" << f.errorString(); |
67 | return; |
68 | } |
69 | |
70 | if (title.isEmpty()) { |
71 | highlightData(device: &f, title: fi.fileName()); |
72 | } else { |
73 | highlightData(device: &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 | */ |
86 | static 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 | |
105 | void 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(t: definition); |
126 | |
127 | int maxId = 0; |
128 | for (const auto &definition : std::as_const(t&: definitions)) { |
129 | for (const auto &format : std::as_const(t&: DefinitionData::get(def: definition)->formats)) { |
130 | maxId = qMax(a: maxId, b: 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(new_size: maxId + 1); |
136 | |
137 | // initialize htmlStyles |
138 | for (const auto &definition : std::as_const(t&: definitions)) { |
139 | for (const auto &format : std::as_const(t&: DefinitionData::get(def: definition)->formats)) { |
140 | auto &buffer = d->htmlStyles[format.id()]; |
141 | if (format.hasTextColor(theme)) { |
142 | buffer += QStringLiteral("color:" ) + toHtmlRgbaString(color: format.textColor(theme)) + QStringLiteral(";" ); |
143 | } |
144 | if (format.hasBackgroundColor(theme)) { |
145 | buffer += QStringLiteral("background-color:" ) + toHtmlRgbaString(color: 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(i: 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(color: QColor::fromRgba(rgba: theme.editorColor(role: Theme::BackgroundColor))); |
177 | if (theme.textColor(style: Theme::Normal)) { |
178 | *d->out << ";color:" << toHtmlRgbaString(color: QColor::fromRgba(rgba: theme.textColor(style: Theme::Normal))); |
179 | } |
180 | *d->out << "\"><pre>\n" ; |
181 | |
182 | QTextStream in(dev); |
183 | while (in.readLineInto(line: &d->currentLine)) { |
184 | state = highlightLine(text: 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 | |
195 | void 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(pos: offset, n: length)) { |
210 | if (ch == u'<') |
211 | *d->out << QStringLiteral("<" ); |
212 | else if (ch == u'&') |
213 | *d->out << QStringLiteral("&" ); |
214 | else |
215 | *d->out << ch; |
216 | } |
217 | |
218 | if (!htmlStyle.isEmpty()) { |
219 | *d->out << QStringLiteral("</span>" ); |
220 | } |
221 | } |
222 | |