From c0dd1621e64248154c66dd398da14ee42c3df5b4 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Fri, 26 Jun 2026 10:53:04 +0300 Subject: [PATCH] =?UTF-8?q?mdview2:=20=D1=80=D0=B0=D1=81=D1=89=D0=B5=D0=BF?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BC=D0=BE=D0=BD=D0=BE=D0=BB?= =?UTF-8?q?=D0=B8=D1=82=D0=B0=20=D0=BD=D0=B0=20=D0=BC=D0=BE=D0=B4=D1=83?= =?UTF-8?q?=D0=BB=D0=B8=20(6=20=D1=84=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2,=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B7=D0=BC=D0=B5=D1=80-=D0=BD=D0=B5=D0=B9=D1=82?= =?UTF-8?q?=D1=80=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Вынос подсистем из 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 --- examples/mdview2/Makefile | 2 +- examples/mdview2/mdview2.c | 1768 +----------------------------- examples/mdview2/mdview2.h | 162 +++ examples/mdview2/mdview2_enc.c | 295 +++++ examples/mdview2/mdview2_help.c | 102 ++ examples/mdview2/mdview2_index.c | 979 +++++++++++++++++ examples/mdview2/mdview2_table.c | 91 ++ examples/mdview2/mdview2_view.c | 246 +++++ 8 files changed, 1921 insertions(+), 1724 deletions(-) create mode 100644 examples/mdview2/mdview2_enc.c create mode 100644 examples/mdview2/mdview2_help.c create mode 100644 examples/mdview2/mdview2_index.c create mode 100644 examples/mdview2/mdview2_table.c create mode 100644 examples/mdview2/mdview2_view.c diff --git a/examples/mdview2/Makefile b/examples/mdview2/Makefile index d119cf1..8bb366d 100644 --- a/examples/mdview2/Makefile +++ b/examples/mdview2/Makefile @@ -8,7 +8,7 @@ PROJ_ROOT := $(abspath $(CURDIR)/../..) EXAMPLE := mdview2 -EXTRA_SRCS := mdview2_raw.c +EXTRA_SRCS := mdview2_raw.c mdview2_help.c mdview2_table.c mdview2_enc.c mdview2_view.c mdview2_index.c MEMORY := small include $(PROJ_ROOT)/app.mk diff --git a/examples/mdview2/mdview2.c b/examples/mdview2/mdview2.c index 0d2f881..f789822 100644 --- a/examples/mdview2/mdview2.c +++ b/examples/mdview2/mdview2.c @@ -53,14 +53,8 @@ static const uint8_t md_pallete[16 * 4] = { 0xFF, 0xFF, 0xFF, 0xFF }; -/* Атрибуты экрана/справки и геометрия — в mdview2.h (общие с модулем RAW). */ -#define ATTR_HELP_HINT COLOR(COLOR_YELLOW, COLOR_BLACK) - -/* Геометрия диалога справки (в символьных координатах 80×32). */ -#define HELP_X 8u /* левая граница рамки */ -#define HELP_Y 4u /* верхняя граница рамки */ -#define HELP_W 64u /* ширина рамки (включая │) */ -#define HELP_H 26u /* высота рамки (включая ─) */ +/* Атрибуты экрана/справки и геометрия — в mdview2.h (общие с модулем RAW). + * Диалог справки (F1) вынесен в модуль mdview2_help.c. */ /* ---- Глобальное состояние ---------------------------------------- */ @@ -84,25 +78,10 @@ static const uint8_t md_pallete[16 * 4] = { * и перезаписывать друг друга. См. заметку memory/sdcc_static_storage_gotcha. */ /* Компактная запись индекса сегмента. * Каждый элемент соответствует одной строке рендера в viewport. */ -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 -#define INDEX_RECS_PER_PAGE 2048u /* 16384 / 8; запись всегда целиком в одной странице */ +/* idx_rec_t + INDEX_REC_SIZE — в mdview2_index.c; INDEX_RECS_PER_PAGE — в mdview2.h. */ #define MAX_INDEX_PAGES 8u /* максимум 8 * 2048 = 16384 сегментов индекса */ -#define IF_CONT 0x01u /* сегмент является продолжением перенесённой строки */ -#define IF_NOWRAP 0x02u /* строка не переносится (кодовый блок / HR / таблица) */ -#define IF_BLANK 0x04u /* визуально пустая строка */ -#define IF_CODE 0x08u /* тело fenced code-блока (verbatim-режим) */ -#define IF_HSCROLL 0x10u /* блок горизонтально скроллируется ЦЕЛИКОМ (код/таблица), - * включая строки короче 80 — двигаются как единый блок. - * НЕ ставится на HR и границы fence (им нечего прятать). */ +/* Флаги IF_* сегмента — в mdview2.h (общие с индексатором/кэшем/вью). */ /* ================================================================== * Фаза 1 (mdview2): директория и пул рендер-кэша @@ -125,29 +104,12 @@ typedef struct idx_rec_s { * 255 (патологически длинные nowrap-строки урезаются; индикатор обрезки * добавляется на этапе вывода из кэша, не при рендере в кэш). */ -/* РОВНО 8 байт — критично: cache_dir_get/put адресуют запись сдвигом - * idx<<3 (умножение на 8 через сдвиг, а не на sizeof()), так что - * структура ОБЯЗАНА быть именно 8 байт, а не "8 минус случайный паддинг - * компилятора". (СSDCC z80 НЕ паддит структуры сама по себе, поэтому - * 1+2+1+1+1 = 6 байт без явных pad-полей превратились бы в 6, а не 8 — - * фактический баг был в том, что без 2 явных pad оно было 7, а - * CACHE_DIR_REC_SIZE был захардкожен как 8: bank_read/bank_write писали - * 8 байт в 7-байтный буфер на стеке, затирая соседнюю переменную - * вызывающей функции — это была настоящая причина "квирка WINREST", - * не компилятор/эмулятор/DSS, см. memory/sprinter_winrest_format.md.) */ -typedef struct cache_rec_s { - uint8_t page; /* EMM-страница рендер-кэша (физический номер) */ - uint16_t off; /* смещение в странице, 0..16383 (байты, не ячейки) */ - uint8_t len; /* длина контента в ЯЧЕЙКАХ (char,attr пар), 0..255; 0 = пустая/HR строка без сохранённого контента */ - uint8_t flags; /* копия IF_NOWRAP/IF_CODE/IF_BLANK на момент рендера */ - uint8_t reserved; /* не используется (раньше — attr для типа 2, см. план) */ - uint8_t pad[2]; /* явный резерв, добивка до 8 байт — см. комментарий выше */ -} cache_rec_t; +/* cache_rec_t (РОВНО 8 байт, см. sprinter_winrest_format) — в mdview2.h. */ #define CACHE_DIR_REC_SIZE ((uint16_t)sizeof(cache_rec_t)) #define CACHE_DIR_RECS_PER_PAGE 2048u #define MAX_CACHE_DIR_PAGES 8u -#define MAX_CACHE_LINE_LEN 255u /* кап длины контента одной строки, в ячейках (char,attr пара = 2 байта) */ +/* MAX_CACHE_LINE_LEN — в mdview2.h (буфер ячеек g_cells живёт в mdview2_index.c). */ #define MAX_CACHE_CONTENT_PAGES 64u /* запас: до 1 МБ кэша контента */ static uint8_t cache_dir_blk = 0; @@ -161,14 +123,14 @@ static uint8_t cache_cur_page_idx = 0; /* индекс текущ static uint16_t cache_cur_off = 0; /* текущее смещение записи в этой странице */ /* Доступ к директории рендер-кэша — тот же приём, что у idx_get/idx_put. */ -static void cache_dir_get(uint16_t idx, cache_rec_t *r) +void cache_dir_get(uint16_t idx, cache_rec_t *r) { uint8_t page = (uint8_t)(idx >> 11); uint16_t off = (uint16_t)((idx & 2047u) << 3); bank_read(cache_dir_phys[page], off, r, CACHE_DIR_REC_SIZE); } -static void cache_dir_put(uint16_t idx, const cache_rec_t *r) +void cache_dir_put(uint16_t idx, const cache_rec_t *r) { uint8_t page = (uint8_t)(idx >> 11); uint16_t off = (uint16_t)((idx & 2047u) << 3); @@ -181,7 +143,7 @@ static void cache_dir_put(uint16_t idx, const cache_rec_t *r) * жертвуя остатком (проще и безопаснее, чем page-crossing в fb()). * При нехватке текущего пула — лениво доращиваем его на 1 страницу. * Возврат: 1 — успех (out_page и out_off заполнены), 0 — EMM кончилась. */ -static uint8_t cache_reserve(uint16_t nbytes, uint8_t *out_page, uint16_t *out_off) +uint8_t cache_reserve(uint16_t nbytes, uint8_t *out_page, uint16_t *out_off) { if (nbytes == 0 || nbytes > PAGE_SIZE) return 0; @@ -214,82 +176,60 @@ static uint8_t cache_reserve(uint16_t nbytes, uint8_t *out_page, uint16_t *out_o * на строку, а не по байту. len — РОВНО как у cache_reserve()/bank_write * — uint16_t, не uint8_t: строка до MAX_CACHE_LINE_LEN=255 ячеек даёт * до 510 байт (char,attr), это уже не лезет в uint8_t. */ -static void cache_commit(uint8_t page, uint16_t off, const void *buf, uint16_t len) +void cache_commit(uint8_t page, uint16_t off, const void *buf, uint16_t len) { bank_write(page, off, buf, len); } /* emph_to_attr определена ниже (рядом с inline-логикой), нужна в inline_scan. */ -static uint8_t emph_to_attr(uint8_t emph, uint8_t base_attr); +/* emph_to_attr — в mdview2_index.c. */ -static uint16_t n_lines = 0; -static uint16_t max_lines = 0; /* ёмкость индекса: index_pages * 2048 */ -static uint16_t top_line = 0; +uint16_t n_lines = 0; /* extern: вью читает */ +uint16_t max_lines = 0; /* ёмкость индекса: index_pages * 2048 */ +uint16_t top_line = 0; /* extern: вью/скролл */ uint32_t file_size = 0; static uint8_t file_blk = 0; static uint8_t file_pages = 0; static uint8_t file_phys[MAX_PAGES] = {0}; -static uint8_t index_blk = 0; -static uint8_t index_pages = 0; -static uint8_t index_phys[MAX_INDEX_PAGES] = {0}; -static uint8_t index_truncated = 0; /* выставляется при исчерпании ёмкости индекса */ -static uint32_t cur_seg_off = 0; /* смещение последнего emit_seg (кэш в near-памяти) */ -static idx_rec_t cur_rec; /* near-копия последней записанной idx-записи */ -static uint8_t cur_page = 0xFF; /* 0xFF = в W3 ещё не отображена ни одна страница */ -static uint8_t viewport_x = 0; /* горизонтальный сдвиг для nowrap-строк */ -static char filename[64] = {0}; +uint8_t index_blk = 0; +uint8_t index_pages = 0; +uint8_t index_phys[MAX_INDEX_PAGES] = {0}; +uint8_t index_truncated = 0; /* выставляется при исчерпании ёмкости индекса */ +/* cur_seg_off/cur_rec (near-состояние индексатора) — в mdview2_index.c. */ +uint8_t cur_page = 0xFF; /* 0xFF = в W3 ещё не показана ни одна страница (extern: кодек инвалидирует) */ +uint8_t viewport_x = 0; /* горизонтальный сдвиг для nowrap-строк (extern: вью) */ +char filename[64] = {0}; /* extern: статус-бар */ /* Прогрессивная загрузка: рисуем первый экран и пускаем скролл, пока * index_lines() ещё досканирует файл (кооперативно, без прерываний). */ #if WITH_RAW uint8_t g_view = VIEW_MD; /* режим просмотра (общий с mdview2_raw.c) */ -static uint8_t g_ready = 0; /* 1 когда первичный документ построен — F2 (RAW) рабочий */ +uint8_t g_ready = 0; /* 1 когда первичный документ построен — F2 (RAW) рабочий (extern: вью) */ static uint8_t g_raw_last = VIEW_RAW_WRAP; /* последний под-режим RAW (возврат по F2) */ #endif -static uint8_t g_loading = 0; /* 1 во время index_lines() */ +uint8_t g_loading = 0; /* 1 во время index_lines() (extern: вью) */ static uint8_t g_first_drawn = 0; /* первый экран уже нарисован */ -static uint8_t g_f8_enabled = 0; /* можно ли сейчас переключать кодировку (метка F8 в меню + F8 в load_key) */ +uint8_t g_f8_enabled = 0; /* можно ли сейчас переключать кодировку (extern: вью-меню) */ static uint8_t g_utf_failed = 0; /* UTF-8 набор не удалось построить (нет EMM) — пропускаем UTF-8 в цикле F8 */ -static uint8_t g_abort = 0; /* F10/Esc во время загрузки → прервать индексацию */ +uint8_t g_abort = 0; /* F10/Esc во время загрузки → прервать индексацию */ static uint16_t g_last_prog = 0; /* n_lines на последнем обновлении статуса */ -static void progress_tick(void); /* определена ниже (нужна в index_lines) */ +/* progress_tick — объявлена в mdview2.h (определена ниже, зовётся из index_lines). */ -/* Кодировки. 8-битные (CP866/CP1251/KOI8R) — общая структура, различаются - * только ремапом глифов [128-255] на этапе отрисовки; UTF-8 — отдельный набор. */ -#define ENC_CP866 0 -#define ENC_CP1251 1 -#define ENC_KOI8R 2 -#define ENC_UTF8 3 -static uint8_t g_encoding = ENC_CP866; /* активная кодировка */ +/* Кодировки (ENC_*) и g_encoding — в mdview2.h / mdview2_enc.c. */ const uint8_t *g_remap = 0; /* активная таблица ремапа (CP1251/KOI8), 0 = без ремапа */ uint8_t g_scratch_phys = 0; /* EMM-страница под ремап-буфер строки для win_rest */ -/* UTF-8 набор (Фаза 2): src декодируется в отдельный блок EMM-страниц как - * чистый CP866. После конвертации активный буфер файла (file_phys/file_size/ - * file_pages, который читает fb()) перенаправляется на эти страницы, и весь - * существующий конвейер индексации/кэша работает по ним без изменений — - * конвертированный поток это уже CP866, ремап на отрисовке не нужен. */ -static uint8_t utf_blk = 0; -static uint8_t utf_pages = 0; -static uint8_t utf_phys[MAX_PAGES] = {0}; -static uint32_t utf_size = 0; -static uint8_t utf_avail = 0; /* 1 = страницы UTF-8 выделены */ - -/* Инкрементальная конвертация UTF-8→CP866: идёт чанками, чередуясь с - * индексацией (index_lines докручивает конвертацию впереди позиции чтения), - * чтобы первый экран появлялся быстро, а не после полного прохода. */ -static uint8_t g_utf_building = 0; /* 1 пока конвертация не дошла до конца исходника */ -static uint32_t utf_src = 0; /* позиция чтения ОРИГИНАЛА (utf-8) конвертером */ -static uint8_t cv_page = 0xFF;/* orig-страница в W3 для cv_read (своя от cur_page) */ -#define CONV_MARGIN 4096u /* на сколько байт держать конвертацию впереди индексатора */ -static void utf_convert_more(uint32_t target); /* forward (нужна в index_lines) */ +/* UTF-8 набор (utf_blk/utf_pages/utf_phys/utf_size/utf_avail, конвертер и его + * состояние) — в mdview2_enc.c; разделяемые символы и API — в mdview2.h. + * build_doc() ниже перенаправляет живой file_* на utf_phys и оркеструет + * инкрементальную конвертацию (utf_conv_reset/utf_convert_more). */ /* Снимок исходного (8-битного) файлового буфера — набор DOC_8BIT читает его * как есть, набор DOC_UTF8 читает utf_phys. */ static uint8_t orig_file_blk = 0; -static uint8_t orig_file_pages = 0; -static uint32_t orig_file_size = 0; -static uint8_t orig_file_phys[MAX_PAGES] = {0}; +uint8_t orig_file_pages = 0; /* extern: читает кодек (utf_alloc) */ +uint32_t orig_file_size = 0; /* extern: читает кодек (utf_convert_more) */ +uint8_t orig_file_phys[MAX_PAGES] = {0}; /* extern: читает кодек (cv_read) */ /* ---------- Два сосуществующих набора индекс/кэш (Фазы 4-5) ---------- * DOC_8BIT — исходные байты (CP866/CP1251/KOI8R, различаются ремапом на @@ -323,7 +263,6 @@ static docset_t g_doc[2] = {{0}, {0}}; /* ОБЯЗАТЕЛЬНО инициа static uint8_t g_active_doc = DOC_8BIT; /* какой набор сейчас в живых глобалах */ static uint8_t g_disp_doc = DOC_8BIT; /* какой набор показан пользователю */ -static void set_encoding(uint8_t enc); /* forward (нужна в doc_load_live) */ static void doc_switch(uint8_t target); /* forward (нужна в progress_tick) */ #define HPAN_STEP 8u @@ -344,7 +283,7 @@ static uint8_t spinner_active = 0; * и останавливается на конце строки или правой границе экрана. * `s` — литерал/статический буфер (CODE/DATA, окна 1/2), поэтому его * можно передать в BIOS LP_PRINT_LN напрямую, без промежуточной копии. */ -static void put_str_attr(uint8_t x, uint8_t y, const char *s, uint8_t attr) +void put_str_attr(uint8_t x, uint8_t y, const char *s, uint8_t attr) { uint8_t maxlen = (x < SCREEN_W) ? (uint8_t)(SCREEN_W - x) : 0; uint8_t len = (uint8_t)strlen(s); @@ -365,7 +304,7 @@ void fill_row(uint8_t y, uint8_t attr) /* Продвигает спиннер на один кадр. * Если спиннер выключен — функция ничего не делает. */ -static void spinner_tick(void) +void spinner_tick(void) { if (!spinner_active) return; wrchar(SPINNER_COL, 0, spinner_chars[spinner_phase & 3], ATTR_BAR_SPINNER); @@ -503,74 +442,7 @@ void win_rest(uint8_t row, uint8_t col, uint8_t h, uint8_t w, uint8_t page, uint __endasm; } -/* Для проверки границ выделения считаем пробел/таб/конец строки/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'))); -} - -/* Тип сегмента-продолжения, передаваемый в emit_seg(). - * Явно в индексе не хранится: рендер при необходимости восстанавливает - * тип через classify_line(), но параметр оставлен для читаемости вызовов. */ -#define CK_PLAIN 0 -#define CK_QUOTE 1 -#define CK_LIST 2 -#define CK_OTHER 3 - -/* Доступ к индексу в 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); } +/* Char-хелперы, idx_get/idx_put/seg_off, set_*_cur — в mdview2_index.c. */ /* Загружает 16 текстовых атрибутов в палитры ink/paper/blink. */ static void set_pallete(void) { @@ -709,1019 +581,10 @@ static void unload_file(void) file_blk = 0; } -/* ================================================================== - * Индексация строк - * ================================================================== */ +/* Парсер/индексатор (classify_line, scan_join_stream, inline_scan, index_lines, emph_to_attr) — в mdview2_index.c. */ -#define INIT_STYLE_PLAIN 0x0 -#define INIT_STYLE_BOLD 0x1 -#define INIT_STYLE_ITALIC 0x2 -#define INIT_STYLE_UNDER 0x3 -#define INIT_STYLE_CODE 0x4 -#define INIT_STYLE_STRIKE 0x5 -/* Таблица сопоставления 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]; -static 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с). */ -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; /* тип сегмента восстанавливается флагами 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. */ -static 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; -} - -/* ====================== Таблицы: выровненная отрисовка ==================== */ -#define TBL_MAX_COLS 16 -#define TBL_ATTR ATTR_BOX -/* CP866 light box-drawing. */ -#define TBL_H 0xC4 /* ─ */ -#define TBL_V 0xB3 /* │ */ -#define TBL_TL 0xDA /* ┌ */ -#define TBL_TM 0xC2 /* ┬ */ -#define TBL_TR 0xBF /* ┐ */ -#define TBL_ML 0xC3 /* ├ */ -#define TBL_MM 0xC5 /* ┼ */ -#define TBL_MR 0xB4 /* ┤ */ -#define TBL_BL 0xC0 /* └ */ -#define TBL_BM 0xC1 /* ┴ */ -#define TBL_BR 0xD9 /* ┘ */ - -/* Смещение завершающего '\n' строки (или file_size). */ -static uint32_t row_end(uint32_t p) -{ - while (p < file_size && fb(p) != '\n') p++; - return p; -} - -/* Начало контента первой ячейки строки (после ведущих пробелов и '|'). */ -static uint32_t table_first_cell(uint32_t row_start) -{ - uint32_t p = row_start; - while (p < file_size && fb(p) == ' ') p++; - if (p < file_size && fb(p) == '|') p++; - return p; -} - -/* Следующая ячейка: [*cs,*ce) — обрезанный диапазон; *pp продвигается за '|'. - * 0 — ячеек больше нет (включая хвостовую пустоту после последнего '|'). */ -static uint8_t table_next_cell(uint32_t *pp, uint32_t lineend, uint32_t *cs, uint32_t *ce) -{ - uint32_t p = *pp; - if (p >= lineend) return 0; - uint32_t a = p; - while (p < lineend && fb(p) != '|') p++; - uint32_t b = p; - uint8_t had_pipe = (uint8_t)(p < lineend && fb(p) == '|'); - if (had_pipe) p++; - *pp = p; - while (a < b && fb(a) == ' ') a++; - while (b > a && fb(b - 1) == ' ') b--; - if (!had_pipe && a == b) return 0; - *cs = a; *ce = b; - return 1; -} - -/* Строка-разделитель (|---|:-:|): каждая ячейка непустая и только '-'/':'. */ -static uint8_t table_is_sep_row(uint32_t row_start, uint32_t lineend) -{ - uint32_t p = table_first_cell(row_start); - uint32_t cs, ce; - uint8_t n = 0; - while (table_next_cell(&p, lineend, &cs, &ce)) { - if (cs >= ce) return 0; - for (uint32_t q = cs; q < ce; q++) { char c = fb(q); if (c != '-' && c != ':') return 0; } - n++; - } - return (uint8_t)(n > 0); -} - -/* Горизонтальная рамка в g_cells: left + (H×(w+2) + mid|right) по колонкам. */ -static void table_border(const uint8_t *widths, uint8_t ncols, char left, char mid, char right) -{ - gc_put(left, TBL_ATTR); - for (uint8_t c = 0; c < ncols; c++) { - gc_fill(TBL_H, TBL_ATTR, (uint8_t)(widths[c] + 2)); - gc_put((c == (uint8_t)(ncols - 1)) ? right : mid, TBL_ATTR); - } -} - -/* Строка данных в g_cells: │ <ячейка, добитая до widths[c]> │ … */ -static void table_data_row(uint32_t row_start, uint32_t lineend, const uint8_t *widths, uint8_t ncols) -{ - uint32_t p = table_first_cell(row_start); - uint32_t cs, ce; - gc_put(TBL_V, TBL_ATTR); - for (uint8_t c = 0; c < ncols; c++) { - uint8_t got = table_next_cell(&p, lineend, &cs, &ce); - gc_put(' ', TBL_ATTR); - uint8_t rendered = 0; - if (got && ce > cs) { - uint8_t before = g_ncells; - (void)inline_scan(cs, ce, 0, CK_OTHER, INIT_STYLE_PLAIN, ATTR_TEXT, 1); - rendered = (uint8_t)(g_ncells - before); - } - if (rendered < widths[c]) gc_fill(' ', ATTR_TEXT, (uint8_t)(widths[c] - rendered)); - gc_put(' ', TBL_ATTR); - gc_put(TBL_V, TBL_ATTR); - } -} - -/* Строит индекс сегментов рендера по загруженному файлу. */ -static void index_lines(void) -{ - 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; - } -} - - -/* Таблицы ремапа СТАРШИХ байтов [128-255] в CP866. Индексируются (ch-0x80): - * младшие 128 (ASCII) — identity, в ремапе не участвуют (win_rest_remap - * трогает только ch>=0x80), поэтому в таблицах их нет. Кириллица + ходовая - * пунктуация; неизвестное → '?' (0x3F). Сгенерированы Python codecs. */ -static const uint8_t cp1251_to_866[128] = { - /* 80 */ 0x3F, 0x3F, 0x27, 0x3F, 0x22, 0x2E, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3C, 0x3F, 0x3F, 0x3F, 0x3F, - /* 90 */ 0x3F, 0x27, 0x27, 0x22, 0x22, 0x07, 0x2D, 0x2D, 0x3F, 0x3F, 0x3F, 0x3E, 0x3F, 0x3F, 0x3F, 0x3F, - /* A0 */ 0xFF, 0xF6, 0xF7, 0x3F, 0xFD, 0x3F, 0x3F, 0x3F, 0xF0, 0x63, 0xF2, 0x3C, 0x3F, 0x3F, 0x72, 0xF4, - /* B0 */ 0xF8, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0xFA, 0xF1, 0xFC, 0xF3, 0x3E, 0x3F, 0x3F, 0x3F, 0xF5, - /* C0 */ 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F, - /* D0 */ 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F, - /* E0 */ 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, - /* F0 */ 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, -}; -static const uint8_t koi8r_to_866[128] = { - /* 80 */ 0xC4, 0xB3, 0xDA, 0xBF, 0xC0, 0xD9, 0xC3, 0xB4, 0xC2, 0xC1, 0xC5, 0xDF, 0xDC, 0xDB, 0xDD, 0xDE, - /* 90 */ 0xB0, 0xB1, 0xB2, 0x3F, 0xFE, 0xF9, 0xFB, 0x3F, 0x3F, 0x3F, 0xFF, 0x3F, 0xF8, 0x3F, 0xFA, 0x3F, - /* A0 */ 0xCD, 0xBA, 0xD5, 0xF1, 0xD6, 0xC9, 0xB8, 0xB7, 0xBB, 0xD4, 0xD3, 0xC8, 0xBE, 0xBD, 0xBC, 0xC6, - /* B0 */ 0xC7, 0xCC, 0xB5, 0xF0, 0xB6, 0xB9, 0xD1, 0xD2, 0xCB, 0xCF, 0xD0, 0xCA, 0xD8, 0xD7, 0xCE, 0x63, - /* C0 */ 0xEE, 0xA0, 0xA1, 0xE6, 0xA4, 0xA5, 0xE4, 0xA3, 0xE5, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, - /* D0 */ 0xAF, 0xEF, 0xE0, 0xE1, 0xE2, 0xE3, 0xA6, 0xA2, 0xEC, 0xEB, 0xA7, 0xE8, 0xED, 0xE9, 0xE7, 0xEA, - /* E0 */ 0x9E, 0x80, 0x81, 0x96, 0x84, 0x85, 0x94, 0x83, 0x95, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, - /* F0 */ 0x8F, 0x9F, 0x90, 0x91, 0x92, 0x93, 0x86, 0x82, 0x9C, 0x9B, 0x87, 0x98, 0x9D, 0x99, 0x97, 0x9A, -}; - -/* Устанавливает активную кодировку и таблицу ремапа (для 8-битных). */ -static void set_encoding(uint8_t enc) -{ - g_encoding = enc; - g_remap = (enc == ENC_CP1251) ? cp1251_to_866 : - (enc == ENC_KOI8R) ? koi8r_to_866 : (const uint8_t *)0; -} +/* Таблицы ремапа, set_encoding, кодировки и UTF-8 конвертер — в mdview2_enc.c. */ /* ---------- Сохранение/восстановление/переключение наборов ---------- * doc_save_live: живые глобалы → g_doc[i]. doc_load_live: g_doc[i] → живые @@ -1771,465 +634,9 @@ static void doc_switch(uint8_t target) g_active_doc = target; } -/* Самые ходовые строчные русские буквы (о е а и н т с р в л) — их байты - * различают 8-битные кодировки по частоте. */ -static const uint8_t common866 [10] = {0xAE,0xA5,0xA0,0xA8,0xAD,0xE2,0xE1,0xE0,0xA2,0xAB}; -static const uint8_t common1251[10] = {0xEE,0xE5,0xE0,0xE8,0xED,0xF2,0xF1,0xF0,0xE2,0xEB}; -static const uint8_t commonkoi [10] = {0xCF,0xC5,0xC1,0xC9,0xCE,0xD4,0xD3,0xD2,0xD7,0xCC}; -static uint8_t in_set10(const uint8_t *s, uint8_t b) -{ - for (uint8_t i = 0; i < 10; i++) if (s[i] == b) return 1; - return 0; -} +/* Автоопределение/имя кодировки, ремап-вывод и UTF-8→CP866 конвертер — в mdview2_enc.c. */ -/* Автоопределение кодировки дешёвым сканом байтов (до построения индекса). - * BOM → UTF8; иначе валидность UTF-8 (структура multibyte); иначе 8-бит по - * частоте ходовых букв; фолбэк CP866. */ -static uint8_t detect_encoding(void) -{ - if (file_size >= 3 && (uint8_t)fb(0) == 0xEF && (uint8_t)fb(1) == 0xBB && (uint8_t)fb(2) == 0xBF) - return ENC_UTF8; - - uint32_t n = file_size; - if (n > 4096u) n = 4096u; /* сэмпл: первые 4 КБ — детекции хватает */ - uint8_t utf_ok = 1, has_mb = 0, has_high = 0, cont = 0; - uint16_t s866 = 0, s1251 = 0, skoi = 0; - - for (uint32_t p = 0; p < n; p++) { - uint8_t b = (uint8_t)fb(p); - if (b < 0x80) { if (cont) { utf_ok = 0; cont = 0; } continue; } - has_high = 1; - if (in_set10(common866, b)) s866++; - if (in_set10(common1251, b)) s1251++; - if (in_set10(commonkoi, b)) skoi++; - if (cont) { - if ((b & 0xC0) == 0x80) cont--; - else { utf_ok = 0; cont = 0; } - } else if (b >= 0xC2 && b <= 0xDF) { cont = 1; has_mb = 1; } - else if (b >= 0xE0 && b <= 0xEF) { cont = 2; has_mb = 1; } - else if (b >= 0xF0 && b <= 0xF4) { cont = 3; has_mb = 1; } - else utf_ok = 0; /* битый лид/одиночный континюэйшн */ - } - /* Незавершённая multibyte-последовательность на КОНЦЕ — нарушение только - * если это настоящий EOF; на границе сэмпла (n= s866 && s1251 >= skoi) return ENC_CP1251; - if (skoi >= s866) return ENC_KOI8R; - return ENC_CP866; -} - -static const char *enc_name(uint8_t enc) -{ - return (enc == ENC_CP1251) ? "CP1251" : - (enc == ENC_KOI8R) ? "KOI8-R" : - (enc == ENC_UTF8) ? "UTF-8 " : "CP866 "; -} - -/* Контентный глиф (ремапим при смене кодировки) vs структурный (рамка/HR/ - * маркеры — уже CP866, не трогаем). Различаем по attr. */ -static uint8_t attr_is_content(uint8_t a) -{ - return (uint8_t)(a != ATTR_BOX && a != ATTR_HR && - a != ATTR_LIST_MARKER && a != ATTR_QUOTE_MARKER); -} - -/* Вывод среза строки кэша с ремапом контентных глифов [128-255] через - * g_remap (CP1251/KOI8). Без активной таблицы — прямой win_rest. */ -static void win_rest_remap(uint8_t row, uint8_t w, uint8_t page, uint16_t off) -{ - if (!g_remap || w == 0) { win_rest(row, 0, 1, w, page, off); return; } - uint8_t buf[SCREEN_W * 2]; - bank_read(page, off, buf, (uint16_t)w * 2u); - for (uint8_t i = 0; i < w; i++) { - uint8_t ch = buf[(uint16_t)i * 2u]; - if (ch >= 0x80 && attr_is_content(buf[(uint16_t)i * 2u + 1u])) - buf[(uint16_t)i * 2u] = g_remap[ch - 0x80]; /* таблицы хранят только старшие 128 */ - } - bank_write(g_scratch_phys, 0, buf, (uint16_t)w * 2u); - win_rest(row, 0, 1, w, g_scratch_phys, 0); -} - -/* ========================= UTF-8 → CP866 (Фаза 2) ======================== */ - -/* Кириллица U+0400..U+045F → CP866. Русский набор + Ё/ё и часть украинских - * (Є є Ї ї Ў ў), которые есть в CP866; пропуски → '?' (0x3F). */ -static const uint8_t utf_cyr_to_866[96] = { - /* 0400 */ 0x3F, 0xF0, 0x3F, 0x3F, 0xF2, 0x3F, 0x3F, 0xF4, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0xF6, 0x3F, - /* 0410 */ 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F, - /* 0420 */ 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F, - /* 0430 */ 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, - /* 0440 */ 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, - /* 0450 */ 0x3F, 0xF1, 0x3F, 0x3F, 0xF3, 0x3F, 0x3F, 0xF5, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0xF7, 0x3F, -}; - -/* Поток вывода конвертера: пишем в utf_phys чанками по странице, буфер в - * near-памяти, флаш через один bank_write (не побайтовый swap W3). */ -static uint8_t conv_buf[256]; -static uint16_t conv_n = 0; /* байт в буфере, ещё не сброшено */ -static uint8_t conv_page = 0; /* индекс текущей страницы в utf_phys */ -static uint16_t conv_off = 0; /* уже сброшенное смещение в этой странице */ - -static void conv_flush(void) -{ - if (conv_n) { - bank_write(utf_phys[conv_page], conv_off, conv_buf, conv_n); - conv_off = (uint16_t)(conv_off + conv_n); - conv_n = 0; - } -} - -/* Добавляет один CP866-байт в выходной поток. Запись никогда не пересекает - * границу страницы (256 делит 16384 нацело). */ -static void conv_put(uint8_t b) -{ - if ((uint16_t)(conv_off + conv_n) == PAGE_SIZE) { /* страница заполнена */ - conv_flush(); - conv_page++; - conv_off = 0; - } - conv_buf[conv_n++] = b; - if (conv_n == (uint16_t)sizeof(conv_buf)) conv_flush(); -} - -/* Подстановки одиночных не-кириллических символов UTF-8 → один CP866-байт. - * Таблица вместо switch (экономия кода); линейный поиск дёшев — вызывается - * лишь при конвертации на редких символьных кодпойнтах. Чтобы добавить новый - * символ — достаточно дописать одну строку {кодпойнт, байт-CP866}. - * (Символы с НЕ-1:1 заменой, напр. … → "...", обрабатываются отдельно ниже.) */ -typedef struct { uint16_t utf8; uint8_t cp866; } utf_sym_t; -static const utf_sym_t utf_sym[] = { - {0x00A0, 0x20}, {0x00AB, 0x3C}, {0x00BB, 0x3E}, /* nbsp « » → space < > */ - {0x00B0, 0xF8}, {0x00B7, 0xFA}, /* ° · */ - {0x2013, 0x2D}, {0x2014, 0x2D}, /* – — → '-' */ - {0x2018, 0x27}, {0x2019, 0x27}, /* ‘ ’ → '\'' */ - {0x201C, 0x22}, {0x201D, 0x22}, /* “ ” → '"' */ - {0x2022, 0xF9}, {0x2116, 0xFC}, /* • → ∙ № */ - {0x2190, 0x1B}, {0x2191, 0x18}, {0x2192, 0x1A}, {0x2193, 0x19}, /* ← ↑ → ↓ */ - {0x2500, 0xC4}, {0x2502, 0xB3}, {0x250C, 0xDA}, {0x2510, 0xBF}, /* ─ │ ┌ ┐ */ - {0x2514, 0xC0}, {0x2518, 0xD9}, {0x251C, 0xC3}, {0x2524, 0xB4}, /* └ ┘ ├ ┤ */ - {0x252C, 0xC2}, {0x2534, 0xC1}, {0x253C, 0xC5}, /* ┬ ┴ ┼ */ - {0x2580, 0xDF}, {0x2584, 0xDC}, {0x2588, 0xDB}, /* ▀ ▄ █ */ - {0x2591, 0xB0}, {0x2592, 0xB1}, {0x2593, 0xB2}, {0x25A0, 0xFE}, /* ░ ▒ ▓ ■ */ - {0x2713, 0xFB}, {0x2714, 0xFB}, /* ✓ ✔ → галка */ - {0x2715, 0x78}, {0x2717, 0x78}, {0x2718, 0x78}, /* ✕ ✗ ✘ → 'x' */ -}; -#define UTF_SYM_N (uint8_t)(sizeof(utf_sym) / sizeof(utf_sym[0])) - -/* Кодпойнт → CP866-глиф(ы). Кириллица по таблице; спецслучаи (…, BOM) явно; - * остальные ходовые символы по таблице utf_sym[]; прочее → '?'. */ -static void conv_emit_cp(uint32_t cp) -{ - if (cp < 0x80) { conv_put((uint8_t)cp); return; } - if (cp >= 0x0400 && cp <= 0x045F) { conv_put(utf_cyr_to_866[cp - 0x0400]); return; } - if (cp == 0x2026) { conv_put('.'); conv_put('.'); conv_put('.'); return; } /* … → "..." */ - if (cp == 0xFEFF) return; /* BOM/ZWNBSP — выкинуть */ - for (uint8_t i = 0; i < UTF_SYM_N; i++) - if (utf_sym[i].utf8 == (uint16_t)cp) { conv_put(utf_sym[i].cp866); return; } - conv_put('?'); -} - -/* Декодирует активный (исходный UTF-8) буфер в utf_phys как CP866. - * Битые/обрезанные последовательности → '?'; 4-байтные (эмодзи и т.п.) → '?'. */ -/* Последовательное чтение байта ОРИГИНАЛА (utf-8) конвертером. Оригинал лежит - * в orig_file_phys[]; мапим его страницу в W3 напрямую, со своей кэш-переменной - * cv_page (отдельной от cur_page индексатора, т.к. оба используют W3 и - * чередуются). На границе с fb()-чтением utf-буфера кэши взаимно сбрасываются. */ -static uint8_t cv_read(uint32_t s) -{ - uint8_t pg = (uint8_t)(s >> PAGE_BITS); - if (pg != cv_page) { sprinter_page_w3(orig_file_phys[pg]); cv_page = pg; } - return *((volatile uint8_t *)(0xC000u + (uint16_t)(s & PAGE_MASK))); -} - -/* Возобновляемая конвертация: дописывает utf_phys из оригинала, пока - * сконвертированных (flushed) байт меньше target и не достигнут конец - * исходника. По выходу file_size = доступная (сконвертированная) часть; при - * достижении конца фиксирует utf_size и снимает g_utf_building. */ -static void utf_convert_more(uint32_t target) -{ - cv_page = 0xFF; /* W3 был на utf (fb индексатора) — пере-смаппим orig */ - uint32_t n = orig_file_size; - while (utf_src < n && - (uint32_t)((uint32_t)conv_page * PAGE_SIZE + conv_off) < target) { - uint8_t b = (uint8_t)cv_read(utf_src++); - if (b < 0x80) { conv_emit_cp(b); continue; } - - uint32_t cp; - uint8_t need; - if ((b & 0xE0) == 0xC0) { cp = (uint32_t)(b & 0x1F); need = 1; } - else if ((b & 0xF0) == 0xE0) { cp = (uint32_t)(b & 0x0F); need = 2; } - else if ((b & 0xF8) == 0xF0) { cp = (uint32_t)(b & 0x07); need = 3; } - else { conv_put('?'); continue; } - - uint8_t ok = 1; - for (uint8_t k = 0; k < need; k++) { - if (utf_src >= n) { ok = 0; break; } - uint8_t cb = (uint8_t)cv_read(utf_src); - if ((cb & 0xC0) != 0x80) { ok = 0; break; } - cp = (cp << 6) | (uint32_t)(cb & 0x3F); - utf_src++; - } - if (!ok || need == 3) { conv_put('?'); continue; } - conv_emit_cp(cp); - } - conv_flush(); - file_size = (uint32_t)conv_page * PAGE_SIZE + conv_off; /* доступно индексатору */ - if (utf_src >= n) { utf_size = file_size; g_utf_building = 0; } - cur_page = 0xFF; /* W3 был на orig — fb индексатора пере-смаппит utf */ -} - -/* Выделяет страницы под UTF-8 набор и конвертирует туда исходник (читает - * оригинал — вызывать, когда живой буфер = исходные байты). НЕ перенаправляет - * активный буфер: это делает build_doc() при сборке набора DOC_UTF8. - * Конвертированный поток ≤ исходного (кириллица 2 байта → 1), хватает того же - * числа страниц. Возврат: 1 — успех, 0 — нет EMM. */ -/* Выделяет страницы под UTF-8 набор (конвертированный ≤ оригинала по размеру). - * Сама конвертация — инкрементальная, через utf_convert_more(). Возврат: 1 — - * успех, 0 — нет EMM. */ -static uint8_t utf_alloc(void) -{ - utf_pages = orig_file_pages; - utf_blk = mem_alloc_pages(utf_pages); - if (utf_blk == 0) return 0; - for (uint8_t i = 0; i < utf_pages; i++) - utf_phys[i] = mem_get_page(utf_blk, i); - return 1; -} - -/* Фаза 4-5 — cache-only draw path: рисует одну строку ИЗ КЭША, без - * единого обращения к fb()/исходному файлу. Горизонтальный скролл для - * nowrap-строк (Фаза 5) — это просто смещение начала среза на vx*2 - * байт ВНУТРИ ТОГО ЖЕ кэш-буфера (cache_reserve гарантирует, что вся - * строка лежит в одной странице, так что off+vx*2 не пересекает - * границу страницы); не nowrap-строки игнорируют viewport_x, как и - * раньше в живом render_line() (там это называлось effective_vx). */ -static void draw_line_from_cache(uint16_t line_idx, uint8_t row) -{ - cache_rec_t rec; - cache_dir_get(line_idx, &rec); - - bios_set_place(row, 0); - bios_fillcharattr(' ', ATTR_TEXT, SCREEN_W); - - /* Горизонтальный сдвиг применяется ко ВСЕМ строкам hscroll-блока (код, - * таблицы) — по ТИПУ, а не по длине: блок двигается целиком, включая - * строки короче 80 (короткая строка, ушедшая за левый край, просто - * показывает маркер '<' на пустом месте). HR и границы fence не имеют - * IF_HSCROLL и не сдвигаются. */ - uint8_t vx = (rec.flags & IF_HSCROLL) ? viewport_x : 0; - - if (rec.len > vx) { - uint16_t remaining = (uint16_t)(rec.len - vx); - uint8_t w = (remaining > SCREEN_W) ? SCREEN_W : (uint8_t)remaining; - uint16_t off = (uint16_t)(rec.off + (uint16_t)vx * 2u); - win_rest_remap(row, w, rec.page, off); - if (remaining > SCREEN_W) { - wrchar(SCREEN_W - 1, row, '>', ATTR_TRUNC); - } - } - if (vx > 0) { - wrchar(0, row, '<', ATTR_TRUNC); - } -} - -/* Перерисовывает все VIEW_H строк окна из кэша — основной путь - * перерисовки видимой области после загрузки файла (Фаза 3-4). */ -static void draw_viewport_from_cache(void) -{ - for (uint8_t i = 0; i < VIEW_H; i++) { - draw_line_from_cache((uint16_t)(top_line + i), (uint8_t)(VIEW_TOP_ROW + i)); - } -} - -/* Число строк, готовых к показу. Во время загрузки последняя эмитированная - * строка ещё в g_cells (не зафлашена), поэтому доступны [0..n_lines-2]. */ -static uint16_t drawable_lines(void) -{ - if (g_loading) return (n_lines >= 1) ? (uint16_t)(n_lines - 1) : 0; - return n_lines; -} - -/* Возвращает процент прокрутки (0..100) относительно доступного диапазона. */ -static uint8_t calc_pct(void) -{ - uint16_t total = drawable_lines(); - uint8_t pct; - if (total <= VIEW_H) - pct = 100; - else - pct = (uint8_t)((uint32_t)top_line * 100UL / (uint32_t)(total - VIEW_H)); - return pct; -} - -/* Рисует динамическую часть статус-бара без printf. - * Полностью перезаписывает колонки 45..79, чтобы не оставлять «хвосты». */ -static void render_status_numbers(void) -{ - uint16_t total = drawable_lines(); - uint16_t last = top_line + VIEW_H; - if (last > total) last = total; - uint8_t pct = calc_pct(); - - textattr(ATTR_BAR); - bios_set_place(0, 45); - bios_fillcharattr(' ', ATTR_BAR, (uint8_t)(SCREEN_W - 45)); - - wrchar(45, 0, 0xB3, ATTR_BAR); /* вертикальный разделитель */ - gotoxy(47, 0); - cputs("L "); - dec16(top_line + 1); - cputs("-"); - dec16(last); - cputs(" / "); - dec16(total); - if (g_loading) cputs("..."); /* ещё грузится */ - - wrchar(70, 0, 0xB3, ATTR_BAR); /* вертикальный разделитель */ - gotoxy( 73, 0); - dec8(pct); - cputs("%"); -} - -/* Полностью перерисовывает статус-бар (заголовок + числа). */ -static void render_full_status(void) -{ - - fill_row(0, ATTR_BAR); - /* Размещение в шапке: "MDVIEW", затем слот спиннера и имя файла. */ - put_str_attr(1, 0, "MDVIEW", ATTR_BAR); - put_str_attr(10, 0, filename, ATTR_BAR); - put_str_attr(37, 0, enc_name(g_encoding), ATTR_BAR); /* текущая кодировка */ - render_status_numbers(); -} - -/* Обновляет только изменяемую числовую часть статус-бара. */ -static void render_updated_status(void) -{ - render_status_numbers(); -} - -/* Отрисовывает нижнюю строку меню. */ -static void render_menu(void) -{ - /* Блок i = колонка i*8 (8 шириной): 2 поз. НОМЕР клавиши без 'F' (стиль - * ATTR_MENU_K, ведущий «чёрный» пробел кроме F10) + 6 поз. ТЕКСТ-функция - * (ATTR_MENU_T) сразу за номером. Номера рисуем для ВСЕХ 10 клавиш; текст — - * только у задействованных и доступных сейчас. */ - fill_row(MENU_ROW, ATTR_MENU_T); - - char num[3]; num[2] = 0; - for (uint8_t i = 0; i < 9; i++) { /* F1..F9: ' 1'..' 9' */ - num[0] = ' '; num[1] = (char)('1' + i); - put_str_attr((uint8_t)(i * 8), MENU_ROW, num, ATTR_MENU_K); - } - put_str_attr(9 * 8, MENU_ROW, "10", ATTR_MENU_K); /* F10 */ - - put_str_attr(0 * 8 + 2, MENU_ROW, "Help", ATTR_MENU_T); -#if WITH_RAW - if (g_ready) /* RAW доступен после постройки документа */ - put_str_attr(1 * 8 + 2, MENU_ROW, (g_view == VIEW_MD) ? "RAW" : "MD", ATTR_MENU_T); - if (g_view != VIEW_MD) /* Wrap/Unwrap — только в RAW */ - put_str_attr(2 * 8 + 2, MENU_ROW, - (g_view == VIEW_RAW_WRAP) ? "UnWrap" : "Wrap", ATTR_MENU_T); -#endif - if (g_f8_enabled) /* смена кодировки сейчас возможна */ - put_str_attr(7 * 8 + 2, MENU_ROW, "CodePg", ATTR_MENU_T); - put_str_attr(9 * 8 + 2, MENU_ROW, "Exit", ATTR_MENU_T); -} - -/* ================================================================== - * Прокрутка - * ================================================================== */ - -/* Ограничивает top_line допустимым диапазоном с учётом высоты окна. */ -static void clamp_top(void) -{ - if (n_lines <= VIEW_H) { - top_line = 0; - } else if (top_line > n_lines - VIEW_H) { - top_line = (uint16_t)(n_lines - VIEW_H); - } -} - -/* Прокрутка вверх на n строк с частичной перерисовкой при n == 1. */ -static void scroll_up(uint16_t n) -{ - uint16_t new_top_line = (top_line >= n) ? (uint16_t)(top_line - n) : 0; - if( new_top_line != top_line) { - top_line = new_top_line; - if (n == 1) { - scroll(0, VIEW_TOP_ROW, SCREEN_W, VIEW_H, 2, 0); - draw_line_from_cache((uint16_t)(top_line), (uint8_t)(VIEW_TOP_ROW)); - } else { - draw_viewport_from_cache(); - } - } -} - -/* Прокрутка вниз на n строк с частичной перерисовкой при n == 1. */ -static void scroll_down(uint16_t n) -{ - uint16_t new_top_line = (top_line + n < n_lines - VIEW_H) ? - (uint16_t)(top_line + n) : n_lines - VIEW_H; - if( new_top_line != top_line) { - top_line = new_top_line; - if (n == 1) { - scroll(0, VIEW_TOP_ROW, SCREEN_W, VIEW_H, 1, 0); - draw_line_from_cache((uint16_t)(top_line + VIEW_H - 1), (uint8_t)(VIEW_TOP_ROW + VIEW_H - 1)); - } else { - clamp_top(); - draw_viewport_from_cache(); - } - } -} - -/* Горизонтальный сдвиг (только если в окне есть nowrap-строки). - * Максимум сдвига ограничен самой широкой nowrap-строкой на экране. - * Фаза 5: ширина берётся прямо из директории кэша (rec.len — уже - * посчитанная реальная длина в ячейках после рендера), без единого - * обращения к fb()/seg_off()/исходному файлу. */ -static void scroll_h(int8_t delta) -{ - uint16_t maxw = 0; - for (uint8_t i = 0; i < VIEW_H; i++) { - uint16_t li = (uint16_t)(top_line + i); - if (li >= n_lines) break; - cache_rec_t rec; - cache_dir_get(li, &rec); - if (!(rec.flags & IF_HSCROLL)) continue; /* только код/таблицы */ - if (rec.len > maxw) maxw = rec.len; /* самая широкая строка блока */ - } - if (maxw == 0) return; /* нет строк шире экрана */ - - /* Максимальный сдвиг = ширина за пределами экрана, в границах uint8. */ - uint16_t over = (maxw > SCREEN_W) ? (uint16_t)(maxw - SCREEN_W) : 0; - if (over > 248u) over = 248u; - uint8_t max_vx = (uint8_t)over; - - int16_t nx = (int16_t)viewport_x + delta; - if (nx < 0) nx = 0; - if (nx > (int16_t)max_vx) nx = max_vx; - - /* ПОРЯДОК ВАЖЕН: записываем viewport_x ДО сравнения, а сравниваем уже - * сохранённую копию old_vx с new_vx. Иначе SDCC генерирует для - * `if (new_vx != viewport_x) viewport_x = new_vx;` такой код: - * ld a, new_vx ; sub a,(viewport_x) ; jr Z,.. ; ld (viewport_x),a - * — т.е. для записи переиспользует регистр A, испорченный вычитанием в - * сравнении, и кладёт в viewport_x не new_vx, а (new_vx - old_vx). - * (asm-дамп 2026-06-24 подтвердил; даёт 10-8=2, 10-2=8 — ровно - * наблюдавшийся "прыжок" скролла.) Записав viewport_x первой, мы убираем - * портящий sub с пути записи; сравнение ниже только решает, перерисовывать - * ли экран. */ - uint8_t new_vx = (uint8_t)nx; - uint8_t old_vx = viewport_x; - viewport_x = new_vx; - if (new_vx != old_vx) - draw_viewport_from_cache(); -} +/* Отрисовка области/статус-бара/меню и прокрутка — в модуле mdview2_view.c. */ /* ================================================================== @@ -2245,89 +652,7 @@ static void die(const char *msg) (void)getkey(); } -/* ================================================================== - * Диалог справки (F1) - * ================================================================== */ - -/* Заполняет одну внутреннюю строку диалога (r=0 — первая строка за рамкой). - * Строка s в кодировке CP866; остаток до края дополняется пробелами. */ -static void help_line(uint8_t r, const char *s, uint8_t attr) -{ - uint8_t x = HELP_X + 1u; - uint8_t y = HELP_Y + 1u + r; - uint8_t len = (uint8_t)strlen(s); - if (len > HELP_W - 2u) len = HELP_W - 2u; - - bios_set_place(y, x); - if (len > 0) bios_writeattr(s, len, attr); - /* place уже продвинут bios_writeattr на len колонок (verified). */ - if (len < HELP_W - 2u) { - bios_fillcharattr(' ', ATTR_HELP_BG, (uint8_t)(HELP_W - 2u - len)); - } -} - -static void show_help(void) -{ - /* " Помощь " в CP866 (8 байт) */ - static const char title[] = " \x8F\xAE\xAC\xAE\xE9\xEC "; - uint8_t tlen = 8u; - uint8_t lft = (uint8_t)((HELP_W - 2u - tlen) / 2u); /* = 27 */ - uint8_t rgt = (uint8_t)(HELP_W - 2u - tlen - lft); - /* Верхняя граница рамки с заголовком по центру. - * place продвигается каждым BIOS-вызовом (verified), поэтому - * достаточно одной установки места на всю строку рамки. */ - wrchar(HELP_X, HELP_Y, 0xDA, ATTR_HELP_BDR); /* ┌ */ - bios_set_place(HELP_Y, (uint8_t)(HELP_X + 1u)); - if (lft > 0) bios_fillcharattr(0xC4, ATTR_HELP_BDR, lft); - if (tlen > 0) bios_writeattr(title, tlen, ATTR_HELP_TIT); - if (rgt > 0) bios_fillcharattr(0xC4, ATTR_HELP_BDR, rgt); - wrchar(HELP_X + HELP_W - 1u, HELP_Y, 0xBF, ATTR_HELP_BDR); /* ┐ */ - - /* Боковые границы (левый и правый │ для каждой строки тела) */ - for (uint8_t r = 1u; r < HELP_H - 1u; r++) { - wrchar(HELP_X, HELP_Y + r, 0xB3, ATTR_HELP_BDR); /* │ */ - wrchar(HELP_X + HELP_W - 1u, HELP_Y + r, 0xB3, ATTR_HELP_BDR); /* │ */ - } - - /* Нижняя граница рамки */ - wrchar(HELP_X, HELP_Y + HELP_H - 1u, 0xC0, ATTR_HELP_BDR); /* └ */ - bios_set_place((uint8_t)(HELP_Y + HELP_H - 1u), (uint8_t)(HELP_X + 1u)); - bios_fillcharattr(0xC4, ATTR_HELP_BDR, (uint8_t)(HELP_W - 2u)); - wrchar(HELP_X + HELP_W - 1u, HELP_Y + HELP_H - 1u, 0xD9, ATTR_HELP_BDR); /* ┘ */ - - /* Содержимое (20 внутренних строк) */ - uint8_t r = 0; - help_line(r++, "", ATTR_HELP_BG); - help_line(r++, " MDView v0.2 -- Markdown Viewer for Sprinter", ATTR_HELP_HDR); - help_line(r++, " (c) 2026 Petrov A.G.", ATTR_HELP_BG); - help_line(r++, "", ATTR_HELP_BG); - help_line(r++, " Navigation:", ATTR_HELP_HDR); - help_line(r++, " \x18 \x19 Scroll one line up / down", ATTR_HELP_BG); - help_line(r++, " PgUp PgDn Scroll one page up / down", ATTR_HELP_BG); - help_line(r++, " Home End Jump to beginning / end of document", ATTR_HELP_BG); - help_line(r++, " \x1B \x1A Horizontal pan (code blocks/tables)", ATTR_HELP_BG); - help_line(r++, " Esc F10 Exit", ATTR_HELP_BG); - help_line(r++, "", ATTR_HELP_BG); - help_line(r++, " Markdown elements:", ATTR_HELP_HDR); - help_line(r++, " # ## ### Headings H1-H6", ATTR_HELP_BG); - help_line(r++, " **bold** *italic* `code` ~~strike~~", ATTR_HELP_BG); - help_line(r++, " > quote ``` ... ``` Fenced code block", ATTR_HELP_BG); - help_line(r++, " - * + item 1. 2. Ordered list", ATTR_HELP_BG); - help_line(r++, " |----|----| Tables", ATTR_HELP_BG); - help_line(r++, "", ATTR_HELP_BG); - help_line(r++, " Encoding:", ATTR_HELP_HDR); - help_line(r++, " F8 Cycle CP866 / CP1251 / KOI8-R / UTF-8", ATTR_HELP_BG); - help_line(r++, " Auto-detected on open; F8 to override", ATTR_HELP_BG); - help_line(r++, "", ATTR_HELP_BG); - help_line(r++, " File size: up to 128 KB (EMM). Lines: up to 16384.", ATTR_HELP_BG); - help_line(r++, "", ATTR_HELP_BG); - - (void)getkey(); - - render_full_status(); - draw_viewport_from_cache(); - render_menu(); -} +/* Диалог справки (F1) — в модуле mdview2_help.c (show_help). */ /* Обработка клавиши во время прогрессивной загрузки: скролл строго в * пределах готовых строк (drawable_lines); End = последняя готовая страница; @@ -2374,7 +699,7 @@ static void load_key(uint16_t k) } /* Кооперативный «тик» прогрессивной загрузки — вызывается из index_lines(). */ -static void progress_tick(void) +void progress_tick(void) { #if WITH_RAW /* В RAW-режиме буфер документа готов сразу после конвертации (до индекса), @@ -2434,11 +759,8 @@ static int build_doc(uint8_t slot, uint8_t enc, uint8_t visible) { if (slot == DOC_UTF8) { if (!utf_avail) { /* первый вход: выделить страницы */ - if (!utf_alloc()) return -7; + if (!utf_alloc()) return -7; /* выделяет страницы + сбрасывает конвертер */ utf_avail = 1; - conv_n = 0; conv_page = 0; conv_off = 0; - utf_src = 0; - g_utf_building = 1; } file_blk = utf_blk; file_pages = utf_pages; memcpy(file_phys, utf_phys, MAX_PAGES); diff --git a/examples/mdview2/mdview2.h b/examples/mdview2/mdview2.h index 006497f..24adba0 100644 --- a/examples/mdview2/mdview2.h +++ b/examples/mdview2/mdview2.h @@ -61,6 +61,54 @@ #define VIEW_RAW_WRAP 1 /* RAW: длинные строки переносятся кратно 80 */ #define VIEW_RAW_HSCROLL 2 /* RAW: длинные строки в одну, гориз. скролл */ +/* ---- Кодировки --------------------------------------------------- */ +/* 8-битные (CP866/CP1251/KOI8R) различаются только ремапом глифов [128-255] + * на отрисовке; UTF-8 — отдельный декодированный набор. */ +#define ENC_CP866 0 +#define ENC_CP1251 1 +#define ENC_KOI8R 2 +#define ENC_UTF8 3 +#define CONV_MARGIN 4096u /* на сколько байт держать UTF-конвертацию впереди индексатора */ + +/* ---- Флаги сегмента индекса (IF_*) — общие для индексатора/кэша/вью ---- */ +#define IF_CONT 0x01u /* сегмент является продолжением перенесённой строки */ +#define IF_NOWRAP 0x02u /* строка не переносится (кодовый блок / HR / таблица) */ +#define IF_BLANK 0x04u /* визуально пустая строка */ +#define IF_CODE 0x08u /* тело fenced code-блока (verbatim-режим) */ +#define IF_HSCROLL 0x10u /* блок горизонтально скроллируется ЦЕЛИКОМ (код/таблица), + * включая строки короче 80 — двигаются как единый блок. + * НЕ ставится на HR и границы fence (им нечего прятать). */ + +/* ---- Словарь сегментов/стилей (общий для индексатора и таблиц) ---- */ +/* Тип сегмента-продолжения, передаваемый в emit_seg()/inline_scan(). */ +#define CK_PLAIN 0 +#define CK_QUOTE 1 +#define CK_LIST 2 +#define CK_OTHER 3 + +/* Начальный inline-стиль сегмента (INIT_STYLE_*). */ +#define INIT_STYLE_PLAIN 0x0 +#define INIT_STYLE_BOLD 0x1 +#define INIT_STYLE_ITALIC 0x2 +#define INIT_STYLE_UNDER 0x3 +#define INIT_STYLE_CODE 0x4 +#define INIT_STYLE_STRIKE 0x5 + +/* ---- Таблицы: CP866 light box-drawing (общий для индексатора и модуля) ---- */ +#define TBL_MAX_COLS 16 +#define TBL_ATTR ATTR_BOX +#define TBL_H 0xC4 /* ─ */ +#define TBL_V 0xB3 /* │ */ +#define TBL_TL 0xDA /* ┌ */ +#define TBL_TM 0xC2 /* ┬ */ +#define TBL_TR 0xBF /* ┐ */ +#define TBL_ML 0xC3 /* ├ */ +#define TBL_MM 0xC5 /* ┼ */ +#define TBL_MR 0xB4 /* ┤ */ +#define TBL_BL 0xC0 /* └ */ +#define TBL_BM 0xC1 /* ┴ */ +#define TBL_BR 0xD9 /* ┘ */ + /* ================================================================== * * Разделяемые символы mdview2.c, используемые модулем RAW. * ================================================================== */ @@ -85,4 +133,118 @@ uint8_t raw_key(uint8_t scan); /* навигация RAW; 1 = о void raw_screen_init(void); /* очистка экрана с нужным аттрибутом */ void raw_renorm(void); /* выровнять позицию при смене под-режима (F3) */ +/* ================================================================== * + * Запись директории рендер-кэша (одна на видимую строку, РОВНО 8 байт — + * cache_dir_get/put адресуют сдвигом idx<<3). Контент — len пар (char,attr). + * ================================================================== */ +typedef struct cache_rec_s { + uint8_t page; /* EMM-страница рендер-кэша (физический номер) */ + uint16_t off; /* смещение в странице, 0..16383 (байты, не ячейки) */ + uint8_t len; /* длина контента в ЯЧЕЙКАХ (char,attr пар), 0..255; 0 = пустая/HR */ + uint8_t flags; /* копия IF_NOWRAP/IF_CODE/IF_BLANK на момент рендера */ + uint8_t reserved; /* не используется */ + uint8_t pad[2]; /* явный резерв, добивка до 8 байт */ +} cache_rec_t; + +void cache_dir_get(uint16_t idx, cache_rec_t *r); /* читатель директории кэша (ядро) */ +void put_str_attr(uint8_t x, uint8_t y, const char *s, uint8_t attr); /* печать строки с атрибутом */ + +/* ================================================================== * + * Состояние навигации/представления (определено в mdview2.c). + * ================================================================== */ +extern uint16_t n_lines; /* всего строк в индексе текущего набора */ +extern uint16_t top_line; /* верхняя видимая строка */ +extern uint8_t viewport_x; /* горизонтальный сдвиг (nowrap-строки) */ +extern char filename[]; /* имя файла для статус-бара */ +extern uint8_t g_loading; /* 1 во время index_lines() */ +extern uint8_t g_ready; /* 1 когда первичный документ построен (F2 RAW) */ +extern uint8_t g_f8_enabled; /* можно ли сейчас переключать кодировку */ + +/* ================================================================== * + * Отрисовка области/статус-бара/меню и прокрутка (mdview2_view.c). + * ================================================================== */ +void draw_viewport_from_cache(void); /* перерисовать область документа из кэша */ +uint16_t drawable_lines(void); /* число строк, готовых к показу */ +void render_status_numbers(void); /* числовая часть статус-бара */ +void render_full_status(void); /* полная перерисовка статус-бара */ +void render_updated_status(void); /* обновить только числа статус-бара */ +void render_menu(void); /* перерисовать строку меню */ +void clamp_top(void); /* привести top_line к диапазону */ +void scroll_up(uint16_t n); +void scroll_down(uint16_t n); +void scroll_h(int8_t delta); /* горизонтальный сдвиг nowrap-блоков */ + +/* ================================================================== * + * API модуля справки (mdview2_help.c). + * ================================================================== */ +void show_help(void); /* модальный диалог F1 */ + +/* ================================================================== * + * Парсер/индексатор (mdview2_index.c) и разделяемое с ним состояние. + * Хранилище индекса (index_blk/pages/phys, n_lines/max_lines) — в ядре + * (аллокация/doc-slot), индексатор пишет в него; cache_*-писатели и + * progress_tick/spinner_tick ядро экспортирует индексатору. + * ================================================================== */ +#define INDEX_RECS_PER_PAGE 2048u /* 16384 / 8; запись всегда в одной странице */ +#define MAX_CACHE_LINE_LEN 255u /* кап длины контента строки в ячейках (буфер g_cells) */ + +extern uint16_t max_lines; /* ёмкость индекса: index_pages * 2048 */ +extern uint8_t index_blk, index_pages; /* дескриптор EMM-блока индекса */ +extern uint8_t index_phys[]; /* физ. страницы индекса */ +extern uint8_t index_truncated; /* индекс упёрся в ёмкость */ +extern uint8_t g_abort; /* F10/Esc во время загрузки → прервать индексацию */ + +uint8_t cache_reserve(uint16_t nbytes, uint8_t *out_page, uint16_t *out_off); +void cache_commit(uint8_t page, uint16_t off, const void *buf, uint16_t len); +void cache_dir_put(uint16_t idx, const cache_rec_t *r); +void spinner_tick(void); /* анимация индикатора загрузки */ +void progress_tick(void); /* кооперативный шаг loading-loop (рисует/листает) */ +void index_lines(void); /* построить индекс по загруженному файлу */ + +/* ================================================================== * + * Буфер ячеек рендера (mdview2.c) — общий с модулем таблиц. + * Таблицы эмитят свои строки теми же примитивами, что и индексатор. + * ================================================================== */ +extern uint8_t g_ncells; /* ячеек в текущем сегменте */ +void gc_put(char ch, uint8_t attr); /* добавить ячейку в g_cells */ +void gc_fill(char ch, uint8_t attr, uint8_t n); /* n одинаковых ячеек */ +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); + +/* ================================================================== * + * API модуля таблиц (mdview2_table.c) — вызывается из index_lines(). + * ================================================================== */ +uint32_t row_end(uint32_t p); /* offset завершающего '\n' (или file_size) */ +uint32_t table_first_cell(uint32_t row_start); /* контент первой ячейки строки */ +uint8_t table_next_cell(uint32_t *pp, uint32_t lineend, uint32_t *cs, uint32_t *ce); +uint8_t table_is_sep_row(uint32_t row_start, uint32_t lineend); +void table_border(const uint8_t *widths, uint8_t ncols, char left, char mid, char right); +void table_data_row(uint32_t row_start, uint32_t lineend, const uint8_t *widths, uint8_t ncols); + +/* ================================================================== * + * Кодировки и UTF-8 конвертация (mdview2_enc.c). + * Состояние кодека владеется модулем; build_doc()/index_lines() ядра + * оркестрируют его (привязка UTF-буфера к живому file_*, сборка наборов). + * ================================================================== */ +extern uint8_t g_encoding; /* активная кодировка (ENC_*) */ +extern uint8_t g_utf_building; /* 1 пока UTF-конвертация не дошла до конца исходника */ +extern uint8_t utf_avail; /* 1 = страницы UTF-8 выделены */ +extern uint8_t utf_blk, utf_pages; /* дескриптор EMM-буфера UTF-набора */ +extern uint8_t utf_phys[]; /* физ. страницы UTF-буфера */ +extern uint32_t utf_size; /* размер сконвертированного UTF-потока */ + +/* Ядро-owned состояние, читаемое кодеком (определено в mdview2.c). */ +extern uint8_t cur_page; /* текущая страница файла в W3 (инвалидация маппинга) */ +extern uint8_t orig_file_phys[]; /* физ. страницы снимка исходного файла */ +extern uint8_t orig_file_pages; +extern uint32_t orig_file_size; + +void set_encoding(uint8_t enc); /* активная кодировка + таблица ремапа */ +const char *enc_name(uint8_t enc); /* короткое имя для статус-бара */ +uint8_t detect_encoding(void); /* автоопределение по сэмплу байт */ +void win_rest_remap(uint8_t row, uint8_t w, uint8_t page, uint16_t off); /* вывод среза с ремапом */ +void utf_convert_more(uint32_t target); /* инкрементальная конвертация до target байт */ +uint8_t utf_alloc(void); /* выделить EMM-страницы под UTF-набор; 0 = нет EMM */ + #endif /* MDVIEW2_H */ diff --git a/examples/mdview2/mdview2_enc.c b/examples/mdview2/mdview2_enc.c new file mode 100644 index 0000000..643288d --- /dev/null +++ b/examples/mdview2/mdview2_enc.c @@ -0,0 +1,295 @@ +/* + * mdview2_enc.c — кодировки и конвертация UTF-8 → CP866. + * + * Две связанные подсистемы: + * 1. 8-битные кодировки (CP866/CP1251/KOI8-R) — различаются только ремапом + * старших байтов [128-255] на ОТРИСОВКЕ (g_remap, win_rest_remap). + * Автоопределение — detect_encoding по частоте ходовых букв. + * 2. UTF-8 — отдельный набор: исходник инкрементально декодируется в + * utf_phys[] как чистый CP866 (utf_convert_more), после чего весь + * конвейер индексации/кэша работает по нему без ремапа. + * + * Владеет состоянием кодека (conv-буфер, позиция конвертации, UTF-страницы); + * описание разделяемых символов и API — в mdview2.h. Оркестрацией (привязка + * UTF-буфера к живому file_*, сборка наборов) занимается build_doc() ядра. + */ + +#include +#include /* COLOR()/COLOR_* для ATTR_* в attr_is_content */ +#include /* sprinter_page_w3 */ +#include /* bank_read/bank_write/mem_alloc_pages/mem_get_page */ +#include "mdview2.h" + +/* ---- Состояние кодировки/UTF (владелец — этот модуль) ------------- */ +uint8_t g_encoding = ENC_CP866; /* активная кодировка */ + +uint8_t utf_blk = 0; +uint8_t utf_pages = 0; +uint8_t utf_phys[MAX_PAGES] = {0}; +uint32_t utf_size = 0; +uint8_t utf_avail = 0; /* 1 = страницы UTF-8 выделены */ +uint8_t g_utf_building = 0; /* 1 пока конвертация не дошла до конца исходника */ + +/* Приватное состояние конвертера (наружу не торчит — сброс через + * utf_conv_reset(), чтение оригинала через cv_read). */ +static uint32_t utf_src = 0; /* позиция чтения ОРИГИНАЛА (utf-8) */ +static uint8_t cv_page = 0xFF; /* orig-страница в W3 для cv_read (своя от cur_page) */ + +/* Таблицы ремапа СТАРШИХ байтов [128-255] в CP866. Индексируются (ch-0x80): + * младшие 128 (ASCII) — identity, в ремапе не участвуют (win_rest_remap + * трогает только ch>=0x80), поэтому в таблицах их нет. Кириллица + ходовая + * пунктуация; неизвестное → '?' (0x3F). Сгенерированы Python codecs. */ +static const uint8_t cp1251_to_866[128] = { + /* 80 */ 0x3F, 0x3F, 0x27, 0x3F, 0x22, 0x2E, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3C, 0x3F, 0x3F, 0x3F, 0x3F, + /* 90 */ 0x3F, 0x27, 0x27, 0x22, 0x22, 0x07, 0x2D, 0x2D, 0x3F, 0x3F, 0x3F, 0x3E, 0x3F, 0x3F, 0x3F, 0x3F, + /* A0 */ 0xFF, 0xF6, 0xF7, 0x3F, 0xFD, 0x3F, 0x3F, 0x3F, 0xF0, 0x63, 0xF2, 0x3C, 0x3F, 0x3F, 0x72, 0xF4, + /* B0 */ 0xF8, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0xFA, 0xF1, 0xFC, 0xF3, 0x3E, 0x3F, 0x3F, 0x3F, 0xF5, + /* C0 */ 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F, + /* D0 */ 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F, + /* E0 */ 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, + /* F0 */ 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, +}; +static const uint8_t koi8r_to_866[128] = { + /* 80 */ 0xC4, 0xB3, 0xDA, 0xBF, 0xC0, 0xD9, 0xC3, 0xB4, 0xC2, 0xC1, 0xC5, 0xDF, 0xDC, 0xDB, 0xDD, 0xDE, + /* 90 */ 0xB0, 0xB1, 0xB2, 0x3F, 0xFE, 0xF9, 0xFB, 0x3F, 0x3F, 0x3F, 0xFF, 0x3F, 0xF8, 0x3F, 0xFA, 0x3F, + /* A0 */ 0xCD, 0xBA, 0xD5, 0xF1, 0xD6, 0xC9, 0xB8, 0xB7, 0xBB, 0xD4, 0xD3, 0xC8, 0xBE, 0xBD, 0xBC, 0xC6, + /* B0 */ 0xC7, 0xCC, 0xB5, 0xF0, 0xB6, 0xB9, 0xD1, 0xD2, 0xCB, 0xCF, 0xD0, 0xCA, 0xD8, 0xD7, 0xCE, 0x63, + /* C0 */ 0xEE, 0xA0, 0xA1, 0xE6, 0xA4, 0xA5, 0xE4, 0xA3, 0xE5, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, + /* D0 */ 0xAF, 0xEF, 0xE0, 0xE1, 0xE2, 0xE3, 0xA6, 0xA2, 0xEC, 0xEB, 0xA7, 0xE8, 0xED, 0xE9, 0xE7, 0xEA, + /* E0 */ 0x9E, 0x80, 0x81, 0x96, 0x84, 0x85, 0x94, 0x83, 0x95, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, + /* F0 */ 0x8F, 0x9F, 0x90, 0x91, 0x92, 0x93, 0x86, 0x82, 0x9C, 0x9B, 0x87, 0x98, 0x9D, 0x99, 0x97, 0x9A, +}; + +/* Устанавливает активную кодировку и таблицу ремапа (для 8-битных). */ +void set_encoding(uint8_t enc) +{ + g_encoding = enc; + g_remap = (enc == ENC_CP1251) ? cp1251_to_866 : + (enc == ENC_KOI8R) ? koi8r_to_866 : (const uint8_t *)0; +} + +/* Самые ходовые строчные русские буквы (о е а и н т с р в л) — их байты + * различают 8-битные кодировки по частоте. */ +static const uint8_t common866 [10] = {0xAE,0xA5,0xA0,0xA8,0xAD,0xE2,0xE1,0xE0,0xA2,0xAB}; +static const uint8_t common1251[10] = {0xEE,0xE5,0xE0,0xE8,0xED,0xF2,0xF1,0xF0,0xE2,0xEB}; +static const uint8_t commonkoi [10] = {0xCF,0xC5,0xC1,0xC9,0xCE,0xD4,0xD3,0xD2,0xD7,0xCC}; +static uint8_t in_set10(const uint8_t *s, uint8_t b) +{ + for (uint8_t i = 0; i < 10; i++) if (s[i] == b) return 1; + return 0; +} + +/* Автоопределение кодировки дешёвым сканом байтов (до построения индекса). + * BOM → UTF8; иначе валидность UTF-8 (структура multibyte); иначе 8-бит по + * частоте ходовых букв; фолбэк CP866. */ +uint8_t detect_encoding(void) +{ + if (file_size >= 3 && (uint8_t)fb(0) == 0xEF && (uint8_t)fb(1) == 0xBB && (uint8_t)fb(2) == 0xBF) + return ENC_UTF8; + + uint32_t n = file_size; + if (n > 4096u) n = 4096u; /* сэмпл: первые 4 КБ — детекции хватает */ + uint8_t utf_ok = 1, has_mb = 0, has_high = 0, cont = 0; + uint16_t s866 = 0, s1251 = 0, skoi = 0; + + for (uint32_t p = 0; p < n; p++) { + uint8_t b = (uint8_t)fb(p); + if (b < 0x80) { if (cont) { utf_ok = 0; cont = 0; } continue; } + has_high = 1; + if (in_set10(common866, b)) s866++; + if (in_set10(common1251, b)) s1251++; + if (in_set10(commonkoi, b)) skoi++; + if (cont) { + if ((b & 0xC0) == 0x80) cont--; + else { utf_ok = 0; cont = 0; } + } else if (b >= 0xC2 && b <= 0xDF) { cont = 1; has_mb = 1; } + else if (b >= 0xE0 && b <= 0xEF) { cont = 2; has_mb = 1; } + else if (b >= 0xF0 && b <= 0xF4) { cont = 3; has_mb = 1; } + else utf_ok = 0; /* битый лид/одиночный континюэйшн */ + } + /* Незавершённая multibyte-последовательность на КОНЦЕ — нарушение только + * если это настоящий EOF; на границе сэмпла (n= s866 && s1251 >= skoi) return ENC_CP1251; + if (skoi >= s866) return ENC_KOI8R; + return ENC_CP866; +} + +const char *enc_name(uint8_t enc) +{ + return (enc == ENC_CP1251) ? "CP1251" : + (enc == ENC_KOI8R) ? "KOI8-R" : + (enc == ENC_UTF8) ? "UTF-8 " : "CP866 "; +} + +/* Контентный глиф (ремапим при смене кодировки) vs структурный (рамка/HR/ + * маркеры — уже CP866, не трогаем). Различаем по attr. */ +static uint8_t attr_is_content(uint8_t a) +{ + return (uint8_t)(a != ATTR_BOX && a != ATTR_HR && + a != ATTR_LIST_MARKER && a != ATTR_QUOTE_MARKER); +} + +/* Вывод среза строки кэша с ремапом контентных глифов [128-255] через + * g_remap (CP1251/KOI8). Без активной таблицы — прямой win_rest. */ +void win_rest_remap(uint8_t row, uint8_t w, uint8_t page, uint16_t off) +{ + if (!g_remap || w == 0) { win_rest(row, 0, 1, w, page, off); return; } + uint8_t buf[SCREEN_W * 2]; + bank_read(page, off, buf, (uint16_t)w * 2u); + for (uint8_t i = 0; i < w; i++) { + uint8_t ch = buf[(uint16_t)i * 2u]; + if (ch >= 0x80 && attr_is_content(buf[(uint16_t)i * 2u + 1u])) + buf[(uint16_t)i * 2u] = g_remap[ch - 0x80]; /* таблицы хранят только старшие 128 */ + } + bank_write(g_scratch_phys, 0, buf, (uint16_t)w * 2u); + win_rest(row, 0, 1, w, g_scratch_phys, 0); +} + +/* ========================= UTF-8 → CP866 (Фаза 2) ======================== */ + +/* Кириллица U+0400..U+045F → CP866. Русский набор + Ё/ё и часть украинских + * (Є є Ї ї Ў ў), которые есть в CP866; пропуски → '?' (0x3F). */ +static const uint8_t utf_cyr_to_866[96] = { + /* 0400 */ 0x3F, 0xF0, 0x3F, 0x3F, 0xF2, 0x3F, 0x3F, 0xF4, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0xF6, 0x3F, + /* 0410 */ 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F, + /* 0420 */ 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F, + /* 0430 */ 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, + /* 0440 */ 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, + /* 0450 */ 0x3F, 0xF1, 0x3F, 0x3F, 0xF3, 0x3F, 0x3F, 0xF5, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0xF7, 0x3F, +}; + +/* Поток вывода конвертера: пишем в utf_phys чанками по странице, буфер в + * near-памяти, флаш через один bank_write (не побайтовый swap W3). */ +static uint8_t conv_buf[256]; +static uint16_t conv_n = 0; /* байт в буфере, ещё не сброшено */ +static uint8_t conv_page = 0; /* индекс текущей страницы в utf_phys */ +static uint16_t conv_off = 0; /* уже сброшенное смещение в этой странице */ + +static void conv_flush(void) +{ + if (conv_n) { + bank_write(utf_phys[conv_page], conv_off, conv_buf, conv_n); + conv_off = (uint16_t)(conv_off + conv_n); + conv_n = 0; + } +} + +/* Добавляет один CP866-байт в выходной поток. Запись никогда не пересекает + * границу страницы (256 делит 16384 нацело). */ +static void conv_put(uint8_t b) +{ + if ((uint16_t)(conv_off + conv_n) == PAGE_SIZE) { /* страница заполнена */ + conv_flush(); + conv_page++; + conv_off = 0; + } + conv_buf[conv_n++] = b; + if (conv_n == (uint16_t)sizeof(conv_buf)) conv_flush(); +} + +/* Подстановки одиночных не-кириллических символов UTF-8 → один CP866-байт. + * Таблица вместо switch (экономия кода); линейный поиск дёшев — вызывается + * лишь при конвертации на редких символьных кодпойнтах. Чтобы добавить новый + * символ — достаточно дописать одну строку {кодпойнт, байт-CP866}. + * (Символы с НЕ-1:1 заменой, напр. … -> "...", обрабатываются отдельно ниже.) */ +typedef struct { uint16_t utf8; uint8_t cp866; } utf_sym_t; +static const utf_sym_t utf_sym[] = { + {0x00A0, 0x20}, {0x00AB, 0x3C}, {0x00BB, 0x3E}, /* nbsp « » → space < > */ + {0x00B0, 0xF8}, {0x00B7, 0xFA}, /* ° · */ + {0x2013, 0x2D}, {0x2014, 0x2D}, /* – — → '-' */ + {0x2018, 0x27}, {0x2019, 0x27}, /* ‘ ’ → '\'' */ + {0x201C, 0x22}, {0x201D, 0x22}, /* “ ” → '"' */ + {0x2022, 0xF9}, {0x2116, 0xFC}, /* • → ∙ № */ + {0x2190, 0x1B}, {0x2191, 0x18}, {0x2192, 0x1A}, {0x2193, 0x19}, /* ← ↑ → ↓ */ + {0x2500, 0xC4}, {0x2502, 0xB3}, {0x250C, 0xDA}, {0x2510, 0xBF}, /* ─ │ ┌ ┐ */ + {0x2514, 0xC0}, {0x2518, 0xD9}, {0x251C, 0xC3}, {0x2524, 0xB4}, /* └ ┘ ├ ┤ */ + {0x252C, 0xC2}, {0x2534, 0xC1}, {0x253C, 0xC5}, /* ┬ ┴ ┼ */ + {0x2580, 0xDF}, {0x2584, 0xDC}, {0x2588, 0xDB}, /* ▀ ▄ █ */ + {0x2591, 0xB0}, {0x2592, 0xB1}, {0x2593, 0xB2}, {0x25A0, 0xFE}, /* ░ ▒ ▓ ■ */ + {0x2713, 0xFB}, {0x2714, 0xFB}, /* ✓ ✔ → галка */ + {0x2715, 0x78}, {0x2717, 0x78}, {0x2718, 0x78}, /* ✕ ✗ ✘ → 'x' */ +}; +#define UTF_SYM_N (uint8_t)(sizeof(utf_sym) / sizeof(utf_sym[0])) + +/* Кодпойнт → CP866-глиф(ы). Кириллица по таблице; спецслучаи (…, BOM) явно; + * остальные ходовые символы по таблице utf_sym[]; прочее → '?'. */ +static void conv_emit_cp(uint32_t cp) +{ + if (cp < 0x80) { conv_put((uint8_t)cp); return; } + if (cp >= 0x0400 && cp <= 0x045F) { conv_put(utf_cyr_to_866[cp - 0x0400]); return; } + if (cp == 0x2026) { conv_put('.'); conv_put('.'); conv_put('.'); return; } /* … → "..." */ + if (cp == 0xFEFF) return; /* BOM/ZWNBSP — выкинуть */ + for (uint8_t i = 0; i < UTF_SYM_N; i++) + if (utf_sym[i].utf8 == (uint16_t)cp) { conv_put(utf_sym[i].cp866); return; } + conv_put('?'); +} + +/* Последовательное чтение байта ОРИГИНАЛА (utf-8) конвертером. Оригинал лежит + * в orig_file_phys[]; мапим его страницу в W3 напрямую, со своей кэш-переменной + * cv_page (отдельной от cur_page индексатора, т.к. оба используют W3 и + * чередуются). На границе с fb()-чтением utf-буфера кэши взаимно сбрасываются. */ +static uint8_t cv_read(uint32_t s) +{ + uint8_t pg = (uint8_t)(s >> PAGE_BITS); + if (pg != cv_page) { sprinter_page_w3(orig_file_phys[pg]); cv_page = pg; } + return *((volatile uint8_t *)(0xC000u + (uint16_t)(s & PAGE_MASK))); +} + +/* Возобновляемая конвертация: дописывает utf_phys из оригинала, пока + * сконвертированных (flushed) байт меньше target и не достигнут конец + * исходника. По выходу file_size = доступная (сконвертированная) часть; при + * достижении конца фиксирует utf_size и снимает g_utf_building. */ +void utf_convert_more(uint32_t target) +{ + cv_page = 0xFF; /* W3 был на utf (fb индексатора) — пере-смаппим orig */ + uint32_t n = orig_file_size; + while (utf_src < n && + (uint32_t)((uint32_t)conv_page * PAGE_SIZE + conv_off) < target) { + uint8_t b = (uint8_t)cv_read(utf_src++); + if (b < 0x80) { conv_emit_cp(b); continue; } + + uint32_t cp; + uint8_t need; + if ((b & 0xE0) == 0xC0) { cp = (uint32_t)(b & 0x1F); need = 1; } + else if ((b & 0xF0) == 0xE0) { cp = (uint32_t)(b & 0x0F); need = 2; } + else if ((b & 0xF8) == 0xF0) { cp = (uint32_t)(b & 0x07); need = 3; } + else { conv_put('?'); continue; } + + uint8_t ok = 1; + for (uint8_t k = 0; k < need; k++) { + if (utf_src >= n) { ok = 0; break; } + uint8_t cb = (uint8_t)cv_read(utf_src); + if ((cb & 0xC0) != 0x80) { ok = 0; break; } + cp = (cp << 6) | (uint32_t)(cb & 0x3F); + utf_src++; + } + if (!ok || need == 3) { conv_put('?'); continue; } + conv_emit_cp(cp); + } + conv_flush(); + file_size = (uint32_t)conv_page * PAGE_SIZE + conv_off; /* доступно индексатору */ + if (utf_src >= n) { utf_size = file_size; g_utf_building = 0; } + cur_page = 0xFF; /* W3 был на orig — fb индексатора пере-смаппит utf */ +} + +/* Выделяет страницы под UTF-8 набор (конвертированный ≤ оригинала по размеру) + * и сбрасывает приватное состояние конвертера в начало (conv-буфер, utf_src, + * g_utf_building). Сама конвертация — инкрементальная, через utf_convert_more(). + * Возврат: 1 — успех, 0 — нет EMM. */ +uint8_t utf_alloc(void) +{ + utf_pages = orig_file_pages; + utf_blk = mem_alloc_pages(utf_pages); + if (utf_blk == 0) return 0; + for (uint8_t i = 0; i < utf_pages; i++) + utf_phys[i] = mem_get_page(utf_blk, i); + conv_n = 0; conv_page = 0; conv_off = 0; /* converter reset (вызывается один раз) */ + utf_src = 0; + g_utf_building = 1; + return 1; +} diff --git a/examples/mdview2/mdview2_help.c b/examples/mdview2/mdview2_help.c new file mode 100644 index 0000000..99b654e --- /dev/null +++ b/examples/mdview2/mdview2_help.c @@ -0,0 +1,102 @@ +/* + * mdview2_help.c — диалог справки (F1). + * + * Модальное окно поверх документа: рамка с заголовком, список горячих + * клавиш и поддерживаемых markdown-элементов. Блокирует до нажатия любой + * клавиши, после чего восстанавливает статус-бар, область документа и меню. + * + * Зависимости от ядра (mdview2.c): render_full_status / draw_viewport_from_cache + * / render_menu для восстановления экрана. Текст справки — в CP866. + */ + +#include +#include +#include +#include +#include "mdview2.h" + +/* Геометрия диалога справки (в символьных координатах 80×32). */ +#define HELP_X 8u /* левая граница рамки */ +#define HELP_Y 4u /* верхняя граница рамки */ +#define HELP_W 64u /* ширина рамки (включая │) */ +#define HELP_H 26u /* высота рамки (включая ─) */ + +/* Заполняет одну внутреннюю строку диалога (r=0 — первая строка за рамкой). + * Строка s в кодировке CP866; остаток до края дополняется пробелами. */ +static void help_line(uint8_t r, const char *s, uint8_t attr) +{ + uint8_t x = HELP_X + 1u; + uint8_t y = HELP_Y + 1u + r; + uint8_t len = (uint8_t)strlen(s); + if (len > HELP_W - 2u) len = HELP_W - 2u; + + bios_set_place(y, x); + if (len > 0) bios_writeattr(s, len, attr); + /* place уже продвинут bios_writeattr на len колонок (verified). */ + if (len < HELP_W - 2u) { + bios_fillcharattr(' ', ATTR_HELP_BG, (uint8_t)(HELP_W - 2u - len)); + } +} + +void show_help(void) +{ + /* " Помощь " в CP866 (8 байт) */ + static const char title[] = " \x8F\xAE\xAC\xAE\xE9\xEC "; + uint8_t tlen = 8u; + uint8_t lft = (uint8_t)((HELP_W - 2u - tlen) / 2u); /* = 27 */ + uint8_t rgt = (uint8_t)(HELP_W - 2u - tlen - lft); + /* Верхняя граница рамки с заголовком по центру. + * place продвигается каждым BIOS-вызовом (verified), поэтому + * достаточно одной установки места на всю строку рамки. */ + wrchar(HELP_X, HELP_Y, 0xDA, ATTR_HELP_BDR); /* ┌ */ + bios_set_place(HELP_Y, (uint8_t)(HELP_X + 1u)); + if (lft > 0) bios_fillcharattr(0xC4, ATTR_HELP_BDR, lft); + if (tlen > 0) bios_writeattr(title, tlen, ATTR_HELP_TIT); + if (rgt > 0) bios_fillcharattr(0xC4, ATTR_HELP_BDR, rgt); + wrchar(HELP_X + HELP_W - 1u, HELP_Y, 0xBF, ATTR_HELP_BDR); /* ┐ */ + + /* Боковые границы (левый и правый │ для каждой строки тела) */ + for (uint8_t r = 1u; r < HELP_H - 1u; r++) { + wrchar(HELP_X, HELP_Y + r, 0xB3, ATTR_HELP_BDR); /* │ */ + wrchar(HELP_X + HELP_W - 1u, HELP_Y + r, 0xB3, ATTR_HELP_BDR); /* │ */ + } + + /* Нижняя граница рамки */ + wrchar(HELP_X, HELP_Y + HELP_H - 1u, 0xC0, ATTR_HELP_BDR); /* └ */ + bios_set_place((uint8_t)(HELP_Y + HELP_H - 1u), (uint8_t)(HELP_X + 1u)); + bios_fillcharattr(0xC4, ATTR_HELP_BDR, (uint8_t)(HELP_W - 2u)); + wrchar(HELP_X + HELP_W - 1u, HELP_Y + HELP_H - 1u, 0xD9, ATTR_HELP_BDR); /* ┘ */ + + /* Содержимое (20 внутренних строк) */ + uint8_t r = 0; + help_line(r++, "", ATTR_HELP_BG); + help_line(r++, " MDView v0.2 -- Markdown Viewer for Sprinter", ATTR_HELP_HDR); + help_line(r++, " (c) 2026 Petrov A.G.", ATTR_HELP_BG); + help_line(r++, "", ATTR_HELP_BG); + help_line(r++, " Navigation:", ATTR_HELP_HDR); + help_line(r++, " \x18 \x19 Scroll one line up / down", ATTR_HELP_BG); + help_line(r++, " PgUp PgDn Scroll one page up / down", ATTR_HELP_BG); + help_line(r++, " Home End Jump to beginning / end of document", ATTR_HELP_BG); + help_line(r++, " \x1B \x1A Horizontal pan (code blocks/tables)", ATTR_HELP_BG); + help_line(r++, " Esc F10 Exit", ATTR_HELP_BG); + help_line(r++, "", ATTR_HELP_BG); + help_line(r++, " Markdown elements:", ATTR_HELP_HDR); + help_line(r++, " # ## ### Headings H1-H6", ATTR_HELP_BG); + help_line(r++, " **bold** *italic* `code` ~~strike~~", ATTR_HELP_BG); + help_line(r++, " > quote ``` ... ``` Fenced code block", ATTR_HELP_BG); + help_line(r++, " - * + item 1. 2. Ordered list", ATTR_HELP_BG); + help_line(r++, " |----|----| Tables", ATTR_HELP_BG); + help_line(r++, "", ATTR_HELP_BG); + help_line(r++, " Encoding:", ATTR_HELP_HDR); + help_line(r++, " F8 Cycle CP866 / CP1251 / KOI8-R / UTF-8", ATTR_HELP_BG); + help_line(r++, " Auto-detected on open; F8 to override", ATTR_HELP_BG); + help_line(r++, "", ATTR_HELP_BG); + help_line(r++, " File size: up to 128 KB (EMM). Lines: up to 16384.", ATTR_HELP_BG); + help_line(r++, "", ATTR_HELP_BG); + + (void)getkey(); + + render_full_status(); + draw_viewport_from_cache(); + render_menu(); +} diff --git a/examples/mdview2/mdview2_index.c b/examples/mdview2/mdview2_index.c new file mode 100644 index 0000000..3bc2f8e --- /dev/null +++ b/examples/mdview2/mdview2_index.c @@ -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 +#include +#include /* COLOR()/COLOR_* для ATTR_* в styles_map */ +#include +#include /* 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; + } +} diff --git a/examples/mdview2/mdview2_table.c b/examples/mdview2/mdview2_table.c new file mode 100644 index 0000000..d0d3aa5 --- /dev/null +++ b/examples/mdview2/mdview2_table.c @@ -0,0 +1,91 @@ +/* + * mdview2_table.c — выровненная отрисовка markdown-таблиц. + * + * Разбор |-разделённых ячеек и эмиссия рамок/строк в общий буфер ячеек + * рендера (gc_put/gc_fill, см. mdview2.h). Вызывается из index_lines() + * ядра при обнаружении таблицы. Геометрия рамок (TBL_n), словарь стилей + * (CK_n, INIT_STYLE_n) и cell-buffer API объявлены в mdview2.h. + */ + +#include +#include /* COLOR() / COLOR_* для атрибутов TBL_ATTR/ATTR_TEXT */ +#include "mdview2.h" + +uint32_t row_end(uint32_t p) +{ + while (p < file_size && fb(p) != '\n') p++; + return p; +} + +/* Начало контента первой ячейки строки (после ведущих пробелов и '|'). */ +uint32_t table_first_cell(uint32_t row_start) +{ + uint32_t p = row_start; + while (p < file_size && fb(p) == ' ') p++; + if (p < file_size && fb(p) == '|') p++; + return p; +} + +/* Следующая ячейка: [*cs,*ce) — обрезанный диапазон; *pp продвигается за '|'. + * 0 — ячеек больше нет (включая хвостовую пустоту после последнего '|'). */ +uint8_t table_next_cell(uint32_t *pp, uint32_t lineend, uint32_t *cs, uint32_t *ce) +{ + uint32_t p = *pp; + if (p >= lineend) return 0; + uint32_t a = p; + while (p < lineend && fb(p) != '|') p++; + uint32_t b = p; + uint8_t had_pipe = (uint8_t)(p < lineend && fb(p) == '|'); + if (had_pipe) p++; + *pp = p; + while (a < b && fb(a) == ' ') a++; + while (b > a && fb(b - 1) == ' ') b--; + if (!had_pipe && a == b) return 0; + *cs = a; *ce = b; + return 1; +} + +/* Строка-разделитель (|---|:-:|): каждая ячейка непустая и только '-'/':'. */ +uint8_t table_is_sep_row(uint32_t row_start, uint32_t lineend) +{ + uint32_t p = table_first_cell(row_start); + uint32_t cs, ce; + uint8_t n = 0; + while (table_next_cell(&p, lineend, &cs, &ce)) { + if (cs >= ce) return 0; + for (uint32_t q = cs; q < ce; q++) { char c = fb(q); if (c != '-' && c != ':') return 0; } + n++; + } + return (uint8_t)(n > 0); +} + +/* Горизонтальная рамка в g_cells: left + (H×(w+2) + mid|right) по колонкам. */ +void table_border(const uint8_t *widths, uint8_t ncols, char left, char mid, char right) +{ + gc_put(left, TBL_ATTR); + for (uint8_t c = 0; c < ncols; c++) { + gc_fill(TBL_H, TBL_ATTR, (uint8_t)(widths[c] + 2)); + gc_put((c == (uint8_t)(ncols - 1)) ? right : mid, TBL_ATTR); + } +} + +/* Строка данных в g_cells: │ <ячейка, добитая до widths[c]> │ … */ +void table_data_row(uint32_t row_start, uint32_t lineend, const uint8_t *widths, uint8_t ncols) +{ + uint32_t p = table_first_cell(row_start); + uint32_t cs, ce; + gc_put(TBL_V, TBL_ATTR); + for (uint8_t c = 0; c < ncols; c++) { + uint8_t got = table_next_cell(&p, lineend, &cs, &ce); + gc_put(' ', TBL_ATTR); + uint8_t rendered = 0; + if (got && ce > cs) { + uint8_t before = g_ncells; + (void)inline_scan(cs, ce, 0, CK_OTHER, INIT_STYLE_PLAIN, ATTR_TEXT, 1); + rendered = (uint8_t)(g_ncells - before); + } + if (rendered < widths[c]) gc_fill(' ', ATTR_TEXT, (uint8_t)(widths[c] - rendered)); + gc_put(' ', TBL_ATTR); + gc_put(TBL_V, TBL_ATTR); + } +} diff --git a/examples/mdview2/mdview2_view.c b/examples/mdview2/mdview2_view.c new file mode 100644 index 0000000..cf02bc6 --- /dev/null +++ b/examples/mdview2/mdview2_view.c @@ -0,0 +1,246 @@ +/* + * mdview2_view.c — представление: отрисовка области документа из рендер-кэша, + * статус-бар, нижнее меню и прокрутка (верт./гориз.). + * + * Работает поверх готового рендер-кэша (cache_dir_get → cache_rec_t) и + * состояния навигации (top_line/viewport_x/n_lines, см. mdview2.h). Никакого + * парсинга/обращения к исходному файлу: всё рисуется из (char,attr)-ячеек кэша + * через win_rest_remap (ремап 8-битных кодировок — в mdview2_enc.c). + */ + +#include +#include /* dec8/dec16 */ +#include /* wrchar/gotoxy/cputs/textattr/scroll/COLOR */ +#include /* bios_set_place/bios_fillcharattr */ +#include "mdview2.h" + +/* Фаза 4-5 — cache-only draw path: рисует одну строку ИЗ КЭША, без + * единого обращения к fb()/исходному файлу. Горизонтальный скролл для + * nowrap-строк (Фаза 5) — это просто смещение начала среза на vx*2 + * байт ВНУТРИ ТОГО ЖЕ кэш-буфера (cache_reserve гарантирует, что вся + * строка лежит в одной странице, так что off+vx*2 не пересекает + * границу страницы); не nowrap-строки игнорируют viewport_x, как и + * раньше в живом render_line() (там это называлось effective_vx). */ +static void draw_line_from_cache(uint16_t line_idx, uint8_t row) +{ + cache_rec_t rec; + cache_dir_get(line_idx, &rec); + + bios_set_place(row, 0); + bios_fillcharattr(' ', ATTR_TEXT, SCREEN_W); + + /* Горизонтальный сдвиг применяется ко ВСЕМ строкам hscroll-блока (код, + * таблицы) — по ТИПУ, а не по длине: блок двигается целиком, включая + * строки короче 80 (короткая строка, ушедшая за левый край, просто + * показывает маркер '<' на пустом месте). HR и границы fence не имеют + * IF_HSCROLL и не сдвигаются. */ + uint8_t vx = (rec.flags & IF_HSCROLL) ? viewport_x : 0; + + if (rec.len > vx) { + uint16_t remaining = (uint16_t)(rec.len - vx); + uint8_t w = (remaining > SCREEN_W) ? SCREEN_W : (uint8_t)remaining; + uint16_t off = (uint16_t)(rec.off + (uint16_t)vx * 2u); + win_rest_remap(row, w, rec.page, off); + if (remaining > SCREEN_W) { + wrchar(SCREEN_W - 1, row, '>', ATTR_TRUNC); + } + } + if (vx > 0) { + wrchar(0, row, '<', ATTR_TRUNC); + } +} + +/* Перерисовывает все VIEW_H строк окна из кэша — основной путь + * перерисовки видимой области после загрузки файла (Фаза 3-4). */ +void draw_viewport_from_cache(void) +{ + for (uint8_t i = 0; i < VIEW_H; i++) { + draw_line_from_cache((uint16_t)(top_line + i), (uint8_t)(VIEW_TOP_ROW + i)); + } +} + +/* Число строк, готовых к показу. Во время загрузки последняя эмитированная + * строка ещё в g_cells (не зафлашена), поэтому доступны [0..n_lines-2]. */ +uint16_t drawable_lines(void) +{ + if (g_loading) return (n_lines >= 1) ? (uint16_t)(n_lines - 1) : 0; + return n_lines; +} + +/* Возвращает процент прокрутки (0..100) относительно доступного диапазона. */ +static uint8_t calc_pct(void) +{ + uint16_t total = drawable_lines(); + uint8_t pct; + if (total <= VIEW_H) + pct = 100; + else + pct = (uint8_t)((uint32_t)top_line * 100UL / (uint32_t)(total - VIEW_H)); + return pct; +} + +/* Рисует динамическую часть статус-бара без printf. + * Полностью перезаписывает колонки 45..79, чтобы не оставлять «хвосты». */ +void render_status_numbers(void) +{ + uint16_t total = drawable_lines(); + uint16_t last = top_line + VIEW_H; + if (last > total) last = total; + uint8_t pct = calc_pct(); + + textattr(ATTR_BAR); + bios_set_place(0, 45); + bios_fillcharattr(' ', ATTR_BAR, (uint8_t)(SCREEN_W - 45)); + + wrchar(45, 0, 0xB3, ATTR_BAR); /* вертикальный разделитель */ + gotoxy(47, 0); + cputs("L "); + dec16(top_line + 1); + cputs("-"); + dec16(last); + cputs(" / "); + dec16(total); + if (g_loading) cputs("..."); /* ещё грузится */ + + wrchar(70, 0, 0xB3, ATTR_BAR); /* вертикальный разделитель */ + gotoxy( 73, 0); + dec8(pct); + cputs("%"); +} + +/* Полностью перерисовывает статус-бар (заголовок + числа). */ +void render_full_status(void) +{ + + fill_row(0, ATTR_BAR); + /* Размещение в шапке: "MDVIEW", затем слот спиннера и имя файла. */ + put_str_attr(1, 0, "MDVIEW", ATTR_BAR); + put_str_attr(10, 0, filename, ATTR_BAR); + put_str_attr(37, 0, enc_name(g_encoding), ATTR_BAR); /* текущая кодировка */ + render_status_numbers(); +} + +/* Обновляет только изменяемую числовую часть статус-бара. */ +void render_updated_status(void) +{ + render_status_numbers(); +} + +/* Отрисовывает нижнюю строку меню. */ +void render_menu(void) +{ + /* Блок i = колонка i*8 (8 шириной): 2 поз. НОМЕР клавиши без 'F' (стиль + * ATTR_MENU_K, ведущий «чёрный» пробел кроме F10) + 6 поз. ТЕКСТ-функция + * (ATTR_MENU_T) сразу за номером. Номера рисуем для ВСЕХ 10 клавиш; текст — + * только у задействованных и доступных сейчас. */ + fill_row(MENU_ROW, ATTR_MENU_T); + + char num[3]; num[2] = 0; + for (uint8_t i = 0; i < 9; i++) { /* F1..F9: ' 1'..' 9' */ + num[0] = ' '; num[1] = (char)('1' + i); + put_str_attr((uint8_t)(i * 8), MENU_ROW, num, ATTR_MENU_K); + } + put_str_attr(9 * 8, MENU_ROW, "10", ATTR_MENU_K); /* F10 */ + + put_str_attr(0 * 8 + 2, MENU_ROW, "Help", ATTR_MENU_T); +#if WITH_RAW + if (g_ready) /* RAW доступен после постройки документа */ + put_str_attr(1 * 8 + 2, MENU_ROW, (g_view == VIEW_MD) ? "RAW" : "MD", ATTR_MENU_T); + if (g_view != VIEW_MD) /* Wrap/Unwrap — только в RAW */ + put_str_attr(2 * 8 + 2, MENU_ROW, + (g_view == VIEW_RAW_WRAP) ? "UnWrap" : "Wrap", ATTR_MENU_T); +#endif + if (g_f8_enabled) /* смена кодировки сейчас возможна */ + put_str_attr(7 * 8 + 2, MENU_ROW, "CodePg", ATTR_MENU_T); + put_str_attr(9 * 8 + 2, MENU_ROW, "Exit", ATTR_MENU_T); +} + +/* ================================================================== + * Прокрутка + * ================================================================== */ + +/* Ограничивает top_line допустимым диапазоном с учётом высоты окна. */ +void clamp_top(void) +{ + if (n_lines <= VIEW_H) { + top_line = 0; + } else if (top_line > n_lines - VIEW_H) { + top_line = (uint16_t)(n_lines - VIEW_H); + } +} + +/* Прокрутка вверх на n строк с частичной перерисовкой при n == 1. */ +void scroll_up(uint16_t n) +{ + uint16_t new_top_line = (top_line >= n) ? (uint16_t)(top_line - n) : 0; + if( new_top_line != top_line) { + top_line = new_top_line; + if (n == 1) { + scroll(0, VIEW_TOP_ROW, SCREEN_W, VIEW_H, 2, 0); + draw_line_from_cache((uint16_t)(top_line), (uint8_t)(VIEW_TOP_ROW)); + } else { + draw_viewport_from_cache(); + } + } +} + +/* Прокрутка вниз на n строк с частичной перерисовкой при n == 1. */ +void scroll_down(uint16_t n) +{ + uint16_t new_top_line = (top_line + n < n_lines - VIEW_H) ? + (uint16_t)(top_line + n) : n_lines - VIEW_H; + if( new_top_line != top_line) { + top_line = new_top_line; + if (n == 1) { + scroll(0, VIEW_TOP_ROW, SCREEN_W, VIEW_H, 1, 0); + draw_line_from_cache((uint16_t)(top_line + VIEW_H - 1), (uint8_t)(VIEW_TOP_ROW + VIEW_H - 1)); + } else { + clamp_top(); + draw_viewport_from_cache(); + } + } +} + +/* Горизонтальный сдвиг (только если в окне есть nowrap-строки). + * Максимум сдвига ограничен самой широкой nowrap-строкой на экране. + * Фаза 5: ширина берётся прямо из директории кэша (rec.len — уже + * посчитанная реальная длина в ячейках после рендера), без единого + * обращения к fb()/seg_off()/исходному файлу. */ +void scroll_h(int8_t delta) +{ + uint16_t maxw = 0; + for (uint8_t i = 0; i < VIEW_H; i++) { + uint16_t li = (uint16_t)(top_line + i); + if (li >= n_lines) break; + cache_rec_t rec; + cache_dir_get(li, &rec); + if (!(rec.flags & IF_HSCROLL)) continue; /* только код/таблицы */ + if (rec.len > maxw) maxw = rec.len; /* самая широкая строка блока */ + } + if (maxw == 0) return; /* нет строк шире экрана */ + + /* Максимальный сдвиг = ширина за пределами экрана, в границах uint8. */ + uint16_t over = (maxw > SCREEN_W) ? (uint16_t)(maxw - SCREEN_W) : 0; + if (over > 248u) over = 248u; + uint8_t max_vx = (uint8_t)over; + + int16_t nx = (int16_t)viewport_x + delta; + if (nx < 0) nx = 0; + if (nx > (int16_t)max_vx) nx = max_vx; + + /* ПОРЯДОК ВАЖЕН: записываем viewport_x ДО сравнения, а сравниваем уже + * сохранённую копию old_vx с new_vx. Иначе SDCC генерирует для + * `if (new_vx != viewport_x) viewport_x = new_vx;` такой код: + * ld a, new_vx ; sub a,(viewport_x) ; jr Z,.. ; ld (viewport_x),a + * — т.е. для записи переиспользует регистр A, испорченный вычитанием в + * сравнении, и кладёт в viewport_x не new_vx, а (new_vx - old_vx). + * (asm-дамп 2026-06-24 подтвердил; даёт 10-8=2, 10-2=8 — ровно + * наблюдавшийся "прыжок" скролла.) Записав viewport_x первой, мы убираем + * портящий sub с пути записи; сравнение ниже только решает, перерисовывать + * ли экран. */ + uint8_t new_vx = (uint8_t)nx; + uint8_t old_vx = viewport_x; + viewport_x = new_vx; + if (new_vx != old_vx) + draw_viewport_from_cache(); +}