mdview2: выровненная отрисовка таблиц с рамкой

Таблица обрабатывается как блок в два прохода:
- проход 1: границы блока + ОТРЕНДЕРЕННЫЕ ширины колонок (мерим тем же
  inline_scan, что и при отрисовке — невидимые маркеры стиля **/код не
  раздувают столбцы);
- проход 2: верхняя рамка ┌┬┐ → строки данных │ ячейка<pad> │ → разделитель
  заголовка ├┼┤ (из строки |---|) → нижняя рамка └┴┘.

Колонки выровнены по содержимому, рамка CP866 box-drawing. Широкая таблица
остаётся nowrap+hscroll. Ячейки разбиваются cell-итератором (без массивов
на стеке); пустой g_cells под измерение освобождается ручным флашем
pending-сегмента (флаг g_skip_flush в emit_seg, чтобы не флашить повторно).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-24 22:23:57 +03:00
parent 68201dca44
commit 13ef8d2fa5
+152 -10
View File
@@ -803,6 +803,7 @@ static uint8_t marker_visible_col(uint8_t kind, uint32_t p_start, uint32_t conte
static uint8_t g_cells[MAX_CACHE_LINE_LEN * 2]; static uint8_t g_cells[MAX_CACHE_LINE_LEN * 2];
static uint8_t g_ncells = 0; /* ячеек в текущем сегменте */ static uint8_t g_ncells = 0; /* ячеек в текущем сегменте */
static uint8_t g_ncells_at_space = 0; /* снимок g_ncells на последнем пробеле (для усечения при переносе) */ static uint8_t g_ncells_at_space = 0; /* снимок g_ncells на последнем пробеле (для усечения при переносе) */
static uint8_t g_skip_flush = 0; /* следующий emit_seg пропустит lag-флаш (pending уже зафлашен вручную, напр. перед таблицей) */
/* Пишет одну (char,attr)-ячейку в g_cells, молча игнорируя переполнение /* Пишет одну (char,attr)-ячейку в g_cells, молча игнорируя переполнение
* MAX_CACHE_LINE_LEN. Оставлена ФУНКЦИЕЙ намеренно: инлайн в сильно * MAX_CACHE_LINE_LEN. Оставлена ФУНКЦИЕЙ намеренно: инлайн в сильно
@@ -856,7 +857,10 @@ static void emit_seg(uint32_t off, uint8_t style, uint8_t ckind, uint8_t cont)
cur_seg_off = off; cur_seg_off = off;
n_lines++; n_lines++;
/* Флаш ячеек ПРЕДЫДУЩего сегмента (lag-1): он полностью собран форвард-сканером. */ /* Флаш ячеек ПРЕДЫДУЩего сегмента (lag-1): он полностью собран форвард-сканером. */
if (n_lines >= 2) seg_commit_cells((uint16_t)(n_lines - 2)); if (n_lines >= 2) {
if (g_skip_flush) g_skip_flush = 0; /* pending уже зафлашен вручную */
else seg_commit_cells((uint16_t)(n_lines - 2));
}
g_ncells = 0; /* буфер ячеек под новый сегмент */ g_ncells = 0; /* буфер ячеек под новый сегмент */
g_ncells_at_space = 0; g_ncells_at_space = 0;
} }
@@ -1339,6 +1343,102 @@ static uint32_t scan_join_stream(uint32_t q, uint32_t para_start, uint8_t col,
return q; return q;
} }
/* ====================== Таблицы: выровненная отрисовка ==================== */
#define TBL_MAX_COLS 16
#define TBL_ATTR ATTR_TEXT
/* CP866 light box-drawing. */
#define TBL_H 0xC4 /* ─ */
#define TBL_V 0xB3 /* │ */
#define TBL_TL 0xDA /* ┌ */
#define TBL_TM 0xC2 /* ┬ */
#define TBL_TR 0xBF /* ┐ */
#define TBL_ML 0xC3 /* ├ */
#define TBL_MM 0xC5 /* ┼ */
#define TBL_MR 0xB4 /* ┤ */
#define TBL_BL 0xC0 /* └ */
#define TBL_BM 0xC1 /* ┴ */
#define TBL_BR 0xD9 /* ┘ */
/* Смещение завершающего '\n' строки (или file_size). */
static uint32_t row_end(uint32_t p)
{
while (p < file_size && fb(p) != '\n') p++;
return p;
}
/* Начало контента первой ячейки строки (после ведущих пробелов и '|'). */
static uint32_t table_first_cell(uint32_t row_start)
{
uint32_t p = row_start;
while (p < file_size && fb(p) == ' ') p++;
if (p < file_size && fb(p) == '|') p++;
return p;
}
/* Следующая ячейка: [*cs,*ce) — обрезанный диапазон; *pp продвигается за '|'.
* 0 — ячеек больше нет (включая хвостовую пустоту после последнего '|'). */
static uint8_t table_next_cell(uint32_t *pp, uint32_t lineend, uint32_t *cs, uint32_t *ce)
{
uint32_t p = *pp;
if (p >= lineend) return 0;
uint32_t a = p;
while (p < lineend && fb(p) != '|') p++;
uint32_t b = p;
uint8_t had_pipe = (uint8_t)(p < lineend && fb(p) == '|');
if (had_pipe) p++;
*pp = p;
while (a < b && fb(a) == ' ') a++;
while (b > a && fb(b - 1) == ' ') b--;
if (!had_pipe && a == b) return 0;
*cs = a; *ce = b;
return 1;
}
/* Строка-разделитель (|---|:-:|): каждая ячейка непустая и только '-'/':'. */
static uint8_t table_is_sep_row(uint32_t row_start, uint32_t lineend)
{
uint32_t p = table_first_cell(row_start);
uint32_t cs, ce;
uint8_t n = 0;
while (table_next_cell(&p, lineend, &cs, &ce)) {
if (cs >= ce) return 0;
for (uint32_t q = cs; q < ce; q++) { char c = fb(q); if (c != '-' && c != ':') return 0; }
n++;
}
return (uint8_t)(n > 0);
}
/* Горизонтальная рамка в g_cells: left + (H×(w+2) + mid|right) по колонкам. */
static void table_border(const uint8_t *widths, uint8_t ncols, char left, char mid, char right)
{
gc_put(left, TBL_ATTR);
for (uint8_t c = 0; c < ncols; c++) {
gc_fill(TBL_H, TBL_ATTR, (uint8_t)(widths[c] + 2));
gc_put((c == (uint8_t)(ncols - 1)) ? right : mid, TBL_ATTR);
}
}
/* Строка данных в g_cells: │ <ячейка, добитая до widths[c]> │ … */
static void table_data_row(uint32_t row_start, uint32_t lineend, const uint8_t *widths, uint8_t ncols)
{
uint32_t p = table_first_cell(row_start);
uint32_t cs, ce;
gc_put(TBL_V, TBL_ATTR);
for (uint8_t c = 0; c < ncols; c++) {
uint8_t got = table_next_cell(&p, lineend, &cs, &ce);
gc_put(' ', TBL_ATTR);
uint8_t rendered = 0;
if (got && ce > cs) {
uint8_t before = g_ncells;
(void)inline_scan(cs, ce, 0, CK_OTHER, INIT_STYLE_PLAIN, ATTR_TEXT, 1);
rendered = (uint8_t)(g_ncells - before);
}
if (rendered < widths[c]) gc_fill(' ', ATTR_TEXT, (uint8_t)(widths[c] - rendered));
gc_put(' ', TBL_ATTR);
gc_put(TBL_V, TBL_ATTR);
}
}
/* Строит индекс сегментов рендера по загруженному файлу. */ /* Строит индекс сегментов рендера по загруженному файлу. */
static void index_lines(void) static void index_lines(void)
{ {
@@ -1417,16 +1517,58 @@ static void index_lines(void)
/* ---- строка таблицы (| ... |): один nowrap-сегмент на исходную строку ---- */ /* ---- строка таблицы (| ... |): один nowrap-сегмент на исходную строку ---- */
if (is_table_raw(p)) { if (is_table_raw(p)) {
emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0); /* Освобождаем g_cells под измерение ширин: коммитим pending-сегмент
set_nowrap_cur(); * сейчас; первый emit_seg таблицы пропустит свой lag-флаш. */
set_hscroll_cur(); /* таблица скроллится целым блоком */ if (n_lines >= 1) { seg_commit_cells((uint16_t)(n_lines - 1)); g_skip_flush = 1; }
{ g_ncells = 0;
uint32_t line_end = p;
while (line_end < file_size && fb(line_end) != '\n') line_end++; /* Проход 1: границы блока + ОТРЕНДЕРЕННЫЕ ширины колонок (мерим тем же
(void)inline_scan(p, line_end, 0, CK_OTHER, INIT_STYLE_PLAIN, ATTR_TEXT, 1); * inline_scan, что и при отрисовке — невидимые маркеры стиля не считаются). */
uint8_t widths[TBL_MAX_COLS];
for (uint8_t i = 0; i < TBL_MAX_COLS; i++) widths[i] = 0;
uint8_t ncols = 0;
uint32_t r = p;
while (r < file_size && is_table_raw(r)) {
uint32_t le = row_end(r);
uint8_t sep = table_is_sep_row(r, le);
uint32_t cp = table_first_cell(r);
uint32_t cs, ce;
uint8_t c = 0;
while (c < TBL_MAX_COLS && table_next_cell(&cp, le, &cs, &ce)) {
if (!sep && ce > cs) {
g_ncells = 0;
(void)inline_scan(cs, ce, 0, CK_OTHER, INIT_STYLE_PLAIN, ATTR_TEXT, 1);
if (g_ncells > widths[c]) widths[c] = g_ncells;
}
c++;
}
if (c > ncols) ncols = c;
r = (le < file_size) ? le + 1 : le;
} }
while (p < file_size && fb(p) != '\n') p++; g_ncells = 0; /* чисто для верхней рамки */
if (p < file_size) p++; uint32_t tbl_end = r;
if (ncols == 0) ncols = 1;
/* Проход 2: верхняя рамка → строки/разделитель → нижняя рамка. */
emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0);
set_nowrap_cur(); set_hscroll_cur();
table_border(widths, ncols, TBL_TL, TBL_TM, TBL_TR);
r = p;
while (r < tbl_end) {
uint32_t le = row_end(r);
emit_seg(r, INIT_STYLE_PLAIN, CK_OTHER, 0);
set_nowrap_cur(); set_hscroll_cur();
if (table_is_sep_row(r, le)) table_border(widths, ncols, TBL_ML, TBL_MM, TBL_MR);
else table_data_row(r, le, widths, ncols);
r = (le < file_size) ? le + 1 : le;
}
emit_seg(tbl_end, INIT_STYLE_PLAIN, CK_OTHER, 0);
set_nowrap_cur(); set_hscroll_cur();
table_border(widths, ncols, TBL_BL, TBL_BM, TBL_BR);
p = tbl_end;
continue; continue;
} }