Files
bantam-cpp/render.cpp
T

500 lines
19 KiB
C++
Raw Normal View History

2026-10-14 00:01:03 +03:00
/* render.cpp — terminal markdown rendering with box-drawing tables. */
#include "bantam.hpp"
#include <array>
#include <cctype>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <span>
#include <vector>
/* ----------------------------- table helpers -------------------------- */
bool is_table_line(std::string_view line) {
std::string_view t = line;
while (!t.empty() && (t.front() == ' ' || t.front() == '\t')) t.remove_prefix(1);
if (t.find('|') == std::string_view::npos) return false;
return (t.front() == '|') || (t.back() == '|');
}
static std::string_view trim_spaces(std::string_view s) {
while (!s.empty() && (s.front() == ' ' || s.front() == '\t')) s.remove_prefix(1);
return s;
}
static bool is_table_sep(const std::vector<std::string> &cells) {
if (cells.empty()) return false;
for (auto &c : cells) {
std::string_view cv = trim_spaces(c);
bool ok = true;
for (char ch : cv) if (ch != '-' && ch != ':') { ok = false; break; }
if (!ok) return false;
}
return true;
}
2026-10-14 14:15:14 +03:00
/* A run of lines only counts as a table when it contains a real delimiter
* row (a separator made of '-'/':' columns) and at least one data row. Without
* this, ordinary lists (or prose that merely contains '|') whose items happen
* to start or end with '|' would be mis-rendered as box-drawing tables. */
static std::vector<std::string> parse_table_cells(std::string_view line);
static bool looks_like_table(const std::vector<std::string> &lines) {
if (lines.size() < 2) return false;
bool has_sep = false, has_row = false;
for (auto &line : lines) {
std::vector<std::string> cells = parse_table_cells(line);
if (is_table_sep(cells)) has_sep = true;
else has_row = true;
}
return has_sep && has_row;
}
2026-10-14 00:01:03 +03:00
static std::vector<std::string> parse_table_cells(std::string_view line) {
std::string_view t = line;
while (!t.empty() && (t.front() == ' ' || t.front() == '\t')) t.remove_prefix(1);
std::string dup(t);
while (!dup.empty() && (dup.back() == '\n' || dup.back() == '\r')) dup.pop_back();
if (!dup.empty() && dup.front() == '|') dup.erase(0, 1);
if (!dup.empty() && dup.back() == '|') dup.pop_back();
size_t pipes = 0;
for (char c : dup) if (c == '|') pipes++;
std::vector<std::string> cells;
cells.reserve(pipes + 1);
std::string cur;
size_t i = 0;
while (i <= dup.size()) {
if (i == dup.size() || dup[i] == '|') {
cells.push_back(std::string(trim_spaces(cur)));
cur.clear();
} else {
cur += dup[i];
}
i++;
}
return cells;
}
/* ----------------------------- inline --------------------------------- */
static bool is_word_byte(unsigned char c) {
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') || c == '_';
}
2026-10-14 07:10:03 +00:00
template<class R>
2026-10-14 00:01:03 +03:00
static void wrap_pairs(std::string &dst, std::string_view src, std::string_view open,
2026-10-14 07:10:03 +00:00
std::string_view close, const R& codes,
2026-10-14 00:01:03 +03:00
bool boundary = false) {
size_t olen = open.size(), clen = close.size();
size_t p = 0;
while (p < src.size()) {
size_t o = src.find(open, p);
if (o == std::string_view::npos) { dst += src.substr(p); break; }
/* When word boundaries are enforced (the _ / __ rules) an opening
* delimiter glued to a preceding word character is not an opener. */
if (boundary && o > 0 && is_word_byte((unsigned char)src[o - 1])) {
dst += src.substr(p, o - p + olen);
p = o + olen;
continue;
}
dst += src.substr(p, o - p);
size_t c = o + olen;
while (true) {
c = src.find(close, c);
if (c == std::string_view::npos) { dst += src.substr(o); break; }
/* A closing delimiter glued to a following word character is not a
* closer (e.g. the underscores in file_name_here); keep scanning. */
if (boundary && c + clen < src.size() &&
is_word_byte((unsigned char)src[c + clen])) {
c += clen;
continue;
}
break;
}
if (c == std::string_view::npos) break; /* substr(o) already appended */
std::string_view inner = src.substr(o + olen, c - (o + olen));
dst += cstr(inner, codes);
p = c + clen;
}
}
static void apply_links(std::string &dst, std::string_view src) {
size_t p = 0;
while (p < src.size()) {
size_t lb = src.find('[', p);
if (lb == std::string_view::npos) { dst += src.substr(p); break; }
size_t rb = src.find(']', lb);
if (rb == std::string_view::npos || rb + 1 >= src.size() || src[rb + 1] != '(') {
dst += src.substr(p, lb - p + 1); p = lb + 1; continue;
}
size_t rp = src.find(')', rb + 2);
if (rp == std::string_view::npos) { dst += src.substr(p); break; }
std::string_view text = src.substr(lb + 1, rb - (lb + 1));
std::string_view url = src.substr(rb + 2, rp - (rb + 2));
dst += src.substr(p, lb - p);
std::string wt = cstr(text, std::array{4, 36});
dst += wt;
std::string wu = cstr(url, std::array{2});
dst += "(" + wu + ")";
p = rp + 1;
}
}
static std::string render_inline(std::string_view s) {
if (!g_col) return std::string(s);
if (s.empty()) return "";
std::vector<std::string> codes;
std::string step1;
size_t p = 0;
while (p < s.size()) {
if (s[p] == '`') {
size_t start = p + 1;
size_t end = s.find('`', start);
if (end != std::string_view::npos && end > start) {
std::string_view ctext = s.substr(start, end - start);
std::string ct = cstr(ctext, std::array{33});
codes.push_back(ct);
str_appendf(step1, "\x01%zu\x01", codes.size() - 1);
p = end + 1;
continue;
}
step1 += s[p]; p++;
} else {
step1 += s[p]; p++;
}
}
std::string s2; apply_links(s2, step1);
std::string s3; wrap_pairs(s3, s2, "***", "***", std::array{1, 3});
std::string s4; wrap_pairs(s4, s3, "___", "___", std::array{1, 3});
std::string s5; wrap_pairs(s5, s4, "**", "**", std::array{1});
std::string s6; wrap_pairs(s6, s5, "__", "__", std::array{1}, true);
std::string s7; wrap_pairs(s7, s6, "*", "*", std::array{3});
std::string s8; wrap_pairs(s8, s7, "_", "_", std::array{3}, true);
std::string s9; wrap_pairs(s9, s8, "~~", "~~", std::array{9});
std::string out;
size_t q = 0;
while (q < s9.size()) {
if (s9[q] == '\x01') {
size_t e = s9.find('\x01', q + 1);
if (e != std::string::npos) {
int idx = to_int_val(std::string_view(s9).substr(q + 1, e - (q + 1)), -1);
if (idx >= 0 && idx < (int)codes.size()) out += codes[idx];
q = e + 1; continue;
}
}
out += s9[q]; q++;
}
return out;
}
/* ----------------------------- line ----------------------------------- */
static int indent_spaces(std::string_view line) {
int n = 0;
while (n < (int)line.size() && line[n] == ' ') n++;
return n;
}
void render_md_line_print(std::string_view line, MdState &st) {
if (!g_col) { printf("%s\n", std::string(line).c_str()); return; }
std::string_view t = trim_spaces(line);
if (t.size() >= 3 && t.substr(0, 3) == "```") {
if (!st.in_code) {
st.in_code = true;
std::string_view lang = t.substr(3);
while (!lang.empty() && lang.front() == ' ') lang.remove_prefix(1);
std::string title;
if (!lang.empty()) title = sfmt(" [ %s ]", std::string(lang).c_str());
std::string s = "───" + title;
for (int i = 0; i < 40; i++) s += "─";
std::string h = cstr(s, std::array{2});
printf("%s\n", h.c_str());
} else {
st.in_code = false;
std::string s;
for (int i = 0; i < 50; i++) s += "─";
std::string h = cstr(s, std::array{2});
printf("%s\n", h.c_str());
}
return;
}
if (st.in_code) {
std::string pre = cstr(" ", std::array{2});
std::string body = cstr(line, std::array{32});
printf("%s%s\n", pre.c_str(), body.c_str());
return;
}
if (t == "---" || t == "***" || t == "___" || t == "----" || t == "------") {
std::string s;
for (int i = 0; i < 50; i++) s += "─";
std::string h = cstr(s, std::array{2});
printf("%s\n", h.c_str());
return;
}
if (!t.empty() && t[0] == '#') {
int lvl = 0;
while (lvl < (int)t.size() && t[lvl] == '#') lvl++;
if (lvl < (int)t.size() && t[lvl] == ' ') {
std::string_view htext = trim_spaces(t.substr(lvl));
if (lvl == 1) { std::string a = cstr("■ ", std::array{35}); std::string b = cstr(htext, std::array{1, 36}); printf("%s%s\n", a.c_str(), b.c_str()); }
else if (lvl == 2) { std::string a = cstr("▲ ", std::array{35}); std::string b = cstr(htext, std::array{1, 36}); printf("%s%s\n", a.c_str(), b.c_str()); }
else if (lvl == 3) { std::string a = cstr("● ", std::array{35}); std::string b = cstr(htext, std::array{1, 32}); printf("%s%s\n", a.c_str(), b.c_str()); }
else if (lvl == 4) { std::string a = cstr("◆ ", std::array{35}); std::string b = cstr(htext, std::array{1, 33}); printf("%s%s\n", a.c_str(), b.c_str()); }
else { std::string b = cstr(htext, std::array{1}); printf("%s\n", b.c_str()); }
return;
}
}
if (!t.empty() && t[0] == '>') {
std::string_view q = trim_spaces(t.substr(1));
std::string bar = cstr("▎ ", std::array{34});
std::string body = render_inline(q);
std::string cbody = cstr(body, std::array{3});
printf("%s%s\n", bar.c_str(), cbody.c_str());
return;
}
if (t.size() >= 6 && (t.substr(0, 6) == "- [ ] " || t.substr(0, 6) == "* [ ] ")) {
int ind = indent_spaces(line);
std::string box = cstr("☐ ", std::array{33});
std::string rest = render_inline(t.substr(6));
printf("%*s%s%s\n", ind, "", box.c_str(), rest.c_str());
return;
}
if (t.size() >= 6 && (t.substr(0, 6) == "- [x] " || t.substr(0, 6) == "* [x] " ||
t.substr(0, 6) == "- [X] " || t.substr(0, 6) == "* [X] ")) {
int ind = indent_spaces(line);
std::string box = cstr("☑ ", std::array{32});
std::string rest = render_inline(t.substr(6));
printf("%*s%s%s\n", ind, "", box.c_str(), rest.c_str());
return;
}
if (t.size() >= 2 && (t.substr(0, 2) == "- " || t.substr(0, 2) == "* " || t.substr(0, 2) == "+ ")) {
int ind = indent_spaces(line);
std::string bullet = cstr("• ", std::array{36});
std::string rest = render_inline(t.substr(2));
printf("%*s%s%s\n", ind, "", bullet.c_str(), rest.c_str());
return;
}
std::string_view num = t;
while (!num.empty() && std::isdigit((unsigned char)num.front())) num.remove_prefix(1);
if (!num.empty() && num.front() == '.' && num.size() > 1 && num[1] == ' ') {
int ind = indent_spaces(line);
size_t nlen = num.data() - t.data();
std::string_view nstr = t.substr(0, nlen);
std::string bullet = cstr(nstr, std::array{33});
std::string rest = render_inline(t.substr(nlen + 2));
printf("%*s%s %s\n", ind, "", bullet.c_str(), rest.c_str());
return;
}
std::string r = render_inline(line);
printf("%s\n", r.c_str());
}
/* ----------------------------- table ---------------------------------- */
static std::vector<std::string> wrap_cell(std::string_view s, int width) {
std::vector<std::string> lines;
if (width <= 0) { lines.push_back(""); return lines; }
std::string copy(s);
std::vector<std::string> blocks;
std::string cur;
size_t p = 0;
while (p < copy.size()) {
if (copy.compare(p, 4, "<br>") == 0) { blocks.push_back(cur); cur.clear(); p += 4; continue; }
if (copy.compare(p, 5, "<br/>") == 0) { blocks.push_back(cur); cur.clear(); p += 5; continue; }
if (copy.compare(p, 6, "<br />") == 0) { blocks.push_back(cur); cur.clear(); p += 6; continue; }
cur += copy[p]; p++;
}
if (!cur.empty() || blocks.empty()) blocks.push_back(cur);
std::vector<std::string> wrapped;
for (auto &cl : blocks) {
std::vector<std::string> words;
size_t wp = 0;
while (wp < cl.size()) {
while (wp < cl.size() && cl[wp] == ' ') wp++;
if (wp >= cl.size()) break;
size_t ws = wp;
while (wp < cl.size() && cl[wp] != ' ') wp++;
words.push_back(cl.substr(ws, wp - ws));
}
std::string curline;
for (auto &word : words) {
int wlen = visible_len(word);
if (wlen > width) {
std::string w = word;
size_t wi = 0;
while (wi < w.size()) {
if (!curline.empty()) { wrapped.push_back(curline); curline.clear(); }
std::string chunk;
int clen = 0;
size_t ci = wi;
while (ci < w.size() && clen < width) {
if (w[ci] == 0x1b) {
size_t j = ci + 1;
if (j < w.size() && w[j] == '[') {
j++;
while (j < w.size() && !(w[j] >= 0x40 && w[j] <= 0x7e)) j++;
if (j < w.size()) j++;
}
chunk += w.substr(ci, j - ci); ci = j;
continue;
}
size_t pl; int cp = utf8_decode((const unsigned char *)(w.data() + ci), w.size() - ci, &pl);
chunk += w.substr(ci, pl); ci += pl; clen += rune_width(cp);
}
wrapped.push_back(chunk);
wi = ci;
}
continue;
}
int curlen = visible_len(curline);
if (curline.empty()) curline = word;
else if (curlen + 1 + wlen <= width) { curline += ' '; curline += word; }
else { wrapped.push_back(curline); curline = word; }
}
if (!curline.empty()) wrapped.push_back(curline);
}
if (wrapped.empty()) wrapped.push_back("");
return wrapped;
}
static void print_row(int num_cols, const std::vector<int> &col_widths,
const std::vector<std::string> &cells, bool is_header) {
std::string s = "│";
for (int i = 0; i < num_cols; i++) {
std::string_view txt = (i < (int)cells.size()) ? std::string_view(cells[i]) : std::string_view();
int pad = col_widths[i] - visible_len(txt);
if (pad < 0) pad = 0;
std::string cell = " ";
cell.append(txt);
cell.append((size_t)pad + 1, ' ');
if (is_header) { std::string cc = cstr(cell, std::array{1}); s += cc; }
else { std::string cc = cstr(cell, std::array{2}); s += cc; }
s += "│";
}
std::string h = cstr(s, std::array{2});
printf("%s\n", h.c_str());
}
void render_table_print(const std::vector<std::string> &lines) {
if (lines.empty()) return;
2026-10-14 14:15:14 +03:00
/* Not an actual table (e.g. a list whose items merely contain '|'): render
* the lines as ordinary markdown instead of forcing a box-drawing table. */
if (!looks_like_table(lines)) {
MdState st;
for (auto &ln : lines) render_md_line_print(ln, st);
return;
}
2026-10-14 00:01:03 +03:00
std::vector<std::vector<std::string>> rows_cells;
std::vector<std::string> header_cells;
bool has_header = false;
for (auto &line : lines) {
auto cells = parse_table_cells(line);
if (is_table_sep(cells)) {
if (!rows_cells.empty() && !has_header) {
header_cells = std::move(rows_cells.back());
rows_cells.pop_back();
has_header = true;
}
continue;
}
rows_cells.push_back(std::move(cells));
}
if (!has_header && lines.size() < 2) {
MdState st;
for (auto &ln : lines) render_md_line_print(ln, st);
return;
}
int num_cols = (int)header_cells.size();
for (auto &row : rows_cells) if ((int)row.size() > num_cols) num_cols = (int)row.size();
if (num_cols == 0) return;
if (has_header) { while ((int)header_cells.size() < num_cols) header_cells.push_back(""); }
std::vector<int> col_widths(num_cols, 0);
for (int i = 0; i < num_cols; i++) {
if (has_header) { int vl = visible_len(header_cells[i]); if (vl > col_widths[i]) col_widths[i] = vl; }
for (auto &row : rows_cells) {
std::string_view cell = (i < (int)row.size()) ? std::string_view(row[i]) : std::string_view();
std::string ri = render_inline(cell);
int vl = visible_len(ri);
if (vl > col_widths[i]) col_widths[i] = vl;
}
if (col_widths[i] < 3) col_widths[i] = 3;
}
int max_table_width = term_width();
if (max_table_width < 20) max_table_width = 80;
int overhead = 3 * num_cols + 1;
int avail = max_table_width - overhead;
if (avail < num_cols * 3) avail = num_cols * 3;
int tot = 0;
for (int i = 0; i < num_cols; i++) tot += col_widths[i];
while (tot > avail) {
int maxIdx = 0, maxVal = col_widths[0];
for (int i = 1; i < num_cols; i++) if (col_widths[i] > maxVal) { maxVal = col_widths[i]; maxIdx = i; }
if (maxVal <= 3) break;
col_widths[maxIdx]--; tot--;
}
{
std::string s = "┌";
for (int i = 0; i < num_cols; i++) { for (int j = 0; j < col_widths[i] + 2; j++) s += "─"; if (i < num_cols - 1) s += "┬"; }
s += "┐";
std::string h = cstr(s, std::array{2});
printf("%s\n", h.c_str());
}
if (has_header) {
std::vector<std::vector<std::string>> hw(num_cols);
int maxh = 1;
for (int i = 0; i < num_cols; i++) {
std::string ri = render_inline(header_cells[i]);
hw[i] = wrap_cell(ri, col_widths[i]);
if ((int)hw[i].size() > maxh) maxh = (int)hw[i].size();
}
for (int lr = 0; lr < maxh; lr++) {
std::vector<std::string> rowview(num_cols);
for (int i = 0; i < num_cols; i++) rowview[i] = (lr < (int)hw[i].size()) ? hw[i][lr] : "";
print_row(num_cols, col_widths, rowview, true);
}
{
std::string s = "├";
for (int i = 0; i < num_cols; i++) { for (int j = 0; j < col_widths[i] + 2; j++) s += "─"; if (i < num_cols - 1) s += "┼"; }
s += "┤";
std::string h = cstr(s, std::array{2});
printf("%s\n", h.c_str());
}
}
for (auto &row : rows_cells) {
std::vector<std::vector<std::string>> rw(num_cols);
int maxr = 1;
for (int i = 0; i < num_cols; i++) {
std::string_view cell = (i < (int)row.size()) ? std::string_view(row[i]) : std::string_view();
std::string ri = render_inline(cell);
rw[i] = wrap_cell(ri, col_widths[i]);
if ((int)rw[i].size() > maxr) maxr = (int)rw[i].size();
}
for (int lr = 0; lr < maxr; lr++) {
std::vector<std::string> rowview(num_cols);
for (int i = 0; i < num_cols; i++) rowview[i] = (lr < (int)rw[i].size()) ? rw[i][lr] : "";
print_row(num_cols, col_widths, rowview, false);
}
}
{
std::string s = "└";
for (int i = 0; i < num_cols; i++) { for (int j = 0; j < col_widths[i] + 2; j++) s += "─"; if (i < num_cols - 1) s += "┴"; }
s += "┘";
std::string h = cstr(s, std::array{2});
printf("%s\n", h.c_str());
}
}
/* ----------------------------- block ---------------------------------- */
void render_md_print(std::string_view text) {
if (!g_col) { if (!text.empty()) fputs(std::string(text).c_str(), stdout); return; }
if (text.empty()) return;
std::vector<std::string> tbl;
MdState st;
size_t start = 0;
while (true) {
auto nl = text.find('\n', start);
std::string_view line = (nl == std::string_view::npos) ? text.substr(start) : text.substr(start, nl - start);
std::string lstr(line);
std::string_view t = trim_spaces(lstr);
bool is_tbl = (!st.in_code && !(t.size() >= 3 && t.substr(0, 3) == "```") && is_table_line(lstr));
if (is_tbl) {
tbl.push_back(lstr);
} else {
if (!tbl.empty()) { render_table_print(tbl); tbl.clear(); }
render_md_line_print(lstr, st);
}
if (nl == std::string_view::npos) break;
start = nl + 1;
}
if (!tbl.empty()) render_table_print(tbl);
}