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