Files
Sprinter-SDCC/examples/mdview2/mdview2_index.c
T
snark13 ffd179e064 mdview2: оптимизация размера — exe 28215 → 25616 (−2599 Б)
Раунд 1 (−1411): --max-allocs 100000 в Makefile (−848 кода) + снятие
всех нулевых инициализаторов file-scope переменных (_INITIALIZER
583→24; _DATA теперь зануляется в crt0).

Раунд 2 (−1188, индексатор 11019→9839): дедупликации в mdview2_index.c:
- classify_line: копия HR-проверки → вызов is_hr_raw;
- next_line() поверх row_end() вместо 9 копий «домотать до \n»;
- inline_marker: emph_to_attr(ls, base) вычисляется один раз (at);
- set_{nowrap,blank,code,hscroll}_cur → set_cur_flags(mask): строка
  code-блока делает один idx_put вместо трёх.

Проверено в MAME (README/UTF8TEST: маркеры, списки, цитаты, таблицы,
code-блоки, F8).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 21:15:43 +03:00

1001 lines
48 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* mdview2_index.c — парсер/индексатор markdown (сердце приложения).
*
* Единственный проход по исходному файлу (index_lines): классификация строк
* (classify_line, is_*_raw), сборка абзацев с переносом (scan_join_stream),
* inline-разметка (inline_marker/inline_scan) и эмиссия готовых (char,attr)-
* ячеек в рендер-кэш (gc_put → seg_commit_cells → cache_*). Ведёт индекс
* сегментов в EMM (idx_get/idx_put) и докручивает UTF-конвертацию впереди
* позиции чтения. Разделяемые символы и API — в mdview2.h.
*
* Приватно (не торчит в .h): idx_rec_t, cur_rec/cur_seg_off, буфер ячеек
* g_cells, styles_map, idx_get/idx_put и все классификаторы/сканеры.
*/
#include <stdint.h>
#include <string.h>
#include <conio.h> /* COLOR()/COLOR_* для ATTR_* в styles_map */
#include <sprinter.h>
#include <sprinter_mem.h> /* bank_read/bank_write (idx_get/idx_put) */
#include "mdview2.h"
static uint8_t emph_to_attr(uint8_t emph, uint8_t base_attr); /* forward (нужна в inline-сканере) */
static uint8_t is_hr_raw(uint32_t p); /* forward (нужна в classify_line) */
/* Начало следующей физической строки (байт после '\n', либо file_size). */
static uint32_t next_line(uint32_t p)
{
p = row_end(p);
return (p < file_size) ? p + 1 : p;
}
typedef struct idx_rec_s {
uint32_t off; /* смещение первого байта исходного текста для сегмента */
uint8_t flags; /* битовые флаги IF_* ниже */
uint8_t style; /* начальный стиль INIT_STYLE_* в начале сегмента */
uint8_t pad0; /* добивка до 8 байт (2048 записей на страницу 16 КБ) */
uint8_t pad1;
} idx_rec_t;
#define INDEX_REC_SIZE 8u
static uint32_t cur_seg_off; /* смещение последнего emit_seg (кэш в near-памяти) */
static idx_rec_t cur_rec; /* near-копия последней записанной idx-записи */
/* Для проверки границ выделения считаем пробел/таб/конец строки/0 пробельными. */
static uint8_t ws_or_eol(char c)
{
return (uint8_t)(c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == 0);
}
static uint8_t ws_or_eol_or_delim(char c)
{
return (uint8_t)(c == ' ' || c == ',' || c == '.' || c == '!' || c == '?' || c == ':' || c == ';' || c == '/'
|| c == '\t' || c == '\n' || c == '\r' || c == 0);
}
/* Маркер inline-выделения (*, **, _) распознаётся только если
* пробельная граница есть ровно с одной стороны (XOR-правило).
* Это защищает выражения вроде "2 * 3", "2 ** 3" и внутрисловные
* случаи (например COLOR_YELLOW) от ложного форматирования. */
static uint8_t is_emph_flanked(char prev_ch, char next_ch)
{
return (uint8_t)(ws_or_eol(prev_ch) != ws_or_eol_or_delim(next_ch));
}
/* Символ можно экранировать обратным слешем (\X): любая печатная ASCII-
* пунктуация (как в CommonMark). \* \_ \` \[ и т.п. → литерал X. */
static uint8_t is_escapable(char c)
{
return (uint8_t)(c >= '!' && c <= '~' &&
!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')));
}
/* Типы сегмента CK_* и стили INIT_STYLE_* — в mdview2.h (общие с таблицами). */
/* Доступ к индексу в EMM.
* Размер записи 8 байт, поэтому запись не пересекает границу страницы
* и bank_read/bank_write работают с цельным блоком. */
static void idx_get(uint16_t idx, idx_rec_t *r)
{
uint8_t page = (uint8_t)(idx >> 11); /* номер страницы: idx / 2048 */
uint16_t off = (uint16_t)((idx & 2047u) << 3); /* смещение записи: (idx % 2048) * 8 */
bank_read(index_phys[page], off, r, INDEX_REC_SIZE);
}
static void idx_put(uint16_t idx, const idx_rec_t *r)
{
uint8_t page = (uint8_t)(idx >> 11);
uint16_t off = (uint16_t)((idx & 2047u) << 3);
bank_write(index_phys[page], off, r, INDEX_REC_SIZE);
}
uint32_t seg_off(uint16_t idx)
{
idx_rec_t r;
if (idx >= n_lines) return file_size;
idx_get(idx, &r);
return r.off;
}
/* Установка флагов (маска IF_*) всегда идёт в последний emit_seg (n_lines-1).
* Используется near-копия cur_rec, чтобы не делать обратное чтение из банка. */
static void set_cur_flags(uint8_t f) { cur_rec.flags |= f; idx_put((uint16_t)(n_lines - 1), &cur_rec); }
/* ==================================================================
* Индексация строк
* ================================================================== */
/* INIT_STYLE_* — в mdview2.h (общие с модулем таблиц). */
/* Таблица сопоставления init-style -> атрибут.
* Порядок должен совпадать с INIT_STYLE_* / EM_* ниже. */
static const uint8_t styles_map[] = {
ATTR_TEXT, ATTR_TEXT_BOLD, ATTR_TEXT_ITALIC, ATTR_TEXT_UNDERSORE, ATTR_TEXT_CODE, ATTR_TEXT_STRIKE
};
/* Тип строки, который возвращает classify_line(). */
#define LK_PLAIN 0
#define LK_H1 1
#define LK_H2 2
#define LK_H3 3
#define LK_H4 4 /* также используется для H5/H6 */
#define LK_HR 5
#define LK_ULIST 6 /* маркер ненумерованного списка */
#define LK_OLIST 7 /* маркер нумерованного списка */
#define LK_QUOTE 8 /* маркер цитаты */
/* Классифицирует логическую строку.
* - Заголовки: 1..4 символа '#' и затем пробел/таб (H5/H6 сводятся к H4).
* - Горизонтальная линия: только '-', '*' или '_' (одного вида, >=3),
* возможно с пробелами/табами между символами.
* - Ненумерованный список: "- ", "* " или "+ " перед содержимым.
* - Нумерованный список: цифры + '.'/ ')' + пробел.
* - Цитата: '>' с опциональным пробелом.
* - Иначе обычный текст.
* Для заголовков/списков/цитат out_content указывает на первый байт
* содержимого после маркера и завершающего пробела. */
static uint8_t classify_line(uint32_t p_start, uint32_t *out_content)
{
if (p_start >= file_size) return LK_PLAIN;
char c0 = fb(p_start);
/* Заголовок? */
if (c0 == '#') {
uint8_t lvl = 0;
uint32_t p = p_start;
while (p < file_size && fb(p) == '#' && lvl < 6) {
lvl++; p++;
}
if (p < file_size) {
char ch = fb(p);
if (ch == ' ' || ch == '\t') {
p++;
*out_content = p;
if (lvl > 4) lvl = 4;
return lvl; /* LK_H1 .. LK_H4 */
}
}
/* Символ '#' без пробела после него не считается заголовком. */
}
/* Горизонтальный разделитель.
* Проверяется до списков, чтобы `- - -` не стало маркером списка. */
if (is_hr_raw(p_start)) return LK_HR;
/* Для списков/цитат дополнительно допускаем ведущие пробелы
* (базовая поддержка вложенности). */
uint32_t lp = p_start;
while (lp < file_size && fb(lp) == ' ') lp++;
char cl = (lp < file_size) ? fb(lp) : 0;
/* Ненумерованный список? */
if ((cl == '-' || cl == '*' || cl == '+') &&
(lp + 1) < file_size &&
fb(lp + 1) == ' ') {
*out_content = lp + 2;
return LK_ULIST;
}
/* Нумерованный список? */
if (cl >= '0' && cl <= '9') {
uint32_t p = lp;
while (p < file_size) {
char ch = fb(p);
if (ch < '0' || ch > '9') break;
p++;
}
if (p < file_size) {
char ch = fb(p);
if ((ch == '.' || ch == ')') &&
(p + 1) < file_size && fb(p + 1) == ' ') {
*out_content = p + 2;
return LK_OLIST;
}
}
}
/* Цитата? */
if (cl == '>') {
uint32_t p = lp + 1;
if (p < file_size && fb(p) == ' ') p++;
*out_content = p;
return LK_QUOTE;
}
return LK_PLAIN;
}
/* Возвращает видимую ширину префикса (отступ + маркер) в колонках. */
static uint8_t marker_visible_col(uint8_t kind, uint32_t p_start, uint32_t content_off)
{
switch (kind) {
case LK_ULIST:
case LK_QUOTE:
case LK_OLIST: return (uint8_t)(content_off - p_start);
default: return 0; /* plain/headers спец-префикса не имеют */
}
}
/* ================== Merge: сборка ячеек на этапе индекс-скана ==============
* Форвард-сканер (scan_join_stream/inline_scan + прямые emit'ы в index_lines)
* собирает готовые (char,attr)-ячейки в g_cells ПО ХОДУ единственного прохода
* по файлу — без отдельного второго скана. На каждом emit_seg ячейки
* ПРЕДЫДУЩего сегмента флашатся в кэш (lag-1: к этому моменту сегмент уже
* полностью просканирован); последний сегмент флашится после цикла index_lines.
* g_ncells_at_space — снимок длины на последнем пробеле для усечения буфера
* при переносе строки (отбрасываем уехавшее за границу слово). */
static uint8_t g_cells[MAX_CACHE_LINE_LEN * 2];
uint8_t g_ncells; /* ячеек в текущем сегменте (общий с таблицами) */
static uint8_t g_ncells_at_space; /* снимок g_ncells на последнем пробеле (для усечения при переносе) */
static uint8_t g_skip_flush; /* следующий emit_seg пропустит lag-флаш (pending уже зафлашен вручную, напр. перед таблицей) */
/* Обрыв индексации: причина (0 / TRUNC_CONTENT / TRUNC_LINES) и строка, на
* которой упёрлись. Выставляются ОДИН раз (первая причина побеждает); по ним
* index_lines() в конце пишет строку-сообщение (IF_TRUNC_MSG, рисуется вживую). */
static uint8_t g_trunc_cause;
static uint16_t g_trunc_line;
/* Пишет одну (char,attr)-ячейку в g_cells, молча игнорируя переполнение
* MAX_CACHE_LINE_LEN. Оставлена ФУНКЦИЕЙ намеренно: инлайн в сильно
* регистро-нагруженный скан-цикл на Z80/SDCC увеличивает спиллы и замедляет
* (проверено замером — макро-инлайн дал регрессию ~11→13с). */
void gc_put(char ch, uint8_t attr)
{
if (g_ncells < MAX_CACHE_LINE_LEN) {
uint16_t i = (uint16_t)g_ncells * 2u;
g_cells[i] = (uint8_t)ch;
g_cells[i + 1] = attr;
g_ncells++;
}
}
void gc_fill(char ch, uint8_t attr, uint8_t n) { while (n-- > 0) gc_put(ch, attr); }
/* Пишет g_cells как контент кэша для line_idx: flags берём из idx-записи,
* пустой буфер/blank → len 0; иначе резервируем место в пуле и коммитим. */
static void seg_commit_cells(uint16_t line_idx)
{
cache_rec_t out_rec;
out_rec.flags = 0; out_rec.reserved = 0; out_rec.pad[0] = 0; out_rec.pad[1] = 0;
idx_rec_t r;
uint8_t have = (uint8_t)(line_idx < n_lines);
if (have) { idx_get(line_idx, &r); out_rec.flags = r.flags; }
if (!have || (r.flags & IF_BLANK) || g_ncells == 0) {
out_rec.page = 0; out_rec.off = 0; out_rec.len = 0;
cache_dir_put(line_idx, &out_rec);
return;
}
out_rec.len = g_ncells;
if (cache_reserve((uint16_t)(g_ncells * 2u), &out_rec.page, &out_rec.off)) {
cache_commit(out_rec.page, out_rec.off, g_cells, (uint16_t)(g_ncells * 2u));
} else {
/* Контент-кэш исчерпан: фиксируем причину и строку обрыва (один раз).
* index_lines() оборвёт цикл и перезапишет эту строку сообщением. */
if (!g_trunc_cause) { g_trunc_cause = TRUNC_CONTENT; g_trunc_line = line_idx; }
out_rec.page = 0; out_rec.off = 0; out_rec.len = 0;
}
cache_dir_put(line_idx, &out_rec);
}
static void emit_seg(uint32_t off, uint8_t style, uint8_t ckind, uint8_t cont)
{
(void)ckind; /* тип сегмента восстанавливается флагами idx-записи */
if (n_lines >= max_lines) {
index_truncated = 1;
if (!g_trunc_cause) { g_trunc_cause = TRUNC_LINES; g_trunc_line = (uint16_t)(max_lines - 1); }
return;
}
cur_rec.off = off;
cur_rec.flags = (uint8_t)(cont ? IF_CONT : 0);
cur_rec.style = (uint8_t)(style & 7u);
cur_rec.pad0 = 0;
cur_rec.pad1 = 0;
idx_put(n_lines, &cur_rec);
cur_seg_off = off;
n_lines++;
/* Флаш ячеек ПРЕДЫДУЩего сегмента (lag-1): он полностью собран форвард-сканером. */
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_at_space = 0;
}
/* Проверяет, что физическая строка пуста (только пробелы/таб + перевод строки). */
static uint8_t is_line_blank(uint32_t p)
{
while (p < file_size) {
char c = fb(p);
if (c == '\n') return 1;
if (c != ' ' && c != '\t') return 0;
p++;
}
return 1;
}
/* Число ведущих пробелов строки (отступ). Используется для отделения
* dedent-пунктов списка (#3) и для отступа обычного абзаца (#4). */
static uint8_t leading_spaces(uint32_t p)
{
uint8_t n = 0;
while (p < file_size && fb(p) == ' ' && n < 250) { n++; p++; }
return n;
}
/* Быстрая проверка границы fenced-блока по маркеру ``` .
* Ведущие пробелы/табы пропускаются — fence может быть с отступом
* (например, ``` ```c ``` внутри элемента списка с отступом 2+ пробела).
* Язык после бэктиков (```c, ```sh) игнорируется: достаточно трёх ` подряд. */
static uint8_t is_fence_raw(uint32_t p)
{
while (p < file_size && (fb(p) == ' ' || fb(p) == '\t')) p++;
if (p + 2 >= file_size) return 0;
return (fb(p) == '`' && fb(p + 1) == '`' && fb(p + 2) == '`');
}
/* Быстрая проверка горизонтального разделителя (`---`, `***`, `___`). */
static uint8_t is_hr_raw(uint32_t p)
{
char c0 = fb(p);
if (c0 != '-' && c0 != '*' && c0 != '_') return 0;
char marker = 0;
uint8_t count = 0, ok = 1;
uint32_t hr_p = p;
while (hr_p < file_size) {
char ch = fb(hr_p++);
if (ch == '\n' || ch == '\r') break;
if (ch == ' ' || ch == '\t') continue;
if (ch != '-' && ch != '*' && ch != '_') { ok = 0; break; }
if (marker == 0) marker = ch;
else if (ch != marker) { ok = 0; break; }
count++;
}
return ok && count >= 3;
}
/* Строка таблицы: первый непустой символ — '|'.
* Ведущие пробелы разрешены; такие строки не склеиваются и не переносятся. */
static uint8_t is_table_raw(uint32_t p)
{
while (p < file_size) {
char c = fb(p);
if (c == ' ' || c == '\t') { p++; continue; }
return (uint8_t)(c == '|');
}
return 0;
}
/* Разбор одного inline-МАРКЕРА в позиции *pq (< bound): \X, [x], `, **, ~~,
* *, _, \t. Общий код для inline_scan() и scan_join_stream(). Возврат: 1 —
* маркер обработан (вызывающий делает continue), 0 — символ обычный (пробел/
* текст), его кладёт сам вызывающий (и проверяет перенос). Меняет
* *pq/*pls/*pseg/*pprev. attr берётся как emph_to_attr(line_style, base) —
* для join base=ATTR_TEXT, что тождественно styles_map[line_style]. */
static uint8_t inline_marker(uint32_t *pq, uint32_t bound, uint8_t *pls,
uint8_t *pseg, char *pprev, uint8_t base)
{
uint32_t q = *pq;
uint8_t ls = *pls;
char ch = fb(q);
uint8_t at = emph_to_attr(ls, base); /* атрибут литералов текущего стиля */
if (ls != INIT_STYLE_CODE && ch == '\\' && (q + 1) < bound && is_escapable(fb(q + 1))) {
char nb = fb(q + 1);
gc_put(nb, at);
*pseg += 1; *pq = q + 2; *pprev = nb; return 1;
}
if (ls != INIT_STYLE_CODE && ch == '[' && (q + 2) < bound && fb(q + 2) == ']') {
gc_put('[', ATTR_TEXT_BOLD); gc_put(fb(q + 1), ATTR_TEXT_BOLD); gc_put(']', ATTR_TEXT_BOLD);
*pseg += 3; *pq = q + 3; *pprev = ']'; return 1;
}
if (ch == '`') {
if (ls == INIT_STYLE_PLAIN) { *pls = INIT_STYLE_CODE; *pq = q + 1; return 1; }
if (ls == INIT_STYLE_CODE) { *pls = INIT_STYLE_PLAIN; *pq = q + 1; return 1; }
gc_put('`', at); *pseg += 1; *pq = q + 1; *pprev = '`'; return 1;
}
if (ch == '*' && (q + 1) < bound && fb(q + 1) == '*') {
char nx = ((q + 2) < bound) ? fb(q + 2) : '\n';
if (is_emph_flanked(*pprev, nx)) {
if (ls == INIT_STYLE_PLAIN && ws_or_eol(*pprev)) { *pls = INIT_STYLE_BOLD; *pq = q + 2; return 1; }
if (ls == INIT_STYLE_BOLD && ws_or_eol_or_delim(nx)) { *pls = INIT_STYLE_PLAIN; *pq = q + 2; return 1; }
}
gc_put('*', at); gc_put('*', at);
*pseg += 2; *pq = q + 2; *pprev = '*'; return 1;
}
if (ch == '~' && (q + 1) < bound && fb(q + 1) == '~') {
char nx = ((q + 2) < bound) ? fb(q + 2) : '\n';
if (is_emph_flanked(*pprev, nx)) {
if (ls == INIT_STYLE_PLAIN && ws_or_eol(*pprev)) { *pls = INIT_STYLE_STRIKE; *pq = q + 2; return 1; }
if (ls == INIT_STYLE_STRIKE && ws_or_eol_or_delim(nx)) { *pls = INIT_STYLE_PLAIN; *pq = q + 2; return 1; }
}
gc_put('~', at); gc_put('~', at);
*pseg += 2; *pq = q + 2; *pprev = '~'; return 1;
}
if (ch == '*' || ch == '_') {
char nx = ((q + 1) < bound) ? fb(q + 1) : '\n';
if (is_emph_flanked(*pprev, nx)) {
uint8_t ns = (ch == '*') ? INIT_STYLE_ITALIC : INIT_STYLE_UNDER;
if (ls == INIT_STYLE_PLAIN && ws_or_eol(*pprev)) { *pls = ns; *pq = q + 1; return 1; }
if (ls != INIT_STYLE_PLAIN && ws_or_eol_or_delim(nx)) { *pls = INIT_STYLE_PLAIN; *pq = q + 1; return 1; }
}
gc_put(ch, at); *pseg += 1; *pq = q + 1; *pprev = ch; return 1;
}
if (ch == '\t') {
uint8_t old_sc = *pseg;
uint8_t tgt_full = (uint8_t)((old_sc & (uint8_t)~(TAB_STOP - 1)) + TAB_STOP);
uint8_t tgt = tgt_full; if (tgt > SCREEN_W) tgt = SCREEN_W;
gc_fill(' ', at, (uint8_t)(tgt_full - old_sc));
*pseg = tgt; *pq = q + 1; *pprev = ' '; return 1;
}
return 0; /* пробел или обычный символ — обрабатывает вызывающий */
}
/* Общий сканер inline-форматирования и переносов.
* Идёт от q до q_end, учитывает стартовую колонку col и при необходимости
* эмитит continuation-сегменты. Возвращает итоговый line_style. */
uint8_t inline_scan(uint32_t q, uint32_t q_end, uint8_t col,
uint8_t ckind, uint8_t line_style, uint8_t base_attr,
uint8_t nowrap)
{
uint32_t last_space = 0xFFFFFFFFu;
uint8_t last_space_style = line_style; /* снимок line_style на момент last_space */
char prev_ch = ' ';
uint8_t seg_col = col;
/* base — атрибут «обычного» текста сегмента. Для заголовка первый сегмент
* имеет title-attr, а continuation-сегменты render рисует как PLAIN
* (ATTR_TEXT, kind=LK_PLAIN при cont) — переключаем base после 1-го переноса. */
uint8_t base = base_attr;
while (q < q_end && n_lines < max_lines) {
char ch = fb(q);
if (ch == '\n') {
/* Жёсткий перенос (уже проверен снаружи) или конец потока. */
q++;
emit_seg(q, line_style, ckind, (ckind == CK_LIST || ckind == CK_QUOTE) ? 1 : 0);
base = ATTR_TEXT;
last_space = 0xFFFFFFFFu; prev_ch = ' '; seg_col = col;
continue;
}
/* Inline-маркеры (\X [x] ` ** ~~ * _ \t) — общий разбор. */
if (inline_marker(&q, q_end, &line_style, &seg_col, &prev_ch, base)) continue;
if (ch == ' ') {
gc_put(' ', emph_to_attr(line_style, base));
last_space = q + 1;
last_space_style = line_style;
g_ncells_at_space = g_ncells;
seg_col++; q++; prev_ch = ' ';
} else {
gc_put(ch, emph_to_attr(line_style, base));
seg_col++; q++; prev_ch = ch;
}
if (!nowrap && seg_col >= SCREEN_W) {
uint8_t use_ls = (uint8_t)(last_space != 0xFFFFFFFFu && last_space > cur_seg_off);
uint32_t wrap_at = use_ls ? last_space : q;
if (wrap_at >= q_end || wrap_at == cur_seg_off) break;
uint8_t wrap_style = use_ls ? last_space_style : line_style;
if (use_ls) g_ncells = g_ncells_at_space;
emit_seg(wrap_at, wrap_style, ckind, 1);
base = ATTR_TEXT; /* continuation заголовка → PLAIN */
seg_col = col; last_space = 0xFFFFFFFFu; prev_ch = ' '; q = wrap_at;
line_style = wrap_style;
}
}
return line_style;
}
#define JOIN_MODE_LIST 1u
#define JOIN_MODE_QUOTE 2u
#define JOIN_MODE_PLAIN 3u
/* Общий потоковый сканер для index_lines().
* Убирает дублирование между ветками list/quote/plain:
* - mode задаёт правила обработки перевода строки,
* - остальная inline-логика (emphasis/таб/перенос) едина.
* Возвращает позицию q, на которой сканер остановился, и обновляет стиль. */
static uint32_t scan_join_stream(uint32_t q, uint32_t para_start, uint8_t col,
uint8_t ckind, uint8_t *io_line_style, uint8_t mode)
{
uint32_t last_space = 0xFFFFFFFFu;
uint8_t last_space_style = *io_line_style; /* снимок line_style на момент last_space */
char prev_ch = ' ';
uint8_t seg_col = col;
uint8_t line_style = *io_line_style;
while (q < file_size && n_lines < max_lines) {
char ch = fb(q);
uint8_t soft_break = 0;
if (ch == '\n') {
uint32_t next = q + 1;
if (next >= file_size) break;
if (mode == JOIN_MODE_LIST) {
if (is_line_blank(next)) break;
if (is_fence_raw(next) || is_hr_raw(next) || is_table_raw(next)) break;
{
uint32_t next_content = next;
uint8_t next_kind = classify_line(next, &next_content);
if (next_kind == LK_ULIST || next_kind == LK_OLIST ||
next_kind == LK_QUOTE ||
(next_kind >= LK_H1 && next_kind <= LK_H4) ||
next_kind == LK_HR) {
break;
}
}
while (next < file_size) {
char ws = fb(next);
if (ws == ' ' || ws == '\t') next++;
else break;
}
q = next;
soft_break = 1;
ch = ' ';
} else if (mode == JOIN_MODE_QUOTE) {
if (is_line_blank(next)) break;
if (is_fence_raw(next) || is_hr_raw(next) || is_table_raw(next)) break;
{
uint32_t next_content = next;
uint8_t next_kind = classify_line(next, &next_content);
if (next_kind != LK_QUOTE) break;
{
uint32_t next_line_end = row_end(next_content);
{
uint8_t empty_quote = 1;
for (uint32_t z = next_content; z < next_line_end; z++) {
char c = fb(z);
if (c != ' ' && c != '\t') { empty_quote = 0; break; }
}
if (empty_quote) break;
}
{
uint32_t t = next_content;
while (t < next_line_end && (fb(t) == ' ' || fb(t) == '\t')) t++;
if (t < next_line_end && fb(t) == '>') break;
}
while (next_content < next_line_end) {
char ws = fb(next_content);
if (ws == ' ' || ws == '\t') next_content++;
else break;
}
if (next_content < next_line_end && fb(next_content) == '>') {
next_content++;
if (next_content < next_line_end && fb(next_content) == ' ') next_content++;
}
q = next_content;
soft_break = 1;
ch = ' ';
}
}
} else { /* JOIN_MODE_PLAIN */
if (fb(next) == '\n') break;
if (is_fence_raw(next) || is_hr_raw(next) || is_table_raw(next)) break;
{
uint32_t dummy;
if (classify_line(next, &dummy) != LK_PLAIN) break;
}
if (is_line_blank(next)) break;
{
uint8_t is_hard = 0;
if (q >= para_start + 1) {
char b1 = fb(q - 1);
if (b1 == '\\') {
is_hard = 1;
} else if (q >= para_start + 2) {
char b2 = fb(q - 2);
if ((b1 == ' ' || b1 == '\t') && (b2 == ' ' || b2 == '\t'))
is_hard = 1;
}
}
if (is_hard) {
q++;
emit_seg(q, line_style, ckind, 0);
last_space = 0xFFFFFFFFu;
prev_ch = ' ';
seg_col = col;
continue;
}
}
q++;
/* как render: пропускаем ведущие пробелы/табы строки-продолжения,
* склейка даёт РОВНО один пробел (иначе merge эмитит отступ как лишние ячейки) */
while (q < file_size && (fb(q) == ' ' || fb(q) == '\t')) q++;
soft_break = 1;
ch = ' ';
}
}
/* Inline-маркеры (\X [x] ` ** ~~ * _ \t) — общий разбор; при
* soft_break ch==' ' (синтетический пробел склейки) — маркеры
* пропускаем, его кладёт ветка пробела ниже. */
if (!soft_break && inline_marker(&q, file_size, &line_style, &seg_col, &prev_ch, ATTR_TEXT))
continue;
if (ch == ' ') {
gc_put(' ', styles_map[line_style]);
if (soft_break) {
last_space = q;
last_space_style = line_style;
seg_col++;
} else {
last_space = q + 1;
last_space_style = line_style;
seg_col++;
q++;
}
g_ncells_at_space = g_ncells; /* снимок для усечения */
prev_ch = ' ';
} else {
gc_put(ch, styles_map[line_style]);
seg_col++;
q++; /* soft_break здесь всегда 0 (ch!=' ') */
prev_ch = ch;
}
if (seg_col >= SCREEN_W) {
uint8_t use_ls = (uint8_t)(last_space != 0xFFFFFFFFu && last_space > cur_seg_off);
uint32_t wrap_at = use_ls ? last_space : q;
if (wrap_at >= file_size || wrap_at == cur_seg_off) break;
uint8_t wrap_style = use_ls ? last_space_style : line_style;
if (use_ls) g_ncells = g_ncells_at_space; /* усечь до пробела */
emit_seg(wrap_at, wrap_style, ckind, 1);
/* Префикс continuation-сегмента: список и отступленный абзац (#4) —
* col пробелов выравнивания; цитата — отступ + маркер 0xB3 + пробел.
* Для обычного абзаца без отступа col==0 → префикса нет. */
if (ckind == CK_LIST || ckind == CK_PLAIN) {
gc_fill(' ', ATTR_TEXT, col);
} else if (ckind == CK_QUOTE) {
uint32_t content_off2 = para_start + col;
uint8_t qsp = 0;
while (qsp + 2 < content_off2 && qsp < SCREEN_W && fb(para_start + qsp) == ' ') qsp++;
gc_fill(' ', ATTR_TEXT, qsp);
gc_put(0xB3, ATTR_QUOTE_MARKER);
gc_put(' ', ATTR_TEXT);
}
seg_col = col;
last_space = 0xFFFFFFFFu;
prev_ch = ' ';
q = wrap_at;
/* Откатываем line_style к снимку на момент wrap_at: диапазон
* [wrap_at, q_old) будет пересканирован заново, и переключающие
* символы (обратная кавычка, звёздочка, подчёркивание, тильда)
* внутри него иначе применились бы дважды — именно так возникал
* баг: code-маркер на границе переноса ломает цвет следующего слова (mdview.c, отчёт пользователя
* 2026-06-23). */
line_style = wrap_style;
}
}
*io_line_style = line_style;
return q;
}
/* Отрисовка таблиц (row_end/table_*) — в модуле mdview2_table.c. */
/* Строит индекс сегментов рендера по загруженному файлу. */
void index_lines(void)
{
uint8_t in_block = 0;
n_lines = 0;
index_truncated = 0;
g_trunc_cause = 0;
g_trunc_line = 0;
cur_seg_off = 0xFFFFFFFFu; /* пока не эмитили ни одного сегмента */
if (file_size == 0) return;
uint32_t p = 0;
while (p < file_size && n_lines < max_lines && !g_abort && !g_trunc_cause) {
/* UTF-8: держим конвертацию впереди позиции чтения (включая look-ahead
* склейки/классификации ~ один абзац). file_size при этом = доступная
* сконвертированная часть и растёт по ходу. */
if (g_utf_building && (uint32_t)(p + CONV_MARGIN) > file_size)
utf_convert_more((uint32_t)(p + CONV_MARGIN));
if (g_loading)
progress_tick(); /* первый экран + статус + скролл по готовым данным */
else if ((n_lines & 15) == 0)
spinner_tick();
/* Пустая строка — разделитель параграфов.
* В индекс добавляем не более одной пустой экранной строки подряд. */
if (is_line_blank(p)) {
p = next_line(p);
/* Серия пустых строк схлопывается в одну визуальную строку. */
if (n_lines == 0 || !(cur_rec.flags & IF_BLANK)) {
emit_seg(p, INIT_STYLE_PLAIN, CK_PLAIN, 0);
set_cur_flags(IF_BLANK);
}
continue;
}
/* ---- fenced code-блок (граница или тело) ---- */
if (is_fence_raw(p)) {
in_block = !in_block;
emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0);
set_cur_flags(IF_NOWRAP);
p = next_line(p);
continue;
}
if (in_block) {
emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0);
set_cur_flags(IF_NOWRAP | IF_CODE | IF_HSCROLL); /* код скроллится целым блоком */
{
uint8_t ccx = 0;
uint32_t pp = p;
while (pp < file_size && fb(pp) != '\n') {
char ch = fb(pp);
if (ch == '\t') {
uint8_t tgt = (uint8_t)((ccx & (uint8_t)~(TAB_STOP - 1)) + TAB_STOP);
gc_fill(' ', ATTR_TEXT_CODE, (uint8_t)(tgt - ccx));
ccx = tgt;
} else if (ch == '\r') {
gc_put(' ', ATTR_TEXT_CODE); ccx++; /* как render: \r → пробел */
} else {
gc_put(ch, ATTR_TEXT_CODE); ccx++;
}
pp++;
}
}
p = next_line(p);
continue;
}
/* ---- горизонтальный разделитель ---- */
if (is_hr_raw(p)) {
emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0);
set_cur_flags(IF_NOWRAP);
gc_fill(0xC4, ATTR_HR, SCREEN_W);
p = next_line(p);
continue;
}
/* ---- строка таблицы (| ... |): один nowrap-сегмент на исходную строку ---- */
if (is_table_raw(p)) {
/* Освобождаем g_cells под измерение ширин: коммитим pending-сегмент
* сейчас; первый emit_seg таблицы пропустит свой lag-флаш. */
if (n_lines >= 1) { seg_commit_cells((uint16_t)(n_lines - 1)); g_skip_flush = 1; }
g_ncells = 0;
/* Проход 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;
}
g_ncells = 0; /* чисто для верхней рамки */
uint32_t tbl_end = r;
if (ncols == 0) ncols = 1;
/* Проход 2: верхняя рамка → строки/разделитель → нижняя рамка. */
emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0);
set_cur_flags(IF_NOWRAP | IF_HSCROLL);
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_cur_flags(IF_NOWRAP | IF_HSCROLL);
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_cur_flags(IF_NOWRAP | IF_HSCROLL);
table_border(widths, ncols, TBL_BL, TBL_BM, TBL_BR);
p = tbl_end;
continue;
}
/* ---- классификация текущей строки ---- */
uint32_t content_off = p;
uint8_t kind = classify_line(p, &content_off);
/* ---- Заголовок: отдельная строка, не склеивается с параграфом ---- */
if (kind >= LK_H1 && kind <= LK_H4) {
uint8_t hbase; /* title-attr, как в render */
switch (kind) {
case LK_H1: hbase = ATTR_TEXT_TITLE1; break;
case LK_H2: hbase = ATTR_TEXT_TITLE2; break;
case LK_H3: hbase = ATTR_TEXT_TITLE3; break;
default: hbase = ATTR_TEXT_TITLE4; break;
}
emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0);
uint32_t line_end = row_end(content_off);
(void)inline_scan(content_off, line_end, 0, CK_OTHER, INIT_STYLE_PLAIN, hbase, 0);
p = next_line(p);
continue;
}
/* ---- Пункт списка: может занимать несколько физических строк ---- */
if (kind == LK_ULIST || kind == LK_OLIST) {
uint8_t col = marker_visible_col(kind, p, content_off);
uint8_t line_style = INIT_STYLE_PLAIN;
uint32_t q = content_off;
emit_seg(p, line_style, CK_LIST, 0);
/* Префикс ПЕРВОГО сегмента: отступ + маркер + пробел (как render). */
{
uint32_t qq = p, qmax = p + SCREEN_W;
if (kind == LK_ULIST) {
while (qq + 2 < content_off && qq < qmax && fb(qq) == ' ') qq++;
gc_fill(' ', ATTR_TEXT, (uint8_t)(qq - p));
gc_put(0x07, ATTR_LIST_MARKER);
gc_put(' ', ATTR_TEXT);
} else {
while (qq < content_off && qq < qmax && fb(qq) == ' ') qq++;
gc_fill(' ', ATTR_TEXT, (uint8_t)(qq - p));
while (qq < content_off - 1) { gc_put(fb(qq), ATTR_LIST_MARKER); qq++; }
gc_put(' ', ATTR_TEXT);
}
}
q = scan_join_stream(q, p, col, CK_LIST, &line_style, JOIN_MODE_LIST);
/* Переходим к началу следующей физической строки после пункта. */
q = next_line(q);
/* Одна пустая строка между соседними маркерами списка подавляется.
* Две и более пустых строки оставляют видимый разделитель. */
if (q < file_size && is_line_blank(q)) {
uint32_t t = q;
uint8_t blanks = 0;
while (t < file_size && is_line_blank(t)) {
t = next_line(t);
blanks++;
// if (blanks >= 2) break;
}
if (blanks == 1 && t < file_size) {
uint32_t d = t;
uint8_t nk = classify_line(t, &d);
if (nk == LK_ULIST || nk == LK_OLIST) {
/* #3: подавляем blank только если следующий пункт того же
* уровня или глубже; при МЕНЬШЕМ отступе (dedent) пустую
* строку оставляем — пункты должны визуально разделиться. */
if (leading_spaces(t) >= leading_spaces(p)) {
p = t; /* подавляем единственную пустую строку */
continue;
}
}
}
}
p = q;
continue;
}
/* ---- Параграф цитаты: может покрывать несколько физических строк ---- */
if (kind == LK_QUOTE) {
uint8_t col = marker_visible_col(kind, p, content_off);
uint8_t line_style = INIT_STYLE_PLAIN;
uint32_t q = content_off;
emit_seg(p, line_style, CK_QUOTE, 0);
/* Префикс ПЕРВОГО сегмента: отступ + маркер 0xB3 + пробел (как render LK_QUOTE). */
{
uint32_t qq = p, qmax = p + SCREEN_W;
while (qq + 1 < content_off && qq < qmax && fb(qq) == ' ') qq++;
gc_fill(' ', ATTR_TEXT, (uint8_t)(qq - p));
gc_put(0xB3, ATTR_QUOTE_MARKER);
gc_put(' ', ATTR_TEXT);
}
q = scan_join_stream(q, p, col, CK_QUOTE, &line_style, JOIN_MODE_QUOTE);
p = next_line(q);
continue;
}
/* ---- Обычный текстовый параграф: склейка через soft-break ---- */
{
uint8_t line_style = INIT_STYLE_PLAIN;
/* #4: если абзац начинается с отступа — все его перенесённые строки
* получают такой же отступ (как continuation-префикс списка, но без
* маркера). Контент сканируем после отступа. Патологически большой
* отступ игнорируем. */
uint8_t indent = leading_spaces(p);
if (indent >= SCREEN_W / 2) indent = 0;
emit_seg(p, line_style, CK_PLAIN, 0);
if (indent) gc_fill(' ', ATTR_TEXT, indent); /* префикс первого сегмента */
uint32_t q = p + indent;
q = scan_join_stream(q, p, indent, CK_PLAIN, &line_style, JOIN_MODE_PLAIN);
if (q >= file_size) {
p = file_size;
} else {
/* q стоит на завершающем newline параграфа.
* Переходим на следующую строку, чтобы внешняя петля её
* корректно переклассифицировала. */
p = next_line(q);
}
}
}
/* emit_seg() флашит ячейки с отставанием на одну строку (см. её
* комментарий) — последняя эмитированная строка файла так и остаётся
* незафлашенной, её ячейки ещё в g_cells; коммитим здесь. */
if (n_lines > 0) seg_commit_cells((uint16_t)(n_lines - 1));
/* Лимит строк ловим ЗДЕСЬ, а не в emit_seg: главный цикл выходит по
* `n_lines < max_lines` ДО того, как emit_seg будет вызван в переполненном
* состоянии (для code-block — один emit_seg на строку, эта ветка не
* достигается). Признак — остановились, а файл не кончился; g_abort и
* исчерпание контента (TRUNC_CONTENT) уже обработаны/исключены. */
if (!g_trunc_cause && !g_abort && p < file_size) {
g_trunc_cause = TRUNC_LINES;
g_trunc_line = (uint16_t)(max_lines - 1);
}
/* Файл был обрезан до 256 КБ при загрузке, а индексация дошла до конца
* клампнутых данных без исчерпания контента/строк → ДОПИСЫВАЕМ строку
* (место есть: n_lines < max_lines). Приоритет ниже content/lines —
* те означают, что обрезали ещё раньше, их сообщение точнее. */
if (!g_trunc_cause && !g_abort && g_file_clamped) {
g_trunc_cause = TRUNC_FILE;
g_trunc_line = n_lines;
}
/* Обрыв по контенту/лимиту строк (не путать с пользовательским g_abort):
* документ заканчиваем строкой-сообщением на g_trunc_line. Сообщение
* рисуется вживую (IF_TRUNC_MSG) — место в контент-кэше ему не нужно, что
* и снимает тупик «кэш полон, а сообщение тоже надо закэшировать». */
if (g_trunc_cause && !g_abort) {
cache_rec_t r;
r.page = 0; r.off = 0; r.len = 0;
r.flags = IF_TRUNC_MSG; r.reserved = g_trunc_cause;
r.pad[0] = 0; r.pad[1] = 0;
cache_dir_put(g_trunc_line, &r);
n_lines = (uint16_t)(g_trunc_line + 1); /* документ кончается на строке-обрыве */
}
}
#define EM_NONE 0
#define EM_BOLD 1
#define EM_ITALIC 2
#define EM_UNDER 3
#define EM_CODE 4
#define EM_STRIKE 5
/* Преобразует внутреннее состояние emphasis в экранный атрибут. */
static uint8_t emph_to_attr(uint8_t emph, uint8_t base_attr)
{
switch (emph) {
case EM_BOLD: return ATTR_TEXT_BOLD;
case EM_ITALIC: return ATTR_TEXT_ITALIC;
case EM_UNDER: return ATTR_TEXT_UNDERSORE;
case EM_CODE: return ATTR_TEXT_CODE;
case EM_STRIKE: return ATTR_TEXT_STRIKE;
default: return base_attr;
}
}