mdview2: расщепление монолита на модули (6 файлов, размер-нейтрально)
Вынос подсистем из mdview2.c в отдельные C-модули для читаемости и навигации. mdview2.c: 2740 → 1062 строк (-61%); ядро теперь чистая инфраструктура (cache-пул, EMM/fb, загрузка файла, doc-slot, loading-loop, build_doc, main). Модули (через EXTRA_SRCS, общий интерфейс в mdview2.h): - mdview2_help.c — диалог справки F1 - mdview2_table.c — выровненная отрисовка таблиц - mdview2_enc.c — кодировки CP866/CP1251/KOI8R + UTF-8 конвертер - mdview2_view.c — отрисовка области/статус-бара/меню + прокрутка - mdview2_index.c — парсер/индексатор markdown (сердце приложения) (mdview2_raw.c был выделен ранее) Чистый перенос static→extern; приватное состояние подсистем осталось приватным. Размер: exe 28090 → 28112 (+22 Б / +0.08% — codegen-шум на двух сильно связанных модулях enc/index). Сборка чистая, smoke-тест ОК. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,979 @@
|
||||
/*
|
||||
* 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-сканере) */
|
||||
|
||||
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 = 0; /* смещение последнего 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;
|
||||
}
|
||||
|
||||
/* Установка флагов всегда идёт в последний emit_seg (n_lines-1).
|
||||
* Используется near-копия cur_rec, чтобы не делать обратное чтение из банка. */
|
||||
static void set_nowrap_cur(void) { cur_rec.flags |= IF_NOWRAP; idx_put((uint16_t)(n_lines - 1), &cur_rec); }
|
||||
static void set_blank_cur(void) { cur_rec.flags |= IF_BLANK; idx_put((uint16_t)(n_lines - 1), &cur_rec); }
|
||||
static void set_code_cur(void) { cur_rec.flags |= IF_CODE; idx_put((uint16_t)(n_lines - 1), &cur_rec); }
|
||||
static void set_hscroll_cur(void){ cur_rec.flags |= IF_HSCROLL; 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 (c0 == '-' || c0 == '*' || c0 == '_') {
|
||||
char marker = 0;
|
||||
uint8_t count = 0;
|
||||
uint8_t ok = 1;
|
||||
uint32_t p = p_start;
|
||||
while (p < file_size) {
|
||||
char ch = fb(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++;
|
||||
}
|
||||
if (ok && count >= 3) 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 = 0; /* ячеек в текущем сегменте (общий с таблицами) */
|
||||
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, молча игнорируя переполнение
|
||||
* 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 {
|
||||
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; 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);
|
||||
|
||||
if (ls != INIT_STYLE_CODE && ch == '\\' && (q + 1) < bound && is_escapable(fb(q + 1))) {
|
||||
char nb = fb(q + 1);
|
||||
gc_put(nb, emph_to_attr(ls, base));
|
||||
*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('`', emph_to_attr(ls, base)); *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('*', emph_to_attr(ls, base)); gc_put('*', emph_to_attr(ls, base));
|
||||
*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('~', emph_to_attr(ls, base)); gc_put('~', emph_to_attr(ls, base));
|
||||
*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, emph_to_attr(ls, base)); *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(' ', emph_to_attr(ls, base), (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 = next_content;
|
||||
while (next_line_end < file_size && fb(next_line_end) != '\n') next_line_end++;
|
||||
|
||||
{
|
||||
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;
|
||||
cur_seg_off = 0xFFFFFFFFu; /* пока не эмитили ни одного сегмента */
|
||||
if (file_size == 0) return;
|
||||
|
||||
uint32_t p = 0;
|
||||
while (p < file_size && n_lines < max_lines && !g_abort) {
|
||||
|
||||
/* 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)) {
|
||||
while (p < file_size && fb(p) != '\n') p++;
|
||||
if (p < file_size) p++;
|
||||
/* Серия пустых строк схлопывается в одну визуальную строку. */
|
||||
if (n_lines == 0 || !(cur_rec.flags & IF_BLANK)) {
|
||||
emit_seg(p, INIT_STYLE_PLAIN, CK_PLAIN, 0);
|
||||
set_blank_cur();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
/* ---- fenced code-блок (граница или тело) ---- */
|
||||
if (is_fence_raw(p)) {
|
||||
in_block = !in_block;
|
||||
emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0);
|
||||
set_nowrap_cur();
|
||||
while (p < file_size && fb(p) != '\n') p++;
|
||||
if (p < file_size) p++;
|
||||
continue;
|
||||
}
|
||||
if (in_block) {
|
||||
emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0);
|
||||
set_nowrap_cur();
|
||||
set_code_cur();
|
||||
set_hscroll_cur(); /* код скроллится целым блоком */
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/* ---- горизонтальный разделитель ---- */
|
||||
if (is_hr_raw(p)) {
|
||||
emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0);
|
||||
set_nowrap_cur();
|
||||
gc_fill(0xC4, ATTR_HR, SCREEN_W);
|
||||
while (p < file_size && fb(p) != '\n') p++;
|
||||
if (p < file_size) 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_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;
|
||||
}
|
||||
|
||||
/* ---- классификация текущей строки ---- */
|
||||
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 = 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, hbase, 0);
|
||||
while (p < file_size && fb(p) != '\n') p++;
|
||||
if (p < file_size) 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);
|
||||
|
||||
/* Переходим к началу следующей физической строки после пункта. */
|
||||
if (q < file_size) {
|
||||
while (q < file_size && fb(q) != '\n') q++;
|
||||
if (q < file_size) 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)) {
|
||||
while (t < file_size && fb(t) != '\n') t++;
|
||||
if (t < file_size) 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);
|
||||
|
||||
while (q < file_size && fb(q) != '\n') q++;
|
||||
if (q < file_size) q++;
|
||||
p = 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 параграфа.
|
||||
* Переходим на следующую строку, чтобы внешняя петля её
|
||||
* корректно переклассифицировала. */
|
||||
while (q < file_size && fb(q) != '\n') q++;
|
||||
if (q < file_size) q++;
|
||||
p = q;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* emit_seg() флашит ячейки с отставанием на одну строку (см. её
|
||||
* комментарий) — последняя эмитированная строка файла так и остаётся
|
||||
* незафлашенной, её ячейки ещё в g_cells; коммитим здесь. */
|
||||
if (n_lines > 0) seg_commit_cells((uint16_t)(n_lines - 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user