mdview2: merge препроцессинга в один проход (22→11с)
Раньше препроцессинг сканировал файл ДВАЖДЫ: index_lines считал точки переноса (forward-сканеры scan_join_stream/inline_scan), а отдельный render_line_to_cache заново сканировал каждый сегмент для сборки (char,attr)-ячеек. Профилирование показало, что всё время — в этих двух посимвольных проходах (bank I/O и cont-walk ≈ 0). Теперь forward-сканер собирает ячейки в g_cells ПО ХОДУ единственного прохода; emit_seg флашит ячейки предыдущего сегмента в кэш (lag-1), последний — после цикла. Перенос строки усекает буфер до снимка на последнем пробеле (g_ncells_at_space), continuation-сегменты получают префикс (отступ списка / маркер цитаты 0xB3 / title только в 1-й строке заголовка). Прямые типы (код verbatim, HR, таблица через nowrap-inline, blank/fence) строят ячейки на месте. Миграция шла по типам блоков с dual-verify (старый render строил эталон, merge сверял ячейки) — найдены и согласованы расхождения forward-сканера со старым рендером: backtick внутри эмфазиса = литерал; одиночный маркер закрывает ЛЮБОЙ активный эмфазис; soft-join пропускает ведущие пробелы строки-продолжения; хвостовые пробелы нерелевантны. Все типы дали 0 расхождений, после чего render_line_to_cache / cc_put / cc_fill / handle_inline_marker и verify-каркас удалены (−185 строк). Итог на 52КБ README: 22→11с (2×). Совокупно с прошлым коммитом 24→11с. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+199
-384
@@ -262,10 +262,8 @@ static void cache_commit(uint8_t page, uint16_t off, const void *buf, uint16_t l
|
||||
bank_write(page, off, buf, len);
|
||||
}
|
||||
|
||||
/* Фаза 3: рендер-воркер вызывается из emit_seg() с отставанием на одну
|
||||
* строку, поэтому нужна форвард-декларация — определение далеко ниже,
|
||||
* рядом с остальной inline-парсинг логикой. */
|
||||
static void render_line_to_cache(uint16_t line_idx);
|
||||
/* emph_to_attr определена ниже (рядом с inline-логикой), нужна в inline_scan. */
|
||||
static uint8_t emph_to_attr(uint8_t emph, uint8_t base_attr);
|
||||
|
||||
static uint16_t n_lines = 0;
|
||||
static uint16_t max_lines = 0; /* ёмкость индекса: index_pages * 2048 */
|
||||
@@ -776,23 +774,59 @@ static uint8_t marker_visible_col(uint8_t kind, uint32_t p_start, uint32_t conte
|
||||
}
|
||||
}
|
||||
|
||||
/* Добавляет запись сегмента в EMM-индекс.
|
||||
* near-копии cur_rec/cur_seg_off используются для быстрых последующих
|
||||
* операций без повторного чтения последней записи из банка.
|
||||
*
|
||||
* Фаза 3: рендер-воркер вызывается отсюда же, но с ОТСТАВАНИЕМ НА ОДНУ
|
||||
* строку — render_line_to_cache(N) сам ищет конец сегмента через
|
||||
* idx_get(N+1), а пока emit_seg() для строки N не вызван, этой записи
|
||||
* ещё не существует. Поэтому здесь, сразу после того как строка N
|
||||
* (n_lines-1, ДО инкремента) получила свой off, мы рендерим строку
|
||||
* N-1 (n_lines-2) — её собственные флаги (IF_NOWRAP/IF_CODE/IF_BLANK)
|
||||
* уже были дописаны вызовом set_*_cur() на ПРЕДЫДУЩЕЙ итерации
|
||||
* index_lines(), раньше этого вызова emit_seg(). Последнюю строку
|
||||
* файла (для которой "следующей" не будет) дорендеривает сам
|
||||
* index_lines() после выхода из своего основного цикла. */
|
||||
/* ================== Merge: сборка ячеек на этапе индекс-скана ==============
|
||||
* Форвард-сканер (scan_join_stream/inline_scan + прямые emit'ы в index_lines)
|
||||
* собирает готовые (char,attr)-ячейки в g_cells ПО ХОДУ единственного прохода
|
||||
* по файлу — без отдельного второго скана. На каждом emit_seg ячейки
|
||||
* ПРЕДЫДУЩего сегмента флашатся в кэш (lag-1: к этому моменту сегмент уже
|
||||
* полностью просканирован); последний сегмент флашится после цикла index_lines.
|
||||
* g_cur_merged переживает переносы внутри одного параграфа; g_next_merged
|
||||
* выставляет index_lines перед блоком (рудимент эпохи поэтапной миграции —
|
||||
* сейчас все типы строит merge, так что флаг всегда 1). */
|
||||
static uint8_t g_cells[MAX_CACHE_LINE_LEN * 2];
|
||||
static uint8_t g_ncells = 0; /* ячеек в текущем сегменте */
|
||||
static uint8_t g_ncells_at_space = 0; /* снимок g_ncells на последнем пробеле (для усечения при переносе) */
|
||||
static uint8_t g_cur_merged = 0; /* строящийся сегмент построен merge'ем (всегда 1) */
|
||||
static uint8_t g_next_merged = 0; /* статус следующего сегмента (ставит index_lines) */
|
||||
|
||||
static 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++;
|
||||
}
|
||||
}
|
||||
static 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 {
|
||||
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; /* тип сегмента восстанавливается на этапе рендера */
|
||||
(void)ckind; /* тип сегмента восстанавливается флагами idx-записи */
|
||||
if (n_lines >= max_lines) { index_truncated = 1; return; }
|
||||
cur_rec.off = off;
|
||||
cur_rec.flags = (uint8_t)(cont ? IF_CONT : 0);
|
||||
@@ -802,7 +836,11 @@ static void emit_seg(uint32_t off, uint8_t style, uint8_t ckind, uint8_t cont)
|
||||
idx_put(n_lines, &cur_rec);
|
||||
cur_seg_off = off;
|
||||
n_lines++;
|
||||
if (n_lines >= 2) render_line_to_cache((uint16_t)(n_lines - 2));
|
||||
/* Флаш ячеек ПРЕДЫДУЩего сегмента (lag-1): он полностью собран форвард-сканером. */
|
||||
if (n_lines >= 2) seg_commit_cells((uint16_t)(n_lines - 2));
|
||||
g_cur_merged = g_next_merged; /* статус нового сегмента (сейчас всегда 1) */
|
||||
g_ncells = 0; /* буфер ячеек под новый сегмент */
|
||||
g_ncells_at_space = 0;
|
||||
}
|
||||
|
||||
/* Проверяет, что физическая строка пуста (только пробелы/таб + перевод строки). */
|
||||
@@ -864,12 +902,17 @@ static uint8_t is_table_raw(uint32_t p)
|
||||
* Идёт от q до q_end, учитывает стартовую колонку col и при необходимости
|
||||
* эмитит continuation-сегменты. Возвращает итоговый line_style. */
|
||||
static uint8_t inline_scan(uint32_t q, uint32_t q_end, uint8_t col,
|
||||
uint8_t ckind, uint8_t line_style)
|
||||
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);
|
||||
@@ -878,14 +921,16 @@ static uint8_t inline_scan(uint32_t q, uint32_t q_end, uint8_t col,
|
||||
/* Жёсткий перенос (уже проверен снаружи) или конец потока. */
|
||||
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;
|
||||
}
|
||||
|
||||
if (ch == '`') {
|
||||
if (line_style == INIT_STYLE_CODE) line_style = INIT_STYLE_PLAIN;
|
||||
else line_style = INIT_STYLE_CODE;
|
||||
q++; continue;
|
||||
if (line_style == INIT_STYLE_PLAIN) { line_style = INIT_STYLE_CODE; q++; continue; }
|
||||
if (line_style == INIT_STYLE_CODE) { line_style = INIT_STYLE_PLAIN; q++; continue; }
|
||||
if (g_cur_merged) gc_put('`', emph_to_attr(line_style, base));
|
||||
seg_col++; q++; prev_ch = '`'; continue;
|
||||
}
|
||||
if (ch == '*' && (q + 1) < q_end && fb(q + 1) == '*') {
|
||||
char next_ch = ((q + 2) < q_end) ? fb(q + 2) : '\n';
|
||||
@@ -896,6 +941,7 @@ static uint8_t inline_scan(uint32_t q, uint32_t q_end, uint8_t col,
|
||||
line_style = INIT_STYLE_PLAIN; q += 2; continue;
|
||||
}
|
||||
}
|
||||
if (g_cur_merged) { gc_put('*', emph_to_attr(line_style, base)); gc_put('*', emph_to_attr(line_style, base)); }
|
||||
seg_col += 2; q += 2; prev_ch = '*'; continue;
|
||||
}
|
||||
if (ch == '~' && (q + 1) < q_end && fb(q + 1) == '~') {
|
||||
@@ -907,6 +953,7 @@ static uint8_t inline_scan(uint32_t q, uint32_t q_end, uint8_t col,
|
||||
line_style = INIT_STYLE_PLAIN; q += 2; continue;
|
||||
}
|
||||
}
|
||||
if (g_cur_merged) { gc_put('~', emph_to_attr(line_style, base)); gc_put('~', emph_to_attr(line_style, base)); }
|
||||
seg_col += 2; q += 2; prev_ch = '~'; continue;
|
||||
}
|
||||
if (ch == '*' || ch == '_') {
|
||||
@@ -915,36 +962,41 @@ static uint8_t inline_scan(uint32_t q, uint32_t q_end, uint8_t col,
|
||||
uint8_t new_style = (ch == '*') ? INIT_STYLE_ITALIC : INIT_STYLE_UNDER;
|
||||
if (line_style == INIT_STYLE_PLAIN && ws_or_eol(prev_ch)) {
|
||||
line_style = new_style; q++; continue;
|
||||
} else if (line_style == new_style && ws_or_eol_or_delim(next_ch)) {
|
||||
} else if (line_style != INIT_STYLE_PLAIN && ws_or_eol_or_delim(next_ch)) {
|
||||
/* как render: закрывает любой активный эмфазис */
|
||||
line_style = INIT_STYLE_PLAIN; q++; continue;
|
||||
}
|
||||
}
|
||||
if (g_cur_merged) gc_put(ch, emph_to_attr(line_style, base));
|
||||
seg_col++; q++; prev_ch = ch; continue;
|
||||
}
|
||||
if (ch == '\t') {
|
||||
uint8_t tgt = (uint8_t)((seg_col & (uint8_t)~(TAB_STOP - 1)) + TAB_STOP);
|
||||
uint8_t old_sc = seg_col;
|
||||
uint8_t tgt_full = (uint8_t)((seg_col & (uint8_t)~(TAB_STOP - 1)) + TAB_STOP);
|
||||
uint8_t tgt = tgt_full;
|
||||
if (tgt > SCREEN_W) tgt = SCREEN_W;
|
||||
if (g_cur_merged) gc_fill(' ', emph_to_attr(line_style, base), (uint8_t)(tgt_full - old_sc));
|
||||
seg_col = tgt; q++; prev_ch = ' '; continue;
|
||||
}
|
||||
if (ch == ' ') {
|
||||
if (g_cur_merged) gc_put(' ', emph_to_attr(line_style, base));
|
||||
last_space = q + 1;
|
||||
last_space_style = line_style;
|
||||
if (g_cur_merged) g_ncells_at_space = g_ncells;
|
||||
seg_col++; q++; prev_ch = ' ';
|
||||
} else {
|
||||
if (g_cur_merged) gc_put(ch, emph_to_attr(line_style, base));
|
||||
seg_col++; q++; prev_ch = ch;
|
||||
}
|
||||
if (seg_col >= SCREEN_W) {
|
||||
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 (g_cur_merged && 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_at: диапазон
|
||||
* [wrap_at, q_old) будет пересканирован заново, и переключающие
|
||||
* символы (обратная кавычка, звёздочка, подчёркивание, тильда)
|
||||
* внутри него иначе применились бы дважды — именно так возникал
|
||||
* баг: code-маркер на границе переноса ломает цвет следующего слова. */
|
||||
line_style = wrap_style;
|
||||
}
|
||||
}
|
||||
@@ -1073,15 +1125,32 @@ static uint32_t scan_join_stream(uint32_t q, uint32_t para_start, uint8_t col,
|
||||
}
|
||||
|
||||
q++;
|
||||
/* как render: пропускаем ведущие пробелы/табы строки-продолжения,
|
||||
* склейка даёт РОВНО один пробел (иначе merge эмитит отступ как лишние ячейки) */
|
||||
while (q < file_size && (fb(q) == ' ' || fb(q) == '\t')) q++;
|
||||
soft_break = 1;
|
||||
ch = ' ';
|
||||
}
|
||||
}
|
||||
|
||||
if (ch == '`') {
|
||||
if (line_style == INIT_STYLE_CODE) line_style = INIT_STYLE_PLAIN;
|
||||
else line_style = INIT_STYLE_CODE;
|
||||
if (!soft_break) q++;
|
||||
/* Как handle_inline_marker в render: backtick переключает code-режим
|
||||
* ТОЛЬКО из PLAIN или CODE; если активен эмфазис (bold/italic/...),
|
||||
* backtick — обычный литеральный символ (нет вложенного кода). */
|
||||
if (line_style == INIT_STYLE_PLAIN) {
|
||||
line_style = INIT_STYLE_CODE;
|
||||
if (!soft_break) q++;
|
||||
continue;
|
||||
}
|
||||
if (line_style == INIT_STYLE_CODE) {
|
||||
line_style = INIT_STYLE_PLAIN;
|
||||
if (!soft_break) q++;
|
||||
continue;
|
||||
}
|
||||
if (g_cur_merged) gc_put('`', styles_map[line_style]);
|
||||
seg_col++;
|
||||
if (!soft_break) q++; else q++;
|
||||
prev_ch = '`';
|
||||
continue;
|
||||
}
|
||||
if (ch == '*' && (q + 1) < file_size && fb(q + 1) == '*') {
|
||||
@@ -1097,6 +1166,7 @@ static uint32_t scan_join_stream(uint32_t q, uint32_t para_start, uint8_t col,
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (g_cur_merged) { gc_put('*', styles_map[line_style]); gc_put('*', styles_map[line_style]); }
|
||||
seg_col += 2;
|
||||
if (!soft_break) q += 2; else q++;
|
||||
prev_ch = '*';
|
||||
@@ -1115,6 +1185,7 @@ static uint32_t scan_join_stream(uint32_t q, uint32_t para_start, uint8_t col,
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (g_cur_merged) { gc_put('~', styles_map[line_style]); gc_put('~', styles_map[line_style]); }
|
||||
seg_col += 2;
|
||||
if (!soft_break) q += 2; else q++;
|
||||
prev_ch = '~';
|
||||
@@ -1128,26 +1199,33 @@ static uint32_t scan_join_stream(uint32_t q, uint32_t para_start, uint8_t col,
|
||||
line_style = new_style;
|
||||
if (!soft_break) q++; else q++;
|
||||
continue;
|
||||
} else if (line_style == new_style && ws_or_eol_or_delim(next_ch)) {
|
||||
} else if (line_style != INIT_STYLE_PLAIN && ws_or_eol_or_delim(next_ch)) {
|
||||
/* как render: одиночный маркер закрывает ЛЮБОЙ активный эмфазис */
|
||||
line_style = INIT_STYLE_PLAIN;
|
||||
if (!soft_break) q++; else q++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (g_cur_merged) gc_put(ch, styles_map[line_style]);
|
||||
seg_col++;
|
||||
if (!soft_break) q++; else q++;
|
||||
prev_ch = ch;
|
||||
continue;
|
||||
}
|
||||
if (ch == '\t') {
|
||||
uint8_t tgt = (uint8_t)((seg_col & (uint8_t)~(TAB_STOP - 1)) + TAB_STOP);
|
||||
uint8_t old_sc = seg_col;
|
||||
uint8_t tgt_full = (uint8_t)((seg_col & (uint8_t)~(TAB_STOP - 1)) + TAB_STOP);
|
||||
uint8_t tgt = tgt_full;
|
||||
if (tgt > SCREEN_W) tgt = SCREEN_W;
|
||||
/* render заполняет до НЕкапнутого tgt_full (по cc); для plain cc==seg_col */
|
||||
if (g_cur_merged) gc_fill(' ', styles_map[line_style], (uint8_t)(tgt_full - old_sc));
|
||||
seg_col = tgt;
|
||||
if (!soft_break) q++; else q++;
|
||||
prev_ch = ' ';
|
||||
continue;
|
||||
}
|
||||
if (ch == ' ') {
|
||||
if (g_cur_merged) gc_put(' ', styles_map[line_style]);
|
||||
if (soft_break) {
|
||||
last_space = q;
|
||||
last_space_style = line_style;
|
||||
@@ -1158,8 +1236,10 @@ static uint32_t scan_join_stream(uint32_t q, uint32_t para_start, uint8_t col,
|
||||
seg_col++;
|
||||
q++;
|
||||
}
|
||||
if (g_cur_merged) g_ncells_at_space = g_ncells; /* снимок для усечения */
|
||||
prev_ch = ' ';
|
||||
} else {
|
||||
if (g_cur_merged) gc_put(ch, styles_map[line_style]);
|
||||
seg_col++;
|
||||
if (!soft_break) q++; else q++;
|
||||
prev_ch = ch;
|
||||
@@ -1169,7 +1249,20 @@ static uint32_t scan_join_stream(uint32_t q, uint32_t para_start, uint8_t col,
|
||||
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 (g_cur_merged && use_ls) g_ncells = g_ncells_at_space; /* усечь до пробела */
|
||||
emit_seg(wrap_at, wrap_style, ckind, 1);
|
||||
/* Префикс continuation-сегмента (как render в cont-блоке): список —
|
||||
* col пробелов; цитата — отступ + маркер 0xB3 + пробел; plain — без префикса. */
|
||||
if (g_cur_merged && ckind == CK_LIST) {
|
||||
gc_fill(' ', ATTR_TEXT, col);
|
||||
} else if (g_cur_merged && 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 = ' ';
|
||||
@@ -1200,6 +1293,8 @@ static void index_lines(void)
|
||||
uint32_t p = 0;
|
||||
while (p < file_size && n_lines < max_lines) {
|
||||
|
||||
g_next_merged = 0; /* по умолчанию блок не мигрирован → render; мигрированные типы выставят 1 */
|
||||
|
||||
if ((n_lines & 15) == 0)
|
||||
spinner_tick();
|
||||
|
||||
@@ -1210,6 +1305,7 @@ static void index_lines(void)
|
||||
if (p < file_size) p++;
|
||||
/* Серия пустых строк схлопывается в одну визуальную строку. */
|
||||
if (n_lines == 0 || !(cur_rec.flags & IF_BLANK)) {
|
||||
g_next_merged = 1; /* S5: blank — пустой буфер (len 0) */
|
||||
emit_seg(p, INIT_STYLE_PLAIN, CK_PLAIN, 0);
|
||||
set_blank_cur();
|
||||
}
|
||||
@@ -1219,6 +1315,7 @@ static void index_lines(void)
|
||||
/* ---- fenced code-блок (граница или тело) ---- */
|
||||
if (is_fence_raw(p)) {
|
||||
in_block = !in_block;
|
||||
g_next_merged = 1; /* S5: граница ``` — пустой буфер (len 0) */
|
||||
emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0);
|
||||
set_nowrap_cur();
|
||||
while (p < file_size && fb(p) != '\n') p++;
|
||||
@@ -1226,10 +1323,28 @@ static void index_lines(void)
|
||||
continue;
|
||||
}
|
||||
if (in_block) {
|
||||
g_next_merged = 1; /* S5: тело кода — verbatim ATTR_TEXT_CODE */
|
||||
emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0);
|
||||
set_nowrap_cur();
|
||||
set_code_cur();
|
||||
set_hscroll_cur(); /* код скроллится целым блоком */
|
||||
if (g_cur_merged) {
|
||||
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++;
|
||||
}
|
||||
}
|
||||
while (p < file_size && fb(p) != '\n') p++;
|
||||
if (p < file_size) p++;
|
||||
continue;
|
||||
@@ -1237,8 +1352,10 @@ static void index_lines(void)
|
||||
|
||||
/* ---- горизонтальный разделитель ---- */
|
||||
if (is_hr_raw(p)) {
|
||||
g_next_merged = 1; /* S5: HR — 80×0xC4 ATTR_HR */
|
||||
emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0);
|
||||
set_nowrap_cur();
|
||||
if (g_cur_merged) gc_fill(0xC4, ATTR_HR, SCREEN_W);
|
||||
while (p < file_size && fb(p) != '\n') p++;
|
||||
if (p < file_size) p++;
|
||||
continue;
|
||||
@@ -1246,9 +1363,15 @@ static void index_lines(void)
|
||||
|
||||
/* ---- строка таблицы (| ... |): один nowrap-сегмент на исходную строку ---- */
|
||||
if (is_table_raw(p)) {
|
||||
g_next_merged = 1; /* S5: таблица — inline-парсинг без переноса */
|
||||
emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0);
|
||||
set_nowrap_cur();
|
||||
set_hscroll_cur(); /* таблица скроллится целым блоком */
|
||||
if (g_cur_merged) {
|
||||
uint32_t line_end = p;
|
||||
while (line_end < file_size && fb(line_end) != '\n') line_end++;
|
||||
(void)inline_scan(p, line_end, 0, CK_OTHER, INIT_STYLE_PLAIN, ATTR_TEXT, 1);
|
||||
}
|
||||
while (p < file_size && fb(p) != '\n') p++;
|
||||
if (p < file_size) p++;
|
||||
continue;
|
||||
@@ -1260,10 +1383,18 @@ static void index_lines(void)
|
||||
|
||||
/* ---- Заголовок: отдельная строка, не склеивается с параграфом ---- */
|
||||
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;
|
||||
}
|
||||
g_next_merged = 1; /* S2: заголовки строит merge */
|
||||
emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0);
|
||||
uint32_t line_end = content_off;
|
||||
while (line_end < file_size && fb(line_end) != '\n') line_end++;
|
||||
(void)inline_scan(content_off, line_end, 0, CK_OTHER, INIT_STYLE_PLAIN);
|
||||
(void)inline_scan(content_off, line_end, 0, CK_OTHER, INIT_STYLE_PLAIN, hbase, 0);
|
||||
while (p < file_size && fb(p) != '\n') p++;
|
||||
if (p < file_size) p++;
|
||||
continue;
|
||||
@@ -1275,7 +1406,23 @@ static void index_lines(void)
|
||||
uint8_t line_style = INIT_STYLE_PLAIN;
|
||||
uint32_t q = content_off;
|
||||
|
||||
g_next_merged = 1; /* S3: списки строит merge */
|
||||
emit_seg(p, line_style, CK_LIST, 0);
|
||||
/* Префикс ПЕРВОГО сегмента: отступ + маркер + пробел (как render). */
|
||||
if (g_cur_merged) {
|
||||
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);
|
||||
|
||||
/* Переходим к началу следующей физической строки после пункта. */
|
||||
@@ -1315,7 +1462,16 @@ static void index_lines(void)
|
||||
uint8_t line_style = INIT_STYLE_PLAIN;
|
||||
uint32_t q = content_off;
|
||||
|
||||
g_next_merged = 1; /* S4: цитаты строит merge */
|
||||
emit_seg(p, line_style, CK_QUOTE, 0);
|
||||
/* Префикс ПЕРВОГО сегмента: отступ + маркер 0xB3 + пробел (как render LK_QUOTE). */
|
||||
if (g_cur_merged) {
|
||||
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);
|
||||
|
||||
while (q < file_size && fb(q) != '\n') q++;
|
||||
@@ -1327,6 +1483,7 @@ static void index_lines(void)
|
||||
/* ---- Обычный текстовый параграф: склейка через soft-break ---- */
|
||||
{
|
||||
uint8_t line_style = INIT_STYLE_PLAIN;
|
||||
g_next_merged = 1; /* S1: параграфы строит merge */
|
||||
emit_seg(p, line_style, CK_PLAIN, 0);
|
||||
|
||||
uint32_t q = p;
|
||||
@@ -1344,12 +1501,10 @@ static void index_lines(void)
|
||||
}
|
||||
}
|
||||
}
|
||||
/* emit_seg() рендерит строки с отставанием на одну (см. её
|
||||
* комментарий) — последняя эмитированная строка файла так и
|
||||
* остаётся неотрендеренной, дорендериваем её здесь. seg_end для
|
||||
* неё в render_line_to_cache() берётся как file_size (нет
|
||||
* "следующей" idx-записи), как и при живом рендере в render_line(). */
|
||||
if (n_lines > 0) render_line_to_cache((uint16_t)(n_lines - 1));
|
||||
/* emit_seg() флашит ячейки с отставанием на одну строку (см. её
|
||||
* комментарий) — последняя эмитированная строка файла так и остаётся
|
||||
* незафлашенной, её ячейки ещё в g_cells; коммитим здесь. */
|
||||
if (n_lines > 0) seg_commit_cells((uint16_t)(n_lines - 1));
|
||||
}
|
||||
#define EM_NONE 0
|
||||
#define EM_BOLD 1
|
||||
@@ -1371,346 +1526,6 @@ static uint8_t emph_to_attr(uint8_t emph, uint8_t base_attr)
|
||||
}
|
||||
}
|
||||
|
||||
/* Единая обработка inline-маркеров в render_line().
|
||||
* Возврат:
|
||||
* 0 = маркер не поглощён (символ выводится как есть),
|
||||
* 1 = маркер поглощён как управляющий (ничего не рисуем),
|
||||
* 2 = литеральный двойной маркер (`**` или `~~`), нужно вывести две копии out_ch. */
|
||||
static uint8_t handle_inline_marker(char ch, uint32_t *io_p, uint32_t seg_end,
|
||||
char prev_ch, uint8_t *io_emph, char *out_ch)
|
||||
{
|
||||
uint32_t p = *io_p;
|
||||
|
||||
*out_ch = 0;
|
||||
|
||||
if (ch == '`') {
|
||||
if (*io_emph == EM_NONE) {
|
||||
*io_emph = EM_CODE;
|
||||
*io_p = p + 1;
|
||||
return 1;
|
||||
}
|
||||
if (*io_emph == EM_CODE) {
|
||||
*io_emph = EM_NONE;
|
||||
*io_p = p + 1;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ((ch == '*' || ch == '~') && (p + 1) < seg_end && fb(p + 1) == ch) {
|
||||
uint8_t pair_emph = (ch == '*') ? EM_BOLD : EM_STRIKE;
|
||||
char next_ch = ((p + 2) < seg_end) ? fb(p + 2) : '\n';
|
||||
|
||||
if (is_emph_flanked(prev_ch, next_ch)) {
|
||||
if (*io_emph == EM_NONE && ws_or_eol(prev_ch)) {
|
||||
*io_emph = pair_emph;
|
||||
*io_p = p + 2;
|
||||
return 1;
|
||||
} else if (*io_emph == pair_emph && ws_or_eol_or_delim(next_ch)) {
|
||||
*io_emph = EM_NONE;
|
||||
*io_p = p + 2;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Невалидный по границам двойной маркер отображаем как литерал. */
|
||||
*io_p = p + 2;
|
||||
*out_ch = ch;
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (ch == '*' || ch == '_') {
|
||||
char next_ch = ((p + 1) < seg_end) ? fb(p + 1) : '\n';
|
||||
if (is_emph_flanked(prev_ch, next_ch)) {
|
||||
uint8_t active = (ch == '*') ? EM_ITALIC : EM_UNDER;
|
||||
if (*io_emph == EM_NONE && ws_or_eol(prev_ch)) {
|
||||
*io_emph = active;
|
||||
*io_p = p + 1;
|
||||
return 1;
|
||||
} else if (*io_emph != EM_NONE && ws_or_eol_or_delim(next_ch)) {
|
||||
*io_emph = EM_NONE;
|
||||
*io_p = p + 1;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Пишет одну (char,attr) ячейку в локальный буфер кэш-строки. Молча
|
||||
* не пишет (и не даёт col перевалить за MAX_CACHE_LINE_LEN), если буфер
|
||||
* уже заполнен — длинные nowrap-строки просто обрезаются по канону,
|
||||
* как и раньше обрезались по SCREEN_W при живом рендере. */
|
||||
static void cc_put(uint8_t *cellbuf, uint8_t *col, char ch, uint8_t attr)
|
||||
{
|
||||
if (*col < MAX_CACHE_LINE_LEN) {
|
||||
uint16_t i = (uint16_t)(*col) * 2u;
|
||||
cellbuf[i] = (uint8_t)ch;
|
||||
cellbuf[i + 1] = attr;
|
||||
(*col)++;
|
||||
}
|
||||
}
|
||||
|
||||
static void cc_fill(uint8_t *cellbuf, uint8_t *col, char ch, uint8_t attr, uint8_t n)
|
||||
{
|
||||
while (n-- > 0) cc_put(cellbuf, col, ch, attr);
|
||||
}
|
||||
|
||||
/* Фаза 2 — рендер-воркер. Адаптация render_line(): та же классификация
|
||||
* строк/префиксов/inline-парсинг (handle_inline_marker, emphasis state
|
||||
* machine, soft-wrap join) БЕЗ ИЗМЕНЕНИЙ, но результат идёт не на экран
|
||||
* (bios_set_place/wrchar/bios_fillcharattr/flush_run), а в локальный
|
||||
* буфер cellbuf через cc_put/cc_fill, и в конце КОММИТИТСЯ одним
|
||||
* cache_commit()+cache_dir_put(). Буферизация через runbuf/flush_run
|
||||
* (нужная в render_line() только чтобы минимизировать число BIOS-
|
||||
* вызовов) здесь не нужна — пишем посимвольно прямо по месту.
|
||||
*
|
||||
* Кэшируется ПОЛНЫЙ контент строки до MAX_CACHE_LINE_LEN ячеек, БЕЗ
|
||||
* обрезки по SCREEN_W и без среза по viewport_x — это view/scroll-time
|
||||
* понятия (Фаза 4-5: h-скролл — это смещение начала среза в ТОМ ЖЕ
|
||||
* кэш-буфере, не повторный рендер). По той же причине здесь нет
|
||||
* truncation-индикаторов '>'/'<' и финального заполнения остатка
|
||||
* строки пробелами до SCREEN_W — оба добавляются на этапе вывода. */
|
||||
static void render_line_to_cache(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;
|
||||
|
||||
uint8_t have_rec = (line_idx < n_lines);
|
||||
idx_rec_t rec;
|
||||
if (have_rec) idx_get(line_idx, &rec);
|
||||
|
||||
if (have_rec) out_rec.flags = rec.flags;
|
||||
|
||||
if (!have_rec || (rec.flags & IF_BLANK)) {
|
||||
out_rec.page = 0;
|
||||
out_rec.off = 0;
|
||||
out_rec.len = 0;
|
||||
cache_dir_put(line_idx, &out_rec);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t col = 0; /* позиция в cellbuf, она же длина уже записанного контента */
|
||||
uint8_t base_attr = ATTR_TEXT;
|
||||
uint8_t is_code = (uint8_t)(rec.flags & IF_CODE);
|
||||
uint8_t cur_attr = is_code ? ATTR_TEXT_CODE
|
||||
: styles_map[rec.style & 7u];
|
||||
uint8_t emph = is_code ? EM_CODE : (uint8_t)(rec.style & 7u);
|
||||
uint8_t parse_inline = 1;
|
||||
|
||||
uint8_t cellbuf[MAX_CACHE_LINE_LEN * 2];
|
||||
|
||||
uint32_t p = rec.off;
|
||||
uint32_t content_off = p;
|
||||
uint8_t cont = (uint8_t)((rec.flags & IF_CONT) != 0);
|
||||
uint32_t seg_end;
|
||||
if ((uint16_t)(line_idx + 1) < n_lines) {
|
||||
idx_rec_t rec_next;
|
||||
idx_get((uint16_t)(line_idx + 1), &rec_next);
|
||||
seg_end = rec_next.off;
|
||||
} else {
|
||||
seg_end = file_size;
|
||||
}
|
||||
if (seg_end > file_size) seg_end = file_size;
|
||||
uint8_t kind;
|
||||
uint8_t quote_stream = 0;
|
||||
|
||||
if (cont) {
|
||||
uint16_t first = line_idx;
|
||||
idx_rec_t rec_first = rec;
|
||||
while (first > 0 && (rec_first.flags & IF_CONT)) {
|
||||
first--;
|
||||
idx_get(first, &rec_first);
|
||||
}
|
||||
uint32_t p_start = rec_first.off;
|
||||
uint32_t content_off2 = p_start;
|
||||
uint8_t first_kind = classify_line(p_start, &content_off2);
|
||||
uint8_t indent = marker_visible_col(first_kind, p_start, content_off2);
|
||||
if (first_kind == LK_QUOTE) {
|
||||
quote_stream = 1;
|
||||
uint8_t q = 0;
|
||||
while (q + 2 < content_off2 && q < SCREEN_W && fb(p_start + q) == ' ') q++;
|
||||
cc_fill(cellbuf, &col, ' ', ATTR_TEXT, q);
|
||||
cc_put(cellbuf, &col, 0xB3, ATTR_QUOTE_MARKER);
|
||||
cc_put(cellbuf, &col, ' ', ATTR_TEXT);
|
||||
} else if (first_kind == LK_ULIST || first_kind == LK_OLIST) {
|
||||
cc_fill(cellbuf, &col, ' ', ATTR_TEXT, indent);
|
||||
}
|
||||
}
|
||||
|
||||
/* Разделитель fenced-блока — пустая строка в кэше (как IF_BLANK).
|
||||
* is_fence_raw пропускает ведущие пробелы, поэтому отступленный ``` тоже
|
||||
* распознаётся (и язык ```c прячется вместе со всей строкой-границей). */
|
||||
if (!cont && is_fence_raw(p)) {
|
||||
out_rec.page = 0;
|
||||
out_rec.off = 0;
|
||||
out_rec.len = 0;
|
||||
cache_dir_put(line_idx, &out_rec);
|
||||
return;
|
||||
}
|
||||
if (is_code) {
|
||||
base_attr = ATTR_TEXT_CODE;
|
||||
cur_attr = ATTR_TEXT_CODE;
|
||||
parse_inline = 0;
|
||||
kind = LK_PLAIN;
|
||||
quote_stream = 0;
|
||||
} else if (cont) {
|
||||
kind = LK_PLAIN;
|
||||
} else {
|
||||
kind = classify_line(p, &content_off);
|
||||
if (kind == LK_QUOTE) quote_stream = 1;
|
||||
}
|
||||
|
||||
if (kind == LK_HR) {
|
||||
cc_fill(cellbuf, &col, 0xC4, ATTR_HR, SCREEN_W);
|
||||
out_rec.len = col;
|
||||
if (cache_reserve((uint16_t)(col * 2u), &out_rec.page, &out_rec.off)) {
|
||||
cache_commit(out_rec.page, out_rec.off, cellbuf, (uint16_t)(col * 2u));
|
||||
} else {
|
||||
out_rec.page = 0; out_rec.off = 0; out_rec.len = 0;
|
||||
}
|
||||
cache_dir_put(line_idx, &out_rec);
|
||||
return;
|
||||
}
|
||||
if (kind >= LK_H1 && kind <= LK_H4) {
|
||||
switch (kind) {
|
||||
case LK_H1: base_attr = ATTR_TEXT_TITLE1; break;
|
||||
case LK_H2: base_attr = ATTR_TEXT_TITLE2; break;
|
||||
case LK_H3: base_attr = ATTR_TEXT_TITLE3; break;
|
||||
default: base_attr = ATTR_TEXT_TITLE4; break;
|
||||
}
|
||||
cur_attr = base_attr;
|
||||
p = content_off;
|
||||
} else if (kind == LK_ULIST) {
|
||||
uint32_t q = p;
|
||||
uint32_t qmax = p + SCREEN_W;
|
||||
while (q + 2 < content_off && q < qmax && fb(q) == ' ') q++;
|
||||
cc_fill(cellbuf, &col, ' ', ATTR_TEXT, (uint8_t)(q - p));
|
||||
cc_put(cellbuf, &col, 0x07, ATTR_LIST_MARKER);
|
||||
cc_put(cellbuf, &col, ' ', ATTR_TEXT);
|
||||
p = content_off;
|
||||
} else if (kind == LK_OLIST) {
|
||||
uint32_t q = p;
|
||||
uint32_t qmax = p + SCREEN_W;
|
||||
while (q < content_off && q < qmax && fb(q) == ' ') q++;
|
||||
cc_fill(cellbuf, &col, ' ', ATTR_TEXT, (uint8_t)(q - p));
|
||||
/* Маркер (цифры/'.'/')') читаем прямо из fb() — здесь, в отличие
|
||||
* от render_line(), нет BIOS-вызова с требованием адреса в
|
||||
* #4000-#BFFF, так что промежуточный mkbuf-чанкинг не нужен. */
|
||||
while (q < content_off - 1) {
|
||||
cc_put(cellbuf, &col, fb(q), ATTR_LIST_MARKER);
|
||||
q++;
|
||||
}
|
||||
cc_put(cellbuf, &col, ' ', ATTR_TEXT);
|
||||
p = content_off;
|
||||
} else if (kind == LK_QUOTE) {
|
||||
uint32_t q = p;
|
||||
uint32_t qmax = p + SCREEN_W;
|
||||
while (q + 1 < content_off && q < qmax && fb(q) == ' ') q++;
|
||||
cc_fill(cellbuf, &col, ' ', ATTR_TEXT, (uint8_t)(q - p));
|
||||
cc_put(cellbuf, &col, 0xB3, ATTR_QUOTE_MARKER);
|
||||
cc_put(cellbuf, &col, ' ', ATTR_TEXT);
|
||||
p = content_off;
|
||||
}
|
||||
|
||||
uint8_t cc = 0; /* позиция внутри контента (после префикса), только для табов */
|
||||
char prev_ch = ' ';
|
||||
|
||||
while (p < seg_end && col < MAX_CACHE_LINE_LEN) {
|
||||
uint8_t soft_join = 0;
|
||||
char ch = fb(p);
|
||||
|
||||
if (parse_inline && ch == '\\' && (p + 1) < seg_end) {
|
||||
char nb = fb(p + 1);
|
||||
if (nb == '\n' || nb == '\r') { p++; continue; }
|
||||
}
|
||||
if (parse_inline && (ch == '\n' || ch == '\r')) {
|
||||
uint32_t next = p + 1;
|
||||
if (quote_stream && next < seg_end) {
|
||||
uint32_t next_content = next;
|
||||
if (classify_line(next, &next_content) == LK_QUOTE) {
|
||||
p = next_content;
|
||||
ch = ' ';
|
||||
soft_join = 1;
|
||||
} else {
|
||||
while (next < seg_end) {
|
||||
char ws = fb(next);
|
||||
if (ws == ' ' || ws == '\t') next++;
|
||||
else break;
|
||||
}
|
||||
if (next > (p + 1)) {
|
||||
p = next;
|
||||
soft_join = 1;
|
||||
}
|
||||
ch = ' ';
|
||||
}
|
||||
} else {
|
||||
while (next < seg_end) {
|
||||
char ws = fb(next);
|
||||
if (ws == ' ' || ws == '\t') next++;
|
||||
else break;
|
||||
}
|
||||
if (next > (p + 1)) {
|
||||
p = next;
|
||||
soft_join = 1;
|
||||
}
|
||||
ch = ' ';
|
||||
}
|
||||
} else if (ch == '\n' || ch == '\r') {
|
||||
ch = ' ';
|
||||
}
|
||||
|
||||
/* handle_inline_marker реагирует только на ` * _ ~ — для остальных
|
||||
* символов (подавляющее большинство) она возвращает 0 без side-эффектов.
|
||||
* Пропускаем вызов функции (6 аргументов) на горячем пути обычного текста. */
|
||||
if (parse_inline && (ch == '`' || ch == '*' || ch == '_' || ch == '~')) {
|
||||
char lit_ch = 0;
|
||||
uint8_t mk = handle_inline_marker(ch, &p, seg_end, prev_ch, &emph, &lit_ch);
|
||||
if (mk == 1) {
|
||||
cur_attr = emph_to_attr(emph, base_attr);
|
||||
continue;
|
||||
}
|
||||
if (mk == 2) {
|
||||
cc_put(cellbuf, &col, lit_ch, cur_attr);
|
||||
cc++;
|
||||
cc_put(cellbuf, &col, lit_ch, cur_attr);
|
||||
cc++;
|
||||
prev_ch = lit_ch;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!soft_join) p++;
|
||||
if (ch == '\t') {
|
||||
uint8_t cc_start = cc;
|
||||
uint8_t tgt = (uint8_t)((cc & (uint8_t)~(TAB_STOP - 1)) + TAB_STOP);
|
||||
uint8_t want = (uint8_t)(tgt - cc_start);
|
||||
cc_fill(cellbuf, &col, ' ', cur_attr, want);
|
||||
cc = tgt;
|
||||
prev_ch = ' ';
|
||||
} else {
|
||||
/* инлайн cc_put: col < MAX_CACHE_LINE_LEN гарантирован условием while */
|
||||
uint16_t ci = (uint16_t)col * 2u;
|
||||
cellbuf[ci] = (uint8_t)ch;
|
||||
cellbuf[ci + 1] = cur_attr;
|
||||
col++;
|
||||
cc++;
|
||||
prev_ch = ch;
|
||||
}
|
||||
}
|
||||
|
||||
out_rec.len = col;
|
||||
if (col > 0 && cache_reserve((uint16_t)(col * 2u), &out_rec.page, &out_rec.off)) {
|
||||
cache_commit(out_rec.page, out_rec.off, cellbuf, (uint16_t)(col * 2u));
|
||||
} else {
|
||||
out_rec.page = 0; out_rec.off = 0; out_rec.len = 0;
|
||||
}
|
||||
cache_dir_put(line_idx, &out_rec);
|
||||
}
|
||||
|
||||
/* Фаза 4-5 — cache-only draw path: рисует одну строку ИЗ КЭША, без
|
||||
* единого обращения к fb()/исходному файлу. Горизонтальный скролл для
|
||||
|
||||
Reference in New Issue
Block a user