SourceHighlighter.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  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 <LibJS/SyntaxHighlighter.h>
  9. #include <LibJS/Token.h>
  10. #include <LibURL/URL.h>
  11. #include <LibWeb/CSS/Parser/Token.h>
  12. #include <LibWeb/CSS/SyntaxHighlighter/SyntaxHighlighter.h>
  13. #include <LibWeb/HTML/SyntaxHighlighter/SyntaxHighlighter.h>
  14. #include <LibWebView/SourceHighlighter.h>
  15. namespace WebView {
  16. SourceDocument::SourceDocument(StringView source)
  17. : m_source(source)
  18. {
  19. m_source.for_each_split_view('\n', AK::SplitBehavior::KeepEmpty, [&](auto line) {
  20. m_lines.append(Syntax::TextDocumentLine { *this, line });
  21. });
  22. }
  23. Syntax::TextDocumentLine& SourceDocument::line(size_t line_index)
  24. {
  25. return m_lines[line_index];
  26. }
  27. Syntax::TextDocumentLine const& SourceDocument::line(size_t line_index) const
  28. {
  29. return m_lines[line_index];
  30. }
  31. SourceHighlighterClient::SourceHighlighterClient(StringView source, Syntax::Language language)
  32. : m_document(SourceDocument::create(source))
  33. {
  34. // HACK: Syntax highlighters require a palette, but we don't actually care about the output styling, only the type of token for each span.
  35. // Also, getting a palette from the chrome is nontrivial. So, create a dummy blank one and use that.
  36. auto buffer = MUST(Core::AnonymousBuffer::create_with_size(sizeof(Gfx::SystemTheme)));
  37. auto palette_impl = Gfx::PaletteImpl::create_with_anonymous_buffer(buffer);
  38. Gfx::Palette dummy_palette { palette_impl };
  39. switch (language) {
  40. case Syntax::Language::CSS:
  41. m_highlighter = make<Web::CSS::SyntaxHighlighter>();
  42. break;
  43. case Syntax::Language::HTML:
  44. m_highlighter = make<Web::HTML::SyntaxHighlighter>();
  45. break;
  46. case Syntax::Language::JavaScript:
  47. m_highlighter = make<JS::SyntaxHighlighter>();
  48. break;
  49. default:
  50. break;
  51. }
  52. if (m_highlighter) {
  53. m_highlighter->attach(*this);
  54. m_highlighter->rehighlight(dummy_palette);
  55. }
  56. }
  57. Vector<Syntax::TextDocumentSpan> const& SourceHighlighterClient::spans() const
  58. {
  59. return document().spans();
  60. }
  61. void SourceHighlighterClient::set_span_at_index(size_t index, Syntax::TextDocumentSpan span)
  62. {
  63. document().set_span_at_index(index, span);
  64. }
  65. Vector<Syntax::TextDocumentFoldingRegion>& SourceHighlighterClient::folding_regions()
  66. {
  67. return document().folding_regions();
  68. }
  69. Vector<Syntax::TextDocumentFoldingRegion> const& SourceHighlighterClient::folding_regions() const
  70. {
  71. return document().folding_regions();
  72. }
  73. ByteString SourceHighlighterClient::highlighter_did_request_text() const
  74. {
  75. return document().text();
  76. }
  77. void SourceHighlighterClient::highlighter_did_request_update()
  78. {
  79. // No-op
  80. }
  81. Syntax::Document& SourceHighlighterClient::highlighter_did_request_document()
  82. {
  83. return document();
  84. }
  85. Syntax::TextPosition SourceHighlighterClient::highlighter_did_request_cursor() const
  86. {
  87. return {};
  88. }
  89. void SourceHighlighterClient::highlighter_did_set_spans(Vector<Syntax::TextDocumentSpan> spans)
  90. {
  91. document().set_spans(span_collection_index, move(spans));
  92. }
  93. void SourceHighlighterClient::highlighter_did_set_folding_regions(Vector<Syntax::TextDocumentFoldingRegion> folding_regions)
  94. {
  95. document().set_folding_regions(move(folding_regions));
  96. }
  97. String highlight_source(String const& url, StringView source, Syntax::Language language, HighlightOutputMode mode)
  98. {
  99. SourceHighlighterClient highlighter_client { source, language };
  100. return highlighter_client.to_html_string(url, mode);
  101. }
  102. StringView SourceHighlighterClient::class_for_token(u64 token_type) const
  103. {
  104. auto class_for_css_token = [](u64 token_type) {
  105. switch (static_cast<Web::CSS::Parser::Token::Type>(token_type)) {
  106. case Web::CSS::Parser::Token::Type::Invalid:
  107. case Web::CSS::Parser::Token::Type::BadString:
  108. case Web::CSS::Parser::Token::Type::BadUrl:
  109. return "invalid"sv;
  110. case Web::CSS::Parser::Token::Type::Ident:
  111. return "identifier"sv;
  112. case Web::CSS::Parser::Token::Type::Function:
  113. return "function"sv;
  114. case Web::CSS::Parser::Token::Type::AtKeyword:
  115. return "at-keyword"sv;
  116. case Web::CSS::Parser::Token::Type::Hash:
  117. return "hash"sv;
  118. case Web::CSS::Parser::Token::Type::String:
  119. return "string"sv;
  120. case Web::CSS::Parser::Token::Type::Url:
  121. return "url"sv;
  122. case Web::CSS::Parser::Token::Type::Number:
  123. case Web::CSS::Parser::Token::Type::Dimension:
  124. case Web::CSS::Parser::Token::Type::Percentage:
  125. return "number"sv;
  126. case Web::CSS::Parser::Token::Type::Whitespace:
  127. return "whitespace"sv;
  128. case Web::CSS::Parser::Token::Type::Delim:
  129. case Web::CSS::Parser::Token::Type::Colon:
  130. case Web::CSS::Parser::Token::Type::Semicolon:
  131. case Web::CSS::Parser::Token::Type::Comma:
  132. case Web::CSS::Parser::Token::Type::OpenSquare:
  133. case Web::CSS::Parser::Token::Type::CloseSquare:
  134. case Web::CSS::Parser::Token::Type::OpenParen:
  135. case Web::CSS::Parser::Token::Type::CloseParen:
  136. case Web::CSS::Parser::Token::Type::OpenCurly:
  137. case Web::CSS::Parser::Token::Type::CloseCurly:
  138. return "delimiter"sv;
  139. case Web::CSS::Parser::Token::Type::CDO:
  140. case Web::CSS::Parser::Token::Type::CDC:
  141. return "comment"sv;
  142. case Web::CSS::Parser::Token::Type::EndOfFile:
  143. default:
  144. break;
  145. }
  146. return ""sv;
  147. };
  148. auto class_for_js_token = [](u64 token_type) {
  149. auto category = JS::Token::category(static_cast<JS::TokenType>(token_type));
  150. switch (category) {
  151. case JS::TokenCategory::Invalid:
  152. return "invalid"sv;
  153. case JS::TokenCategory::Number:
  154. return "number"sv;
  155. case JS::TokenCategory::String:
  156. return "string"sv;
  157. case JS::TokenCategory::Punctuation:
  158. return "punctuation"sv;
  159. case JS::TokenCategory::Operator:
  160. return "operator"sv;
  161. case JS::TokenCategory::Keyword:
  162. return "keyword"sv;
  163. case JS::TokenCategory::ControlKeyword:
  164. return "control-keyword"sv;
  165. case JS::TokenCategory::Identifier:
  166. return "identifier"sv;
  167. default:
  168. break;
  169. }
  170. return ""sv;
  171. };
  172. switch (m_highlighter->language()) {
  173. case Syntax::Language::CSS:
  174. return class_for_css_token(token_type);
  175. case Syntax::Language::JavaScript:
  176. return class_for_js_token(token_type);
  177. case Syntax::Language::HTML: {
  178. // HTML has nested CSS and JS highlighters, so we have to decode their token types.
  179. // HTML
  180. if (token_type < Web::HTML::SyntaxHighlighter::JS_TOKEN_START_VALUE) {
  181. switch (static_cast<Web::HTML::AugmentedTokenKind>(token_type)) {
  182. case Web::HTML::AugmentedTokenKind::AttributeName:
  183. return "attribute-name"sv;
  184. case Web::HTML::AugmentedTokenKind::AttributeValue:
  185. return "attribute-value"sv;
  186. case Web::HTML::AugmentedTokenKind::OpenTag:
  187. case Web::HTML::AugmentedTokenKind::CloseTag:
  188. return "tag"sv;
  189. case Web::HTML::AugmentedTokenKind::Comment:
  190. return "comment"sv;
  191. case Web::HTML::AugmentedTokenKind::Doctype:
  192. return "doctype"sv;
  193. case Web::HTML::AugmentedTokenKind::__Count:
  194. default:
  195. return ""sv;
  196. }
  197. }
  198. // JS
  199. if (token_type < Web::HTML::SyntaxHighlighter::CSS_TOKEN_START_VALUE) {
  200. return class_for_js_token(token_type - Web::HTML::SyntaxHighlighter::JS_TOKEN_START_VALUE);
  201. }
  202. // CSS
  203. return class_for_css_token(token_type - Web::HTML::SyntaxHighlighter::CSS_TOKEN_START_VALUE);
  204. }
  205. default:
  206. return "unknown"sv;
  207. }
  208. }
  209. String SourceHighlighterClient::to_html_string(String const& url, HighlightOutputMode mode) const
  210. {
  211. StringBuilder builder;
  212. auto append_escaped = [&](Utf32View text) {
  213. for (auto code_point : text) {
  214. if (code_point == '&') {
  215. builder.append("&amp;"sv);
  216. } else if (code_point == 0xA0) {
  217. builder.append("&nbsp;"sv);
  218. } else if (code_point == '<') {
  219. builder.append("&lt;"sv);
  220. } else if (code_point == '>') {
  221. builder.append("&gt;"sv);
  222. } else {
  223. builder.append_code_point(code_point);
  224. }
  225. }
  226. };
  227. auto start_token = [&](u64 type) {
  228. builder.appendff("<span class=\"{}\">", class_for_token(type));
  229. };
  230. auto end_token = [&]() {
  231. builder.append("</span>"sv);
  232. };
  233. if (mode == HighlightOutputMode::FullDocument) {
  234. builder.append(R"~~~(
  235. <!DOCTYPE html>
  236. <html>
  237. <head>
  238. <meta name="color-scheme" content="dark light">)~~~"sv);
  239. builder.appendff("<title>View Source - {}</title>", escape_html_entities(url));
  240. builder.appendff("<style type=\"text/css\">{}</style>", HTML_HIGHLIGHTER_STYLE);
  241. builder.append(R"~~~(
  242. </head>
  243. <body>)~~~"sv);
  244. }
  245. builder.append("<pre class=\"html\">"sv);
  246. size_t span_index = 0;
  247. for (size_t line_index = 0; line_index < document().line_count(); ++line_index) {
  248. auto& line = document().line(line_index);
  249. auto line_view = line.view();
  250. builder.append("<div class=\"line\">"sv);
  251. size_t next_column = 0;
  252. auto draw_text_helper = [&](size_t start, size_t end, Optional<Syntax::TextDocumentSpan const&> span) {
  253. size_t length = end - start;
  254. if (length == 0)
  255. return;
  256. auto text = line_view.substring_view(start, length);
  257. if (span.has_value()) {
  258. start_token(span->data);
  259. append_escaped(text);
  260. end_token();
  261. } else {
  262. append_escaped(text);
  263. }
  264. };
  265. while (span_index < document().spans().size()) {
  266. auto& span = document().spans()[span_index];
  267. if (span.range.start().line() > line_index) {
  268. // No more spans in this line, moving on
  269. break;
  270. }
  271. size_t span_start;
  272. if (span.range.start().line() < line_index) {
  273. span_start = 0;
  274. } else {
  275. span_start = span.range.start().column();
  276. }
  277. size_t span_end;
  278. bool span_consumed;
  279. if (span.range.end().line() > line_index) {
  280. span_end = line.length();
  281. span_consumed = false;
  282. } else {
  283. span_end = span.range.end().column();
  284. span_consumed = true;
  285. }
  286. if (span_start != next_column) {
  287. // Draw unspanned text between spans
  288. draw_text_helper(next_column, span_start, {});
  289. }
  290. draw_text_helper(span_start, span_end, span);
  291. next_column = span_end;
  292. if (!span_consumed) {
  293. // Continue with same span on next line
  294. break;
  295. } else {
  296. ++span_index;
  297. }
  298. }
  299. // Draw unspanned text after last span
  300. if (next_column < line.length()) {
  301. draw_text_helper(next_column, line.length(), {});
  302. }
  303. builder.append("</div>"sv);
  304. }
  305. builder.append("</pre>"sv);
  306. if (mode == HighlightOutputMode::FullDocument) {
  307. builder.append(R"~~~(
  308. </body>
  309. </html>
  310. )~~~"sv);
  311. }
  312. return builder.to_string_without_validation();
  313. }
  314. }