Serenity Operating System
at master 202 lines 5.3 kB view raw
1/* 2 * Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org> 3 * Copyright (c) 2022, Peter Elliott <pelliott@serenityos.org> 4 * 5 * SPDX-License-Identifier: BSD-2-Clause 6 */ 7 8#include <AK/Forward.h> 9#include <AK/StringBuilder.h> 10#include <LibJS/MarkupGenerator.h> 11#include <LibMarkdown/CodeBlock.h> 12#include <LibMarkdown/Visitor.h> 13#include <LibRegex/Regex.h> 14 15namespace Markdown { 16 17DeprecatedString CodeBlock::render_to_html(bool) const 18{ 19 StringBuilder builder; 20 21 builder.append("<pre>"sv); 22 23 if (m_style.length() >= 2) 24 builder.append("<strong>"sv); 25 else if (m_style.length() >= 2) 26 builder.append("<em>"sv); 27 28 if (m_language.is_empty()) 29 builder.append("<code>"sv); 30 else 31 builder.appendff("<code class=\"language-{}\">", escape_html_entities(m_language)); 32 33 if (m_language == "js") { 34 auto html_or_error = JS::MarkupGenerator::html_from_source(m_code); 35 if (html_or_error.is_error()) { 36 warnln("Could not render js code to html: {}", html_or_error.error()); 37 builder.append(escape_html_entities(m_code)); 38 } else { 39 builder.append(html_or_error.release_value()); 40 } 41 } else { 42 builder.append(escape_html_entities(m_code)); 43 } 44 45 builder.append("</code>"sv); 46 47 if (m_style.length() >= 2) 48 builder.append("</strong>"sv); 49 else if (m_style.length() >= 2) 50 builder.append("</em>"sv); 51 52 builder.append("</pre>\n"sv); 53 54 return builder.to_deprecated_string(); 55} 56 57Vector<DeprecatedString> CodeBlock::render_lines_for_terminal(size_t) const 58{ 59 Vector<DeprecatedString> lines; 60 61 // Do not indent too much if we are in the synopsis 62 auto indentation = " "sv; 63 if (m_current_section != nullptr) { 64 auto current_section_name = m_current_section->render_lines_for_terminal()[0]; 65 if (current_section_name.contains("SYNOPSIS"sv)) 66 indentation = " "sv; 67 } 68 69 for (auto const& line : m_code.split('\n')) 70 lines.append(DeprecatedString::formatted("{}{}", indentation, line)); 71 lines.append(""); 72 73 return lines; 74} 75 76RecursionDecision CodeBlock::walk(Visitor& visitor) const 77{ 78 RecursionDecision rd = visitor.visit(*this); 79 if (rd != RecursionDecision::Recurse) 80 return rd; 81 82 rd = visitor.visit(m_code); 83 if (rd != RecursionDecision::Recurse) 84 return rd; 85 86 // Don't recurse on m_language and m_style. 87 88 // Normalize return value. 89 return RecursionDecision::Continue; 90} 91 92static Regex<ECMA262> open_fence_re("^ {0,3}(([\\`\\~])\\2{2,})\\s*([\\*_]*)\\s*([^\\*_\\s]*).*$"); 93static Regex<ECMA262> close_fence_re("^ {0,3}(([\\`\\~])\\2{2,})\\s*$"); 94 95static Optional<int> line_block_prefix(StringView const& line) 96{ 97 int characters = 0; 98 int indents = 0; 99 100 for (char ch : line) { 101 if (indents == 4) 102 break; 103 104 if (ch == ' ') { 105 ++characters; 106 ++indents; 107 } else if (ch == '\t') { 108 ++characters; 109 indents = 4; 110 } else { 111 break; 112 } 113 } 114 115 if (indents == 4) 116 return characters; 117 118 return {}; 119} 120 121OwnPtr<CodeBlock> CodeBlock::parse(LineIterator& lines, Heading* current_section) 122{ 123 if (lines.is_end()) 124 return {}; 125 126 StringView line = *lines; 127 if (open_fence_re.match(line).success) 128 return parse_backticks(lines, current_section); 129 130 if (line_block_prefix(line).has_value()) 131 return parse_indent(lines); 132 133 return {}; 134} 135 136OwnPtr<CodeBlock> CodeBlock::parse_backticks(LineIterator& lines, Heading* current_section) 137{ 138 StringView line = *lines; 139 140 // Our Markdown extension: we allow 141 // specifying a style and a language 142 // for a code block, like so: 143 // 144 // ```**sh** 145 // $ echo hello friends! 146 // ```` 147 // 148 // The code block will be made bold, 149 // and if possible syntax-highlighted 150 // as appropriate for a shell script. 151 152 auto matches = open_fence_re.match(line).capture_group_matches[0]; 153 auto fence = matches[0].view.string_view(); 154 auto style = matches[2].view.string_view(); 155 auto language = matches[3].view.string_view(); 156 157 ++lines; 158 159 StringBuilder builder; 160 161 while (true) { 162 if (lines.is_end()) 163 break; 164 line = *lines; 165 ++lines; 166 167 auto close_match = close_fence_re.match(line); 168 if (close_match.success) { 169 auto close_fence = close_match.capture_group_matches[0][0].view.string_view(); 170 if (close_fence[0] == fence[0] && close_fence.length() >= fence.length()) 171 break; 172 } 173 builder.append(line); 174 builder.append('\n'); 175 } 176 177 return make<CodeBlock>(language, style, builder.to_deprecated_string(), current_section); 178} 179 180OwnPtr<CodeBlock> CodeBlock::parse_indent(LineIterator& lines) 181{ 182 StringBuilder builder; 183 184 while (true) { 185 if (lines.is_end()) 186 break; 187 StringView line = *lines; 188 189 auto prefix_length = line_block_prefix(line); 190 if (!prefix_length.has_value()) 191 break; 192 193 line = line.substring_view(prefix_length.value()); 194 ++lines; 195 196 builder.append(line); 197 builder.append('\n'); 198 } 199 200 return make<CodeBlock>("", "", builder.to_deprecated_string(), nullptr); 201} 202}