/* render.cpp — terminal markdown rendering with box-drawing tables. */ #include "bantam.hpp" #include #include #include #include #include #include #include /* ----------------------------- 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 &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; } static std::vector 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 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 == '_'; } static void wrap_pairs(std::string &dst, std::string_view src, std::string_view open, std::string_view close, std::span codes, 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 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 wrap_cell(std::string_view s, int width) { std::vector lines; if (width <= 0) { lines.push_back(""); return lines; } std::string copy(s); std::vector blocks; std::string cur; size_t p = 0; while (p < copy.size()) { if (copy.compare(p, 4, "
") == 0) { blocks.push_back(cur); cur.clear(); p += 4; continue; } if (copy.compare(p, 5, "
") == 0) { blocks.push_back(cur); cur.clear(); p += 5; continue; } if (copy.compare(p, 6, "
") == 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 wrapped; for (auto &cl : blocks) { std::vector 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 &col_widths, const std::vector &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 &lines) { if (lines.empty()) return; std::vector> rows_cells; std::vector 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 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> 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 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> 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 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 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); }