/src/serenity/Userland/Libraries/LibMarkdown/Table.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2020, the SerenityOS developers. |
3 | | * |
4 | | * SPDX-License-Identifier: BSD-2-Clause |
5 | | */ |
6 | | |
7 | | #include <AK/Debug.h> |
8 | | #include <AK/StringBuilder.h> |
9 | | #include <AK/Vector.h> |
10 | | #include <LibMarkdown/Table.h> |
11 | | #include <LibMarkdown/Visitor.h> |
12 | | |
13 | | namespace Markdown { |
14 | | |
15 | | Vector<ByteString> Table::render_lines_for_terminal(size_t view_width) const |
16 | 0 | { |
17 | 0 | auto unit_width_length = view_width == 0 ? 4 : ((float)(view_width - m_columns.size()) / (float)m_total_width); |
18 | 0 | StringBuilder builder; |
19 | 0 | Vector<ByteString> lines; |
20 | |
|
21 | 0 | auto write_aligned = [&](auto const& text, auto width, auto alignment) { |
22 | 0 | size_t original_length = text.terminal_length(); |
23 | 0 | auto string = text.render_for_terminal(); |
24 | 0 | if (alignment == Alignment::Center) { |
25 | 0 | auto padding_length = (width - original_length) / 2; |
26 | | // FIXME: We're using a StringView literal to bypass the compile-time AK::Format checking here, since it can't handle the "}}" |
27 | 0 | builder.appendff("{:{1}}"sv, "", (int)padding_length); |
28 | 0 | builder.append(string); |
29 | 0 | builder.appendff("{:{1}}"sv, "", (int)padding_length); |
30 | 0 | if ((width - original_length) % 2) |
31 | 0 | builder.append(' '); |
32 | 0 | } else { |
33 | | // FIXME: We're using StringView literals to bypass the compile-time AK::Format checking here, since it can't handle the "}}" |
34 | 0 | builder.appendff(alignment == Alignment::Left ? "{:<{1}}"sv : "{:>{1}}"sv, string, (int)(width + (string.length() - original_length))); |
35 | 0 | } |
36 | 0 | }; |
37 | |
|
38 | 0 | bool first = true; |
39 | 0 | for (auto& col : m_columns) { |
40 | 0 | if (!first) |
41 | 0 | builder.append('|'); |
42 | 0 | first = false; |
43 | 0 | size_t width = col.relative_width * unit_width_length; |
44 | 0 | write_aligned(col.header, width, col.alignment); |
45 | 0 | } |
46 | |
|
47 | 0 | lines.append(builder.to_byte_string()); |
48 | 0 | builder.clear(); |
49 | |
|
50 | 0 | for (size_t i = 0; i < view_width; ++i) |
51 | 0 | builder.append('-'); |
52 | 0 | lines.append(builder.to_byte_string()); |
53 | 0 | builder.clear(); |
54 | |
|
55 | 0 | for (size_t i = 0; i < m_row_count; ++i) { |
56 | 0 | bool first = true; |
57 | 0 | for (auto& col : m_columns) { |
58 | 0 | VERIFY(i < col.rows.size()); |
59 | 0 | auto& cell = col.rows[i]; |
60 | |
|
61 | 0 | if (!first) |
62 | 0 | builder.append('|'); |
63 | 0 | first = false; |
64 | |
|
65 | 0 | size_t width = col.relative_width * unit_width_length; |
66 | 0 | write_aligned(cell, width, col.alignment); |
67 | 0 | } |
68 | 0 | lines.append(builder.to_byte_string()); |
69 | 0 | builder.clear(); |
70 | 0 | } |
71 | | |
72 | 0 | lines.append(""); |
73 | |
|
74 | 0 | return lines; |
75 | 0 | } |
76 | | |
77 | | ByteString Table::render_to_html(bool) const |
78 | 0 | { |
79 | 0 | auto alignment_string = [](Alignment alignment) { |
80 | 0 | switch (alignment) { |
81 | 0 | case Alignment::Center: |
82 | 0 | return "center"sv; |
83 | 0 | case Alignment::Left: |
84 | 0 | return "left"sv; |
85 | 0 | case Alignment::Right: |
86 | 0 | return "right"sv; |
87 | 0 | } |
88 | 0 | VERIFY_NOT_REACHED(); |
89 | 0 | }; |
90 | |
|
91 | 0 | StringBuilder builder; |
92 | |
|
93 | 0 | builder.append("<table>"sv); |
94 | 0 | builder.append("<thead>"sv); |
95 | 0 | builder.append("<tr>"sv); |
96 | 0 | for (auto& column : m_columns) { |
97 | 0 | builder.appendff("<th style='text-align: {}'>", alignment_string(column.alignment)); |
98 | 0 | builder.append(column.header.render_to_html()); |
99 | 0 | builder.append("</th>"sv); |
100 | 0 | } |
101 | 0 | builder.append("</tr>"sv); |
102 | 0 | builder.append("</thead>"sv); |
103 | 0 | builder.append("<tbody>"sv); |
104 | 0 | for (size_t i = 0; i < m_row_count; ++i) { |
105 | 0 | builder.append("<tr>"sv); |
106 | 0 | for (auto& column : m_columns) { |
107 | 0 | VERIFY(i < column.rows.size()); |
108 | 0 | builder.appendff("<td style='text-align: {}'>", alignment_string(column.alignment)); |
109 | 0 | builder.append(column.rows[i].render_to_html()); |
110 | 0 | builder.append("</td>"sv); |
111 | 0 | } |
112 | 0 | builder.append("</tr>"sv); |
113 | 0 | } |
114 | 0 | builder.append("</tbody>"sv); |
115 | 0 | builder.append("</table>"sv); |
116 | |
|
117 | 0 | return builder.to_byte_string(); |
118 | 0 | } |
119 | | |
120 | | RecursionDecision Table::walk(Visitor& visitor) const |
121 | 0 | { |
122 | 0 | RecursionDecision rd = visitor.visit(*this); |
123 | 0 | if (rd != RecursionDecision::Recurse) |
124 | 0 | return rd; |
125 | | |
126 | 0 | for (auto const& column : m_columns) { |
127 | 0 | rd = column.walk(visitor); |
128 | 0 | if (rd == RecursionDecision::Break) |
129 | 0 | return rd; |
130 | 0 | } |
131 | | |
132 | 0 | return RecursionDecision::Continue; |
133 | 0 | } |
134 | | |
135 | | OwnPtr<Table> Table::parse(LineIterator& lines) |
136 | 17.1M | { |
137 | 17.1M | auto peek_it = lines; |
138 | 17.1M | auto first_line = *peek_it; |
139 | 17.1M | if (!first_line.starts_with('|')) |
140 | 16.6M | return {}; |
141 | | |
142 | 499k | ++peek_it; |
143 | | |
144 | 499k | if (peek_it.is_end()) |
145 | 16.4k | return {}; |
146 | | |
147 | 483k | auto header_segments = first_line.split_view('|', SplitBehavior::KeepEmpty); |
148 | 483k | auto header_delimiters = peek_it->split_view('|', SplitBehavior::KeepEmpty); |
149 | | |
150 | 483k | if (!header_segments.is_empty()) |
151 | 483k | header_segments.take_first(); |
152 | 483k | if (!header_segments.is_empty() && header_segments.last().is_empty()) |
153 | 452k | header_segments.take_last(); |
154 | | |
155 | 483k | if (!header_delimiters.is_empty()) |
156 | 475k | header_delimiters.take_first(); |
157 | 483k | if (!header_delimiters.is_empty() && header_delimiters.last().is_empty()) |
158 | 362k | header_delimiters.take_last(); |
159 | | |
160 | 483k | ++peek_it; |
161 | | |
162 | 483k | if (header_delimiters.size() != header_segments.size()) |
163 | 28.8k | return {}; |
164 | | |
165 | 454k | if (header_delimiters.is_empty()) |
166 | 434k | return {}; |
167 | | |
168 | 20.1k | size_t total_width = 0; |
169 | | |
170 | 20.1k | auto table = make<Table>(); |
171 | 20.1k | table->m_columns.resize(header_delimiters.size()); |
172 | | |
173 | 691k | for (size_t i = 0; i < header_segments.size(); ++i) { |
174 | 670k | auto text = Text::parse(header_segments[i]); |
175 | | |
176 | 670k | auto& column = table->m_columns[i]; |
177 | | |
178 | 670k | column.header = move(text); |
179 | | |
180 | 670k | auto delimiter = header_delimiters[i].trim_whitespace(); |
181 | | |
182 | 670k | auto align_left = delimiter.starts_with(':'); |
183 | 670k | auto align_right = delimiter != ":" && delimiter.ends_with(':'); |
184 | | |
185 | 670k | if (align_left) |
186 | 2.41k | delimiter = delimiter.substring_view(1, delimiter.length() - 1); |
187 | 670k | if (align_right) |
188 | 484 | delimiter = delimiter.substring_view(0, delimiter.length() - 1); |
189 | | |
190 | 670k | if (align_left && align_right) |
191 | 219 | column.alignment = Alignment::Center; |
192 | 670k | else if (align_right) |
193 | 265 | column.alignment = Alignment::Right; |
194 | 670k | else |
195 | 670k | column.alignment = Alignment::Left; |
196 | | |
197 | 670k | size_t relative_width = delimiter.length(); |
198 | 1.32M | for (auto ch : delimiter) { |
199 | 1.32M | if (ch != '-') { |
200 | 1.32M | dbgln_if(MARKDOWN_DEBUG, "Invalid character _{}_ in table heading delimiter (ignored)", ch); |
201 | 1.32M | --relative_width; |
202 | 1.32M | } |
203 | 1.32M | } |
204 | | |
205 | 670k | column.relative_width = relative_width; |
206 | 670k | total_width += relative_width; |
207 | 670k | } |
208 | | |
209 | 20.1k | table->m_total_width = total_width; |
210 | | |
211 | 40.3k | for (off_t i = 0; i < peek_it - lines; ++i) |
212 | 20.1k | ++lines; |
213 | | |
214 | 20.1k | size_t row_count = 0; |
215 | 20.1k | ++lines; |
216 | 327k | while (!lines.is_end()) { |
217 | 325k | auto line = *lines; |
218 | 325k | if (!line.starts_with('|')) |
219 | 18.4k | break; |
220 | | |
221 | 307k | ++lines; |
222 | | |
223 | 307k | auto segments = line.split_view('|', SplitBehavior::KeepEmpty); |
224 | 307k | segments.take_first(); |
225 | 307k | if (!segments.is_empty() && segments.last().is_empty()) |
226 | 258k | segments.take_last(); |
227 | 307k | ++row_count; |
228 | | |
229 | 24.4M | for (size_t i = 0; i < header_segments.size(); ++i) { |
230 | 24.1M | if (i >= segments.size()) { |
231 | | // Ran out of segments, but still have headers. |
232 | | // Just make an empty cell. |
233 | 23.8M | table->m_columns[i].rows.append(Text::parse(""sv)); |
234 | 23.8M | } else { |
235 | 240k | auto text = Text::parse(segments[i]); |
236 | 240k | table->m_columns[i].rows.append(move(text)); |
237 | 240k | } |
238 | 24.1M | } |
239 | 307k | } |
240 | | |
241 | 20.1k | table->m_row_count = row_count; |
242 | | |
243 | 20.1k | return table; |
244 | 454k | } |
245 | | |
246 | | RecursionDecision Table::Column::walk(Visitor& visitor) const |
247 | 0 | { |
248 | 0 | RecursionDecision rd = visitor.visit(*this); |
249 | 0 | if (rd != RecursionDecision::Recurse) |
250 | 0 | return rd; |
251 | | |
252 | 0 | rd = header.walk(visitor); |
253 | 0 | if (rd != RecursionDecision::Recurse) |
254 | 0 | return rd; |
255 | | |
256 | 0 | for (auto const& row : rows) { |
257 | 0 | rd = row.walk(visitor); |
258 | 0 | if (rd == RecursionDecision::Break) |
259 | 0 | return rd; |
260 | 0 | } |
261 | | |
262 | 0 | return RecursionDecision::Continue; |
263 | 0 | } |
264 | | |
265 | | } |