SourceHighlighter.cpp 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. /*
  2. * Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
  3. * Copyright (c) 2024, Sam Atkins <sam@ladybird.org>
  4. *
  5. * SPDX-License-Identifier: BSD-2-Clause
  6. */
  7. #include <AK/StringBuilder.h>
  8. #include <LibURL/URL.h>
  9. #include <LibWeb/HTML/SyntaxHighlighter/SyntaxHighlighter.h>
  10. #include <LibWebView/SourceHighlighter.h>
  11. namespace WebView {
  12. SourceDocument::SourceDocument(StringView source)
  13. : m_source(source)
  14. {
  15. m_source.for_each_split_view('\n', AK::SplitBehavior::KeepEmpty, [&](auto line) {
  16. m_lines.append(Syntax::TextDocumentLine { *this, line });
  17. });
  18. }
  19. Syntax::TextDocumentLine& SourceDocument::line(size_t line_index)
  20. {
  21. return m_lines[line_index];
  22. }
  23. Syntax::TextDocumentLine const& SourceDocument::line(size_t line_index) const
  24. {
  25. return m_lines[line_index];
  26. }
  27. SourceHighlighterClient::SourceHighlighterClient(StringView source, Syntax::Language language)
  28. : m_document(SourceDocument::create(source))
  29. {
  30. // HACK: Syntax highlighters require a palette, but we don't actually care about the output styling, only the type of token for each span.
  31. // Also, getting a palette from the chrome is nontrivial. So, create a dummy blank one and use that.
  32. auto buffer = MUST(Core::AnonymousBuffer::create_with_size(sizeof(Gfx::SystemTheme)));
  33. auto palette_impl = Gfx::PaletteImpl::create_with_anonymous_buffer(buffer);
  34. Gfx::Palette dummy_palette { palette_impl };
  35. switch (language) {
  36. case Syntax::Language::HTML:
  37. m_highlighter = make<Web::HTML::SyntaxHighlighter>();
  38. break;
  39. default:
  40. break;
  41. }
  42. if (m_highlighter) {
  43. m_highlighter->attach(*this);
  44. m_highlighter->rehighlight(dummy_palette);
  45. }
  46. }
  47. Vector<Syntax::TextDocumentSpan> const& SourceHighlighterClient::spans() const
  48. {
  49. return document().spans();
  50. }
  51. void SourceHighlighterClient::set_span_at_index(size_t index, Syntax::TextDocumentSpan span)
  52. {
  53. document().set_span_at_index(index, span);
  54. }
  55. Vector<Syntax::TextDocumentFoldingRegion>& SourceHighlighterClient::folding_regions()
  56. {
  57. return document().folding_regions();
  58. }
  59. Vector<Syntax::TextDocumentFoldingRegion> const& SourceHighlighterClient::folding_regions() const
  60. {
  61. return document().folding_regions();
  62. }
  63. ByteString SourceHighlighterClient::highlighter_did_request_text() const
  64. {
  65. return document().text();
  66. }
  67. void SourceHighlighterClient::highlighter_did_request_update()
  68. {
  69. // No-op
  70. }
  71. Syntax::Document& SourceHighlighterClient::highlighter_did_request_document()
  72. {
  73. return document();
  74. }
  75. Syntax::TextPosition SourceHighlighterClient::highlighter_did_request_cursor() const
  76. {
  77. return {};
  78. }
  79. void SourceHighlighterClient::highlighter_did_set_spans(Vector<Syntax::TextDocumentSpan> spans)
  80. {
  81. document().set_spans(span_collection_index, move(spans));
  82. }
  83. void SourceHighlighterClient::highlighter_did_set_folding_regions(Vector<Syntax::TextDocumentFoldingRegion> folding_regions)
  84. {
  85. document().set_folding_regions(move(folding_regions));
  86. }
  87. String highlight_source(URL::URL const& url, StringView source)
  88. {
  89. SourceHighlighterClient highlighter_client { source, Syntax::Language::HTML };
  90. return highlighter_client.to_html_string(url);
  91. }
  92. StringView SourceHighlighterClient::class_for_token(u64 token_type) const
  93. {
  94. switch (static_cast<Web::HTML::AugmentedTokenKind>(token_type)) {
  95. case Web::HTML::AugmentedTokenKind::AttributeName:
  96. return "attribute-name"sv;
  97. case Web::HTML::AugmentedTokenKind::AttributeValue:
  98. return "attribute-value"sv;
  99. case Web::HTML::AugmentedTokenKind::OpenTag:
  100. case Web::HTML::AugmentedTokenKind::CloseTag:
  101. return "tag"sv;
  102. case Web::HTML::AugmentedTokenKind::Comment:
  103. return "comment"sv;
  104. case Web::HTML::AugmentedTokenKind::Doctype:
  105. return "doctype"sv;
  106. case Web::HTML::AugmentedTokenKind::__Count:
  107. default:
  108. break;
  109. }
  110. return "unknown"sv;
  111. }
  112. static String generate_style()
  113. {
  114. StringBuilder builder;
  115. builder.append(HTML_HIGHLIGHTER_STYLE);
  116. builder.append(R"~~~(
  117. .html {
  118. counter-reset: line;
  119. }
  120. .line {
  121. counter-increment: line;
  122. white-space: nowrap;
  123. }
  124. .line::before {
  125. content: counter(line) " ";
  126. display: inline-block;
  127. width: 2.5em;
  128. padding-right: 0.5em;
  129. text-align: right;
  130. }
  131. @media (prefers-color-scheme: dark) {
  132. .line::before {
  133. color: darkgrey;
  134. }
  135. }
  136. @media (prefers-color-scheme: light) {
  137. .line::before {
  138. color: dimgray;
  139. }
  140. }
  141. )~~~"sv);
  142. return MUST(builder.to_string());
  143. }
  144. String SourceHighlighterClient::to_html_string(URL::URL const& url) const
  145. {
  146. StringBuilder builder;
  147. auto append_escaped = [&](Utf32View text) {
  148. for (auto code_point : text) {
  149. if (code_point == '&') {
  150. builder.append("&amp;"sv);
  151. } else if (code_point == 0xA0) {
  152. builder.append("&nbsp;"sv);
  153. } else if (code_point == '<') {
  154. builder.append("&lt;"sv);
  155. } else if (code_point == '>') {
  156. builder.append("&gt;"sv);
  157. } else {
  158. builder.append_code_point(code_point);
  159. }
  160. }
  161. };
  162. auto start_token = [&](u64 type) {
  163. builder.appendff("<span class=\"{}\">", class_for_token(type));
  164. };
  165. auto end_token = [&]() {
  166. builder.append("</span>"sv);
  167. };
  168. builder.append(R"~~~(
  169. <!DOCTYPE html>
  170. <html>
  171. <head>
  172. <meta name="color-scheme" content="dark light">)~~~"sv);
  173. builder.appendff("<title>View Source - {}</title>", escape_html_entities(MUST(url.to_string())));
  174. builder.appendff("<style type=\"text/css\">{}</style>", generate_style());
  175. builder.append(R"~~~(
  176. </head>
  177. <body>
  178. <pre class="html">)~~~"sv);
  179. size_t span_index = 0;
  180. for (size_t line_index = 0; line_index < document().line_count(); ++line_index) {
  181. auto& line = document().line(line_index);
  182. auto line_view = line.view();
  183. builder.append("<div class=\"line\">"sv);
  184. size_t next_column = 0;
  185. auto draw_text_helper = [&](size_t start, size_t end, Optional<Syntax::TextDocumentSpan const&> span) {
  186. size_t length = end - start;
  187. if (length == 0)
  188. return;
  189. auto text = line_view.substring_view(start, length);
  190. if (span.has_value()) {
  191. start_token(span->data);
  192. append_escaped(text);
  193. end_token();
  194. } else {
  195. append_escaped(text);
  196. }
  197. };
  198. while (span_index < document().spans().size()) {
  199. auto& span = document().spans()[span_index];
  200. if (span.range.start().line() > line_index) {
  201. // No more spans in this line, moving on
  202. break;
  203. }
  204. size_t span_start;
  205. if (span.range.start().line() < line_index) {
  206. span_start = 0;
  207. } else {
  208. span_start = span.range.start().column();
  209. }
  210. size_t span_end;
  211. bool span_consumed;
  212. if (span.range.end().line() > line_index) {
  213. span_end = line.length();
  214. span_consumed = false;
  215. } else {
  216. span_end = span.range.end().column();
  217. span_consumed = true;
  218. }
  219. if (span_start != next_column) {
  220. // Draw unspanned text between spans
  221. draw_text_helper(next_column, span_start, {});
  222. }
  223. draw_text_helper(span_start, span_end, span);
  224. next_column = span_end;
  225. if (!span_consumed) {
  226. // Continue with same span on next line
  227. break;
  228. } else {
  229. ++span_index;
  230. }
  231. }
  232. // Draw unspanned text after last span
  233. if (next_column < line.length()) {
  234. draw_text_helper(next_column, line.length(), {});
  235. }
  236. builder.append("</div>"sv);
  237. }
  238. builder.append(R"~~~(
  239. </pre>
  240. </body>
  241. </html>
  242. )~~~"sv);
  243. return builder.to_string_without_validation();
  244. }
  245. }