SourceHighlighter.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  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(URL::URL const& url, StringView source)
  98. {
  99. SourceHighlighterClient highlighter_client { source, Syntax::Language::HTML };
  100. return highlighter_client.to_html_string(url);
  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. static String generate_style()
  210. {
  211. StringBuilder builder;
  212. builder.append(HTML_HIGHLIGHTER_STYLE);
  213. builder.append(R"~~~(
  214. .html {
  215. counter-reset: line;
  216. }
  217. .line {
  218. counter-increment: line;
  219. white-space: pre;
  220. }
  221. .line::before {
  222. content: counter(line) " ";
  223. display: inline-block;
  224. width: 2.5em;
  225. padding-right: 0.5em;
  226. text-align: right;
  227. }
  228. @media (prefers-color-scheme: dark) {
  229. .line::before {
  230. color: darkgrey;
  231. }
  232. }
  233. @media (prefers-color-scheme: light) {
  234. .line::before {
  235. color: dimgray;
  236. }
  237. }
  238. )~~~"sv);
  239. return MUST(builder.to_string());
  240. }
  241. String SourceHighlighterClient::to_html_string(URL::URL const& url) const
  242. {
  243. StringBuilder builder;
  244. auto append_escaped = [&](Utf32View text) {
  245. for (auto code_point : text) {
  246. if (code_point == '&') {
  247. builder.append("&amp;"sv);
  248. } else if (code_point == 0xA0) {
  249. builder.append("&nbsp;"sv);
  250. } else if (code_point == '<') {
  251. builder.append("&lt;"sv);
  252. } else if (code_point == '>') {
  253. builder.append("&gt;"sv);
  254. } else {
  255. builder.append_code_point(code_point);
  256. }
  257. }
  258. };
  259. auto start_token = [&](u64 type) {
  260. builder.appendff("<span class=\"{}\">", class_for_token(type));
  261. };
  262. auto end_token = [&]() {
  263. builder.append("</span>"sv);
  264. };
  265. builder.append(R"~~~(
  266. <!DOCTYPE html>
  267. <html>
  268. <head>
  269. <meta name="color-scheme" content="dark light">)~~~"sv);
  270. builder.appendff("<title>View Source - {}</title>", escape_html_entities(MUST(url.to_string())));
  271. builder.appendff("<style type=\"text/css\">{}</style>", generate_style());
  272. builder.append(R"~~~(
  273. </head>
  274. <body>
  275. <pre class="html">)~~~"sv);
  276. size_t span_index = 0;
  277. for (size_t line_index = 0; line_index < document().line_count(); ++line_index) {
  278. auto& line = document().line(line_index);
  279. auto line_view = line.view();
  280. builder.append("<div class=\"line\">"sv);
  281. size_t next_column = 0;
  282. auto draw_text_helper = [&](size_t start, size_t end, Optional<Syntax::TextDocumentSpan const&> span) {
  283. size_t length = end - start;
  284. if (length == 0)
  285. return;
  286. auto text = line_view.substring_view(start, length);
  287. if (span.has_value()) {
  288. start_token(span->data);
  289. append_escaped(text);
  290. end_token();
  291. } else {
  292. append_escaped(text);
  293. }
  294. };
  295. while (span_index < document().spans().size()) {
  296. auto& span = document().spans()[span_index];
  297. if (span.range.start().line() > line_index) {
  298. // No more spans in this line, moving on
  299. break;
  300. }
  301. size_t span_start;
  302. if (span.range.start().line() < line_index) {
  303. span_start = 0;
  304. } else {
  305. span_start = span.range.start().column();
  306. }
  307. size_t span_end;
  308. bool span_consumed;
  309. if (span.range.end().line() > line_index) {
  310. span_end = line.length();
  311. span_consumed = false;
  312. } else {
  313. span_end = span.range.end().column();
  314. span_consumed = true;
  315. }
  316. if (span_start != next_column) {
  317. // Draw unspanned text between spans
  318. draw_text_helper(next_column, span_start, {});
  319. }
  320. draw_text_helper(span_start, span_end, span);
  321. next_column = span_end;
  322. if (!span_consumed) {
  323. // Continue with same span on next line
  324. break;
  325. } else {
  326. ++span_index;
  327. }
  328. }
  329. // Draw unspanned text after last span
  330. if (next_column < line.length()) {
  331. draw_text_helper(next_column, line.length(), {});
  332. }
  333. builder.append("</div>"sv);
  334. }
  335. builder.append(R"~~~(
  336. </pre>
  337. </body>
  338. </html>
  339. )~~~"sv);
  340. return builder.to_string_without_validation();
  341. }
  342. }