mdview2: расщепление монолита на модули (6 файлов, размер-нейтрально)
Вынос подсистем из mdview2.c в отдельные C-модули для читаемости и навигации. mdview2.c: 2740 → 1062 строк (-61%); ядро теперь чистая инфраструктура (cache-пул, EMM/fb, загрузка файла, doc-slot, loading-loop, build_doc, main). Модули (через EXTRA_SRCS, общий интерфейс в mdview2.h): - mdview2_help.c — диалог справки F1 - mdview2_table.c — выровненная отрисовка таблиц - mdview2_enc.c — кодировки CP866/CP1251/KOI8R + UTF-8 конвертер - mdview2_view.c — отрисовка области/статус-бара/меню + прокрутка - mdview2_index.c — парсер/индексатор markdown (сердце приложения) (mdview2_raw.c был выделен ранее) Чистый перенос static→extern; приватное состояние подсистем осталось приватным. Размер: exe 28090 → 28112 (+22 Б / +0.08% — codegen-шум на двух сильно связанных модулях enc/index). Сборка чистая, smoke-тест ОК. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
PROJ_ROOT := $(abspath $(CURDIR)/../..)
|
||||||
EXAMPLE := mdview2
|
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
|
MEMORY := small
|
||||||
|
|
||||||
include $(PROJ_ROOT)/app.mk
|
include $(PROJ_ROOT)/app.mk
|
||||||
|
|||||||
+45
-1723
File diff suppressed because it is too large
Load Diff
@@ -61,6 +61,54 @@
|
|||||||
#define VIEW_RAW_WRAP 1 /* RAW: длинные строки переносятся кратно 80 */
|
#define VIEW_RAW_WRAP 1 /* RAW: длинные строки переносятся кратно 80 */
|
||||||
#define VIEW_RAW_HSCROLL 2 /* RAW: длинные строки в одну, гориз. скролл */
|
#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.
|
* Разделяемые символы mdview2.c, используемые модулем RAW.
|
||||||
* ================================================================== */
|
* ================================================================== */
|
||||||
@@ -85,4 +133,118 @@ uint8_t raw_key(uint8_t scan); /* навигация RAW; 1 = о
|
|||||||
void raw_screen_init(void); /* очистка экрана с нужным аттрибутом */
|
void raw_screen_init(void); /* очистка экрана с нужным аттрибутом */
|
||||||
void raw_renorm(void); /* выровнять позицию при смене под-режима (F3) */
|
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 */
|
#endif /* MDVIEW2_H */
|
||||||
|
|||||||
@@ -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 <stdint.h>
|
||||||
|
#include <conio.h> /* COLOR()/COLOR_* для ATTR_* в attr_is_content */
|
||||||
|
#include <sprinter.h> /* sprinter_page_w3 */
|
||||||
|
#include <sprinter_mem.h> /* 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<file_size) это просто
|
||||||
|
* обрезка, не считаем за ошибку. */
|
||||||
|
if (cont && n == file_size) utf_ok = 0;
|
||||||
|
|
||||||
|
if (!has_high) return ENC_CP866; /* чистый ASCII */
|
||||||
|
if (utf_ok && has_mb) return ENC_UTF8;
|
||||||
|
if (s1251 >= 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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
/*
|
||||||
|
* mdview2_help.c — диалог справки (F1).
|
||||||
|
*
|
||||||
|
* Модальное окно поверх документа: рамка с заголовком, список горячих
|
||||||
|
* клавиш и поддерживаемых markdown-элементов. Блокирует до нажатия любой
|
||||||
|
* клавиши, после чего восстанавливает статус-бар, область документа и меню.
|
||||||
|
*
|
||||||
|
* Зависимости от ядра (mdview2.c): render_full_status / draw_viewport_from_cache
|
||||||
|
* / render_menu для восстановления экрана. Текст справки — в CP866.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <conio.h>
|
||||||
|
#include <bios/text.h>
|
||||||
|
#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();
|
||||||
|
}
|
||||||
@@ -0,0 +1,979 @@
|
|||||||
|
/*
|
||||||
|
* mdview2_index.c — парсер/индексатор markdown (сердце приложения).
|
||||||
|
*
|
||||||
|
* Единственный проход по исходному файлу (index_lines): классификация строк
|
||||||
|
* (classify_line, is_*_raw), сборка абзацев с переносом (scan_join_stream),
|
||||||
|
* inline-разметка (inline_marker/inline_scan) и эмиссия готовых (char,attr)-
|
||||||
|
* ячеек в рендер-кэш (gc_put → seg_commit_cells → cache_*). Ведёт индекс
|
||||||
|
* сегментов в EMM (idx_get/idx_put) и докручивает UTF-конвертацию впереди
|
||||||
|
* позиции чтения. Разделяемые символы и API — в mdview2.h.
|
||||||
|
*
|
||||||
|
* Приватно (не торчит в .h): idx_rec_t, cur_rec/cur_seg_off, буфер ячеек
|
||||||
|
* g_cells, styles_map, idx_get/idx_put и все классификаторы/сканеры.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <conio.h> /* COLOR()/COLOR_* для ATTR_* в styles_map */
|
||||||
|
#include <sprinter.h>
|
||||||
|
#include <sprinter_mem.h> /* bank_read/bank_write (idx_get/idx_put) */
|
||||||
|
#include "mdview2.h"
|
||||||
|
|
||||||
|
static uint8_t emph_to_attr(uint8_t emph, uint8_t base_attr); /* forward (нужна в inline-сканере) */
|
||||||
|
|
||||||
|
typedef struct idx_rec_s {
|
||||||
|
uint32_t off; /* смещение первого байта исходного текста для сегмента */
|
||||||
|
uint8_t flags; /* битовые флаги IF_* ниже */
|
||||||
|
uint8_t style; /* начальный стиль INIT_STYLE_* в начале сегмента */
|
||||||
|
uint8_t pad0; /* добивка до 8 байт (2048 записей на страницу 16 КБ) */
|
||||||
|
uint8_t pad1;
|
||||||
|
} idx_rec_t;
|
||||||
|
|
||||||
|
#define INDEX_REC_SIZE 8u
|
||||||
|
|
||||||
|
static uint32_t cur_seg_off = 0; /* смещение последнего emit_seg (кэш в near-памяти) */
|
||||||
|
static idx_rec_t cur_rec; /* near-копия последней записанной idx-записи */
|
||||||
|
|
||||||
|
/* Для проверки границ выделения считаем пробел/таб/конец строки/0 пробельными. */
|
||||||
|
static uint8_t ws_or_eol(char c)
|
||||||
|
{
|
||||||
|
return (uint8_t)(c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t ws_or_eol_or_delim(char c)
|
||||||
|
{
|
||||||
|
return (uint8_t)(c == ' ' || c == ',' || c == '.' || c == '!' || c == '?' || c == ':' || c == ';' || c == '/'
|
||||||
|
|| c == '\t' || c == '\n' || c == '\r' || c == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Маркер inline-выделения (*, **, _) распознаётся только если
|
||||||
|
* пробельная граница есть ровно с одной стороны (XOR-правило).
|
||||||
|
* Это защищает выражения вроде "2 * 3", "2 ** 3" и внутрисловные
|
||||||
|
* случаи (например COLOR_YELLOW) от ложного форматирования. */
|
||||||
|
static uint8_t is_emph_flanked(char prev_ch, char next_ch)
|
||||||
|
{
|
||||||
|
return (uint8_t)(ws_or_eol(prev_ch) != ws_or_eol_or_delim(next_ch));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Символ можно экранировать обратным слешем (\X): любая печатная ASCII-
|
||||||
|
* пунктуация (как в CommonMark). \* \_ \` \[ и т.п. → литерал X. */
|
||||||
|
static uint8_t is_escapable(char c)
|
||||||
|
{
|
||||||
|
return (uint8_t)(c >= '!' && c <= '~' &&
|
||||||
|
!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Типы сегмента CK_* и стили INIT_STYLE_* — в mdview2.h (общие с таблицами). */
|
||||||
|
|
||||||
|
/* Доступ к индексу в EMM.
|
||||||
|
* Размер записи 8 байт, поэтому запись не пересекает границу страницы
|
||||||
|
* и bank_read/bank_write работают с цельным блоком. */
|
||||||
|
static void idx_get(uint16_t idx, idx_rec_t *r)
|
||||||
|
{
|
||||||
|
uint8_t page = (uint8_t)(idx >> 11); /* номер страницы: idx / 2048 */
|
||||||
|
uint16_t off = (uint16_t)((idx & 2047u) << 3); /* смещение записи: (idx % 2048) * 8 */
|
||||||
|
bank_read(index_phys[page], off, r, INDEX_REC_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void idx_put(uint16_t idx, const idx_rec_t *r)
|
||||||
|
{
|
||||||
|
uint8_t page = (uint8_t)(idx >> 11);
|
||||||
|
uint16_t off = (uint16_t)((idx & 2047u) << 3);
|
||||||
|
bank_write(index_phys[page], off, r, INDEX_REC_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t seg_off(uint16_t idx)
|
||||||
|
{
|
||||||
|
idx_rec_t r;
|
||||||
|
if (idx >= n_lines) return file_size;
|
||||||
|
idx_get(idx, &r);
|
||||||
|
return r.off;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Установка флагов всегда идёт в последний emit_seg (n_lines-1).
|
||||||
|
* Используется near-копия cur_rec, чтобы не делать обратное чтение из банка. */
|
||||||
|
static void set_nowrap_cur(void) { cur_rec.flags |= IF_NOWRAP; idx_put((uint16_t)(n_lines - 1), &cur_rec); }
|
||||||
|
static void set_blank_cur(void) { cur_rec.flags |= IF_BLANK; idx_put((uint16_t)(n_lines - 1), &cur_rec); }
|
||||||
|
static void set_code_cur(void) { cur_rec.flags |= IF_CODE; idx_put((uint16_t)(n_lines - 1), &cur_rec); }
|
||||||
|
static void set_hscroll_cur(void){ cur_rec.flags |= IF_HSCROLL; idx_put((uint16_t)(n_lines - 1), &cur_rec); }
|
||||||
|
|
||||||
|
/* ==================================================================
|
||||||
|
* Индексация строк
|
||||||
|
* ================================================================== */
|
||||||
|
|
||||||
|
/* INIT_STYLE_* — в mdview2.h (общие с модулем таблиц). */
|
||||||
|
|
||||||
|
/* Таблица сопоставления init-style -> атрибут.
|
||||||
|
* Порядок должен совпадать с INIT_STYLE_* / EM_* ниже. */
|
||||||
|
static const uint8_t styles_map[] = {
|
||||||
|
ATTR_TEXT, ATTR_TEXT_BOLD, ATTR_TEXT_ITALIC, ATTR_TEXT_UNDERSORE, ATTR_TEXT_CODE, ATTR_TEXT_STRIKE
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Тип строки, который возвращает classify_line(). */
|
||||||
|
#define LK_PLAIN 0
|
||||||
|
#define LK_H1 1
|
||||||
|
#define LK_H2 2
|
||||||
|
#define LK_H3 3
|
||||||
|
#define LK_H4 4 /* также используется для H5/H6 */
|
||||||
|
#define LK_HR 5
|
||||||
|
#define LK_ULIST 6 /* маркер ненумерованного списка */
|
||||||
|
#define LK_OLIST 7 /* маркер нумерованного списка */
|
||||||
|
#define LK_QUOTE 8 /* маркер цитаты */
|
||||||
|
|
||||||
|
/* Классифицирует логическую строку.
|
||||||
|
* - Заголовки: 1..4 символа '#' и затем пробел/таб (H5/H6 сводятся к H4).
|
||||||
|
* - Горизонтальная линия: только '-', '*' или '_' (одного вида, >=3),
|
||||||
|
* возможно с пробелами/табами между символами.
|
||||||
|
* - Ненумерованный список: "- ", "* " или "+ " перед содержимым.
|
||||||
|
* - Нумерованный список: цифры + '.'/ ')' + пробел.
|
||||||
|
* - Цитата: '>' с опциональным пробелом.
|
||||||
|
* - Иначе обычный текст.
|
||||||
|
* Для заголовков/списков/цитат out_content указывает на первый байт
|
||||||
|
* содержимого после маркера и завершающего пробела. */
|
||||||
|
static uint8_t classify_line(uint32_t p_start, uint32_t *out_content)
|
||||||
|
{
|
||||||
|
if (p_start >= file_size) return LK_PLAIN;
|
||||||
|
|
||||||
|
char c0 = fb(p_start);
|
||||||
|
|
||||||
|
/* Заголовок? */
|
||||||
|
if (c0 == '#') {
|
||||||
|
uint8_t lvl = 0;
|
||||||
|
uint32_t p = p_start;
|
||||||
|
while (p < file_size && fb(p) == '#' && lvl < 6) {
|
||||||
|
lvl++; p++;
|
||||||
|
}
|
||||||
|
if (p < file_size) {
|
||||||
|
char ch = fb(p);
|
||||||
|
if (ch == ' ' || ch == '\t') {
|
||||||
|
p++;
|
||||||
|
*out_content = p;
|
||||||
|
if (lvl > 4) lvl = 4;
|
||||||
|
return lvl; /* LK_H1 .. LK_H4 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Символ '#' без пробела после него не считается заголовком. */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Горизонтальный разделитель.
|
||||||
|
* Проверяется до списков, чтобы `- - -` не стало маркером списка. */
|
||||||
|
if (c0 == '-' || c0 == '*' || c0 == '_') {
|
||||||
|
char marker = 0;
|
||||||
|
uint8_t count = 0;
|
||||||
|
uint8_t ok = 1;
|
||||||
|
uint32_t p = p_start;
|
||||||
|
while (p < file_size) {
|
||||||
|
char ch = fb(p++);
|
||||||
|
if (ch == '\n' || ch == '\r') break;
|
||||||
|
if (ch == ' ' || ch == '\t') continue;
|
||||||
|
if (ch != '-' && ch != '*' && ch != '_') { ok = 0; break; }
|
||||||
|
if (marker == 0) marker = ch;
|
||||||
|
else if (ch != marker) { ok = 0; break; }
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
if (ok && count >= 3) return LK_HR;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Для списков/цитат дополнительно допускаем ведущие пробелы
|
||||||
|
* (базовая поддержка вложенности). */
|
||||||
|
uint32_t lp = p_start;
|
||||||
|
while (lp < file_size && fb(lp) == ' ') lp++;
|
||||||
|
char cl = (lp < file_size) ? fb(lp) : 0;
|
||||||
|
|
||||||
|
/* Ненумерованный список? */
|
||||||
|
if ((cl == '-' || cl == '*' || cl == '+') &&
|
||||||
|
(lp + 1) < file_size &&
|
||||||
|
fb(lp + 1) == ' ') {
|
||||||
|
*out_content = lp + 2;
|
||||||
|
return LK_ULIST;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Нумерованный список? */
|
||||||
|
if (cl >= '0' && cl <= '9') {
|
||||||
|
uint32_t p = lp;
|
||||||
|
while (p < file_size) {
|
||||||
|
char ch = fb(p);
|
||||||
|
if (ch < '0' || ch > '9') break;
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
if (p < file_size) {
|
||||||
|
char ch = fb(p);
|
||||||
|
if ((ch == '.' || ch == ')') &&
|
||||||
|
(p + 1) < file_size && fb(p + 1) == ' ') {
|
||||||
|
*out_content = p + 2;
|
||||||
|
return LK_OLIST;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Цитата? */
|
||||||
|
if (cl == '>') {
|
||||||
|
uint32_t p = lp + 1;
|
||||||
|
if (p < file_size && fb(p) == ' ') p++;
|
||||||
|
*out_content = p;
|
||||||
|
return LK_QUOTE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return LK_PLAIN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Возвращает видимую ширину префикса (отступ + маркер) в колонках. */
|
||||||
|
static uint8_t marker_visible_col(uint8_t kind, uint32_t p_start, uint32_t content_off)
|
||||||
|
{
|
||||||
|
switch (kind) {
|
||||||
|
case LK_ULIST:
|
||||||
|
case LK_QUOTE:
|
||||||
|
case LK_OLIST: return (uint8_t)(content_off - p_start);
|
||||||
|
default: return 0; /* plain/headers спец-префикса не имеют */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================== Merge: сборка ячеек на этапе индекс-скана ==============
|
||||||
|
* Форвард-сканер (scan_join_stream/inline_scan + прямые emit'ы в index_lines)
|
||||||
|
* собирает готовые (char,attr)-ячейки в g_cells ПО ХОДУ единственного прохода
|
||||||
|
* по файлу — без отдельного второго скана. На каждом emit_seg ячейки
|
||||||
|
* ПРЕДЫДУЩего сегмента флашатся в кэш (lag-1: к этому моменту сегмент уже
|
||||||
|
* полностью просканирован); последний сегмент флашится после цикла index_lines.
|
||||||
|
* g_ncells_at_space — снимок длины на последнем пробеле для усечения буфера
|
||||||
|
* при переносе строки (отбрасываем уехавшее за границу слово). */
|
||||||
|
static uint8_t g_cells[MAX_CACHE_LINE_LEN * 2];
|
||||||
|
uint8_t g_ncells = 0; /* ячеек в текущем сегменте (общий с таблицами) */
|
||||||
|
static uint8_t g_ncells_at_space = 0; /* снимок g_ncells на последнем пробеле (для усечения при переносе) */
|
||||||
|
static uint8_t g_skip_flush = 0; /* следующий emit_seg пропустит lag-флаш (pending уже зафлашен вручную, напр. перед таблицей) */
|
||||||
|
|
||||||
|
/* Пишет одну (char,attr)-ячейку в g_cells, молча игнорируя переполнение
|
||||||
|
* MAX_CACHE_LINE_LEN. Оставлена ФУНКЦИЕЙ намеренно: инлайн в сильно
|
||||||
|
* регистро-нагруженный скан-цикл на Z80/SDCC увеличивает спиллы и замедляет
|
||||||
|
* (проверено замером — макро-инлайн дал регрессию ~11→13с). */
|
||||||
|
void gc_put(char ch, uint8_t attr)
|
||||||
|
{
|
||||||
|
if (g_ncells < MAX_CACHE_LINE_LEN) {
|
||||||
|
uint16_t i = (uint16_t)g_ncells * 2u;
|
||||||
|
g_cells[i] = (uint8_t)ch;
|
||||||
|
g_cells[i + 1] = attr;
|
||||||
|
g_ncells++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void gc_fill(char ch, uint8_t attr, uint8_t n) { while (n-- > 0) gc_put(ch, attr); }
|
||||||
|
|
||||||
|
/* Пишет g_cells как контент кэша для line_idx: flags берём из idx-записи,
|
||||||
|
* пустой буфер/blank → len 0; иначе резервируем место в пуле и коммитим. */
|
||||||
|
static void seg_commit_cells(uint16_t line_idx)
|
||||||
|
{
|
||||||
|
cache_rec_t out_rec;
|
||||||
|
out_rec.flags = 0; out_rec.reserved = 0; out_rec.pad[0] = 0; out_rec.pad[1] = 0;
|
||||||
|
idx_rec_t r;
|
||||||
|
uint8_t have = (uint8_t)(line_idx < n_lines);
|
||||||
|
if (have) { idx_get(line_idx, &r); out_rec.flags = r.flags; }
|
||||||
|
if (!have || (r.flags & IF_BLANK) || g_ncells == 0) {
|
||||||
|
out_rec.page = 0; out_rec.off = 0; out_rec.len = 0;
|
||||||
|
cache_dir_put(line_idx, &out_rec);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
out_rec.len = g_ncells;
|
||||||
|
if (cache_reserve((uint16_t)(g_ncells * 2u), &out_rec.page, &out_rec.off)) {
|
||||||
|
cache_commit(out_rec.page, out_rec.off, g_cells, (uint16_t)(g_ncells * 2u));
|
||||||
|
} else {
|
||||||
|
out_rec.page = 0; out_rec.off = 0; out_rec.len = 0;
|
||||||
|
}
|
||||||
|
cache_dir_put(line_idx, &out_rec);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void emit_seg(uint32_t off, uint8_t style, uint8_t ckind, uint8_t cont)
|
||||||
|
{
|
||||||
|
(void)ckind; /* тип сегмента восстанавливается флагами idx-записи */
|
||||||
|
if (n_lines >= max_lines) { index_truncated = 1; return; }
|
||||||
|
cur_rec.off = off;
|
||||||
|
cur_rec.flags = (uint8_t)(cont ? IF_CONT : 0);
|
||||||
|
cur_rec.style = (uint8_t)(style & 7u);
|
||||||
|
cur_rec.pad0 = 0;
|
||||||
|
cur_rec.pad1 = 0;
|
||||||
|
idx_put(n_lines, &cur_rec);
|
||||||
|
cur_seg_off = off;
|
||||||
|
n_lines++;
|
||||||
|
/* Флаш ячеек ПРЕДЫДУЩего сегмента (lag-1): он полностью собран форвард-сканером. */
|
||||||
|
if (n_lines >= 2) {
|
||||||
|
if (g_skip_flush) g_skip_flush = 0; /* pending уже зафлашен вручную */
|
||||||
|
else seg_commit_cells((uint16_t)(n_lines - 2));
|
||||||
|
}
|
||||||
|
g_ncells = 0; /* буфер ячеек под новый сегмент */
|
||||||
|
g_ncells_at_space = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Проверяет, что физическая строка пуста (только пробелы/таб + перевод строки). */
|
||||||
|
static uint8_t is_line_blank(uint32_t p)
|
||||||
|
{
|
||||||
|
while (p < file_size) {
|
||||||
|
char c = fb(p);
|
||||||
|
if (c == '\n') return 1;
|
||||||
|
if (c != ' ' && c != '\t') return 0;
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Число ведущих пробелов строки (отступ). Используется для отделения
|
||||||
|
* dedent-пунктов списка (#3) и для отступа обычного абзаца (#4). */
|
||||||
|
static uint8_t leading_spaces(uint32_t p)
|
||||||
|
{
|
||||||
|
uint8_t n = 0;
|
||||||
|
while (p < file_size && fb(p) == ' ' && n < 250) { n++; p++; }
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Быстрая проверка границы fenced-блока по маркеру ``` .
|
||||||
|
* Ведущие пробелы/табы пропускаются — fence может быть с отступом
|
||||||
|
* (например, ``` ```c ``` внутри элемента списка с отступом 2+ пробела).
|
||||||
|
* Язык после бэктиков (```c, ```sh) игнорируется: достаточно трёх ` подряд. */
|
||||||
|
static uint8_t is_fence_raw(uint32_t p)
|
||||||
|
{
|
||||||
|
while (p < file_size && (fb(p) == ' ' || fb(p) == '\t')) p++;
|
||||||
|
if (p + 2 >= file_size) return 0;
|
||||||
|
return (fb(p) == '`' && fb(p + 1) == '`' && fb(p + 2) == '`');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Быстрая проверка горизонтального разделителя (`---`, `***`, `___`). */
|
||||||
|
static uint8_t is_hr_raw(uint32_t p)
|
||||||
|
{
|
||||||
|
char c0 = fb(p);
|
||||||
|
if (c0 != '-' && c0 != '*' && c0 != '_') return 0;
|
||||||
|
char marker = 0;
|
||||||
|
uint8_t count = 0, ok = 1;
|
||||||
|
uint32_t hr_p = p;
|
||||||
|
while (hr_p < file_size) {
|
||||||
|
char ch = fb(hr_p++);
|
||||||
|
if (ch == '\n' || ch == '\r') break;
|
||||||
|
if (ch == ' ' || ch == '\t') continue;
|
||||||
|
if (ch != '-' && ch != '*' && ch != '_') { ok = 0; break; }
|
||||||
|
if (marker == 0) marker = ch;
|
||||||
|
else if (ch != marker) { ok = 0; break; }
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
return ok && count >= 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Строка таблицы: первый непустой символ — '|'.
|
||||||
|
* Ведущие пробелы разрешены; такие строки не склеиваются и не переносятся. */
|
||||||
|
static uint8_t is_table_raw(uint32_t p)
|
||||||
|
{
|
||||||
|
while (p < file_size) {
|
||||||
|
char c = fb(p);
|
||||||
|
if (c == ' ' || c == '\t') { p++; continue; }
|
||||||
|
return (uint8_t)(c == '|');
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Разбор одного inline-МАРКЕРА в позиции *pq (< bound): \X, [x], `, **, ~~,
|
||||||
|
* *, _, \t. Общий код для inline_scan() и scan_join_stream(). Возврат: 1 —
|
||||||
|
* маркер обработан (вызывающий делает continue), 0 — символ обычный (пробел/
|
||||||
|
* текст), его кладёт сам вызывающий (и проверяет перенос). Меняет
|
||||||
|
* *pq/*pls/*pseg/*pprev. attr берётся как emph_to_attr(line_style, base) —
|
||||||
|
* для join base=ATTR_TEXT, что тождественно styles_map[line_style]. */
|
||||||
|
static uint8_t inline_marker(uint32_t *pq, uint32_t bound, uint8_t *pls,
|
||||||
|
uint8_t *pseg, char *pprev, uint8_t base)
|
||||||
|
{
|
||||||
|
uint32_t q = *pq;
|
||||||
|
uint8_t ls = *pls;
|
||||||
|
char ch = fb(q);
|
||||||
|
|
||||||
|
if (ls != INIT_STYLE_CODE && ch == '\\' && (q + 1) < bound && is_escapable(fb(q + 1))) {
|
||||||
|
char nb = fb(q + 1);
|
||||||
|
gc_put(nb, emph_to_attr(ls, base));
|
||||||
|
*pseg += 1; *pq = q + 2; *pprev = nb; return 1;
|
||||||
|
}
|
||||||
|
if (ls != INIT_STYLE_CODE && ch == '[' && (q + 2) < bound && fb(q + 2) == ']') {
|
||||||
|
gc_put('[', ATTR_TEXT_BOLD); gc_put(fb(q + 1), ATTR_TEXT_BOLD); gc_put(']', ATTR_TEXT_BOLD);
|
||||||
|
*pseg += 3; *pq = q + 3; *pprev = ']'; return 1;
|
||||||
|
}
|
||||||
|
if (ch == '`') {
|
||||||
|
if (ls == INIT_STYLE_PLAIN) { *pls = INIT_STYLE_CODE; *pq = q + 1; return 1; }
|
||||||
|
if (ls == INIT_STYLE_CODE) { *pls = INIT_STYLE_PLAIN; *pq = q + 1; return 1; }
|
||||||
|
gc_put('`', emph_to_attr(ls, base)); *pseg += 1; *pq = q + 1; *pprev = '`'; return 1;
|
||||||
|
}
|
||||||
|
if (ch == '*' && (q + 1) < bound && fb(q + 1) == '*') {
|
||||||
|
char nx = ((q + 2) < bound) ? fb(q + 2) : '\n';
|
||||||
|
if (is_emph_flanked(*pprev, nx)) {
|
||||||
|
if (ls == INIT_STYLE_PLAIN && ws_or_eol(*pprev)) { *pls = INIT_STYLE_BOLD; *pq = q + 2; return 1; }
|
||||||
|
if (ls == INIT_STYLE_BOLD && ws_or_eol_or_delim(nx)) { *pls = INIT_STYLE_PLAIN; *pq = q + 2; return 1; }
|
||||||
|
}
|
||||||
|
gc_put('*', emph_to_attr(ls, base)); gc_put('*', emph_to_attr(ls, base));
|
||||||
|
*pseg += 2; *pq = q + 2; *pprev = '*'; return 1;
|
||||||
|
}
|
||||||
|
if (ch == '~' && (q + 1) < bound && fb(q + 1) == '~') {
|
||||||
|
char nx = ((q + 2) < bound) ? fb(q + 2) : '\n';
|
||||||
|
if (is_emph_flanked(*pprev, nx)) {
|
||||||
|
if (ls == INIT_STYLE_PLAIN && ws_or_eol(*pprev)) { *pls = INIT_STYLE_STRIKE; *pq = q + 2; return 1; }
|
||||||
|
if (ls == INIT_STYLE_STRIKE && ws_or_eol_or_delim(nx)) { *pls = INIT_STYLE_PLAIN; *pq = q + 2; return 1; }
|
||||||
|
}
|
||||||
|
gc_put('~', emph_to_attr(ls, base)); gc_put('~', emph_to_attr(ls, base));
|
||||||
|
*pseg += 2; *pq = q + 2; *pprev = '~'; return 1;
|
||||||
|
}
|
||||||
|
if (ch == '*' || ch == '_') {
|
||||||
|
char nx = ((q + 1) < bound) ? fb(q + 1) : '\n';
|
||||||
|
if (is_emph_flanked(*pprev, nx)) {
|
||||||
|
uint8_t ns = (ch == '*') ? INIT_STYLE_ITALIC : INIT_STYLE_UNDER;
|
||||||
|
if (ls == INIT_STYLE_PLAIN && ws_or_eol(*pprev)) { *pls = ns; *pq = q + 1; return 1; }
|
||||||
|
if (ls != INIT_STYLE_PLAIN && ws_or_eol_or_delim(nx)) { *pls = INIT_STYLE_PLAIN; *pq = q + 1; return 1; }
|
||||||
|
}
|
||||||
|
gc_put(ch, emph_to_attr(ls, base)); *pseg += 1; *pq = q + 1; *pprev = ch; return 1;
|
||||||
|
}
|
||||||
|
if (ch == '\t') {
|
||||||
|
uint8_t old_sc = *pseg;
|
||||||
|
uint8_t tgt_full = (uint8_t)((old_sc & (uint8_t)~(TAB_STOP - 1)) + TAB_STOP);
|
||||||
|
uint8_t tgt = tgt_full; if (tgt > SCREEN_W) tgt = SCREEN_W;
|
||||||
|
gc_fill(' ', emph_to_attr(ls, base), (uint8_t)(tgt_full - old_sc));
|
||||||
|
*pseg = tgt; *pq = q + 1; *pprev = ' '; return 1;
|
||||||
|
}
|
||||||
|
return 0; /* пробел или обычный символ — обрабатывает вызывающий */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Общий сканер inline-форматирования и переносов.
|
||||||
|
* Идёт от q до q_end, учитывает стартовую колонку col и при необходимости
|
||||||
|
* эмитит continuation-сегменты. Возвращает итоговый line_style. */
|
||||||
|
uint8_t inline_scan(uint32_t q, uint32_t q_end, uint8_t col,
|
||||||
|
uint8_t ckind, uint8_t line_style, uint8_t base_attr,
|
||||||
|
uint8_t nowrap)
|
||||||
|
{
|
||||||
|
uint32_t last_space = 0xFFFFFFFFu;
|
||||||
|
uint8_t last_space_style = line_style; /* снимок line_style на момент last_space */
|
||||||
|
char prev_ch = ' ';
|
||||||
|
uint8_t seg_col = col;
|
||||||
|
/* base — атрибут «обычного» текста сегмента. Для заголовка первый сегмент
|
||||||
|
* имеет title-attr, а continuation-сегменты render рисует как PLAIN
|
||||||
|
* (ATTR_TEXT, kind=LK_PLAIN при cont) — переключаем base после 1-го переноса. */
|
||||||
|
uint8_t base = base_attr;
|
||||||
|
|
||||||
|
while (q < q_end && n_lines < max_lines) {
|
||||||
|
char ch = fb(q);
|
||||||
|
|
||||||
|
if (ch == '\n') {
|
||||||
|
/* Жёсткий перенос (уже проверен снаружи) или конец потока. */
|
||||||
|
q++;
|
||||||
|
emit_seg(q, line_style, ckind, (ckind == CK_LIST || ckind == CK_QUOTE) ? 1 : 0);
|
||||||
|
base = ATTR_TEXT;
|
||||||
|
last_space = 0xFFFFFFFFu; prev_ch = ' '; seg_col = col;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inline-маркеры (\X [x] ` ** ~~ * _ \t) — общий разбор. */
|
||||||
|
if (inline_marker(&q, q_end, &line_style, &seg_col, &prev_ch, base)) continue;
|
||||||
|
|
||||||
|
if (ch == ' ') {
|
||||||
|
gc_put(' ', emph_to_attr(line_style, base));
|
||||||
|
last_space = q + 1;
|
||||||
|
last_space_style = line_style;
|
||||||
|
g_ncells_at_space = g_ncells;
|
||||||
|
seg_col++; q++; prev_ch = ' ';
|
||||||
|
} else {
|
||||||
|
gc_put(ch, emph_to_attr(line_style, base));
|
||||||
|
seg_col++; q++; prev_ch = ch;
|
||||||
|
}
|
||||||
|
if (!nowrap && seg_col >= SCREEN_W) {
|
||||||
|
uint8_t use_ls = (uint8_t)(last_space != 0xFFFFFFFFu && last_space > cur_seg_off);
|
||||||
|
uint32_t wrap_at = use_ls ? last_space : q;
|
||||||
|
if (wrap_at >= q_end || wrap_at == cur_seg_off) break;
|
||||||
|
uint8_t wrap_style = use_ls ? last_space_style : line_style;
|
||||||
|
if (use_ls) g_ncells = g_ncells_at_space;
|
||||||
|
emit_seg(wrap_at, wrap_style, ckind, 1);
|
||||||
|
base = ATTR_TEXT; /* continuation заголовка → PLAIN */
|
||||||
|
seg_col = col; last_space = 0xFFFFFFFFu; prev_ch = ' '; q = wrap_at;
|
||||||
|
line_style = wrap_style;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return line_style;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define JOIN_MODE_LIST 1u
|
||||||
|
#define JOIN_MODE_QUOTE 2u
|
||||||
|
#define JOIN_MODE_PLAIN 3u
|
||||||
|
|
||||||
|
/* Общий потоковый сканер для index_lines().
|
||||||
|
* Убирает дублирование между ветками list/quote/plain:
|
||||||
|
* - mode задаёт правила обработки перевода строки,
|
||||||
|
* - остальная inline-логика (emphasis/таб/перенос) едина.
|
||||||
|
* Возвращает позицию q, на которой сканер остановился, и обновляет стиль. */
|
||||||
|
static uint32_t scan_join_stream(uint32_t q, uint32_t para_start, uint8_t col,
|
||||||
|
uint8_t ckind, uint8_t *io_line_style, uint8_t mode)
|
||||||
|
{
|
||||||
|
uint32_t last_space = 0xFFFFFFFFu;
|
||||||
|
uint8_t last_space_style = *io_line_style; /* снимок line_style на момент last_space */
|
||||||
|
char prev_ch = ' ';
|
||||||
|
uint8_t seg_col = col;
|
||||||
|
uint8_t line_style = *io_line_style;
|
||||||
|
|
||||||
|
while (q < file_size && n_lines < max_lines) {
|
||||||
|
char ch = fb(q);
|
||||||
|
uint8_t soft_break = 0;
|
||||||
|
|
||||||
|
if (ch == '\n') {
|
||||||
|
uint32_t next = q + 1;
|
||||||
|
if (next >= file_size) break;
|
||||||
|
|
||||||
|
if (mode == JOIN_MODE_LIST) {
|
||||||
|
if (is_line_blank(next)) break;
|
||||||
|
if (is_fence_raw(next) || is_hr_raw(next) || is_table_raw(next)) break;
|
||||||
|
|
||||||
|
{
|
||||||
|
uint32_t next_content = next;
|
||||||
|
uint8_t next_kind = classify_line(next, &next_content);
|
||||||
|
if (next_kind == LK_ULIST || next_kind == LK_OLIST ||
|
||||||
|
next_kind == LK_QUOTE ||
|
||||||
|
(next_kind >= LK_H1 && next_kind <= LK_H4) ||
|
||||||
|
next_kind == LK_HR) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (next < file_size) {
|
||||||
|
char ws = fb(next);
|
||||||
|
if (ws == ' ' || ws == '\t') next++;
|
||||||
|
else break;
|
||||||
|
}
|
||||||
|
q = next;
|
||||||
|
soft_break = 1;
|
||||||
|
ch = ' ';
|
||||||
|
} else if (mode == JOIN_MODE_QUOTE) {
|
||||||
|
if (is_line_blank(next)) break;
|
||||||
|
if (is_fence_raw(next) || is_hr_raw(next) || is_table_raw(next)) break;
|
||||||
|
|
||||||
|
{
|
||||||
|
uint32_t next_content = next;
|
||||||
|
uint8_t next_kind = classify_line(next, &next_content);
|
||||||
|
if (next_kind != LK_QUOTE) break;
|
||||||
|
|
||||||
|
{
|
||||||
|
uint32_t next_line_end = next_content;
|
||||||
|
while (next_line_end < file_size && fb(next_line_end) != '\n') next_line_end++;
|
||||||
|
|
||||||
|
{
|
||||||
|
uint8_t empty_quote = 1;
|
||||||
|
for (uint32_t z = next_content; z < next_line_end; z++) {
|
||||||
|
char c = fb(z);
|
||||||
|
if (c != ' ' && c != '\t') { empty_quote = 0; break; }
|
||||||
|
}
|
||||||
|
if (empty_quote) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
uint32_t t = next_content;
|
||||||
|
while (t < next_line_end && (fb(t) == ' ' || fb(t) == '\t')) t++;
|
||||||
|
if (t < next_line_end && fb(t) == '>') break;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (next_content < next_line_end) {
|
||||||
|
char ws = fb(next_content);
|
||||||
|
if (ws == ' ' || ws == '\t') next_content++;
|
||||||
|
else break;
|
||||||
|
}
|
||||||
|
if (next_content < next_line_end && fb(next_content) == '>') {
|
||||||
|
next_content++;
|
||||||
|
if (next_content < next_line_end && fb(next_content) == ' ') next_content++;
|
||||||
|
}
|
||||||
|
q = next_content;
|
||||||
|
soft_break = 1;
|
||||||
|
ch = ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else { /* JOIN_MODE_PLAIN */
|
||||||
|
if (fb(next) == '\n') break;
|
||||||
|
if (is_fence_raw(next) || is_hr_raw(next) || is_table_raw(next)) break;
|
||||||
|
{
|
||||||
|
uint32_t dummy;
|
||||||
|
if (classify_line(next, &dummy) != LK_PLAIN) break;
|
||||||
|
}
|
||||||
|
if (is_line_blank(next)) break;
|
||||||
|
|
||||||
|
{
|
||||||
|
uint8_t is_hard = 0;
|
||||||
|
if (q >= para_start + 1) {
|
||||||
|
char b1 = fb(q - 1);
|
||||||
|
if (b1 == '\\') {
|
||||||
|
is_hard = 1;
|
||||||
|
} else if (q >= para_start + 2) {
|
||||||
|
char b2 = fb(q - 2);
|
||||||
|
if ((b1 == ' ' || b1 == '\t') && (b2 == ' ' || b2 == '\t'))
|
||||||
|
is_hard = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (is_hard) {
|
||||||
|
q++;
|
||||||
|
emit_seg(q, line_style, ckind, 0);
|
||||||
|
last_space = 0xFFFFFFFFu;
|
||||||
|
prev_ch = ' ';
|
||||||
|
seg_col = col;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
q++;
|
||||||
|
/* как render: пропускаем ведущие пробелы/табы строки-продолжения,
|
||||||
|
* склейка даёт РОВНО один пробел (иначе merge эмитит отступ как лишние ячейки) */
|
||||||
|
while (q < file_size && (fb(q) == ' ' || fb(q) == '\t')) q++;
|
||||||
|
soft_break = 1;
|
||||||
|
ch = ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inline-маркеры (\X [x] ` ** ~~ * _ \t) — общий разбор; при
|
||||||
|
* soft_break ch==' ' (синтетический пробел склейки) — маркеры
|
||||||
|
* пропускаем, его кладёт ветка пробела ниже. */
|
||||||
|
if (!soft_break && inline_marker(&q, file_size, &line_style, &seg_col, &prev_ch, ATTR_TEXT))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (ch == ' ') {
|
||||||
|
gc_put(' ', styles_map[line_style]);
|
||||||
|
if (soft_break) {
|
||||||
|
last_space = q;
|
||||||
|
last_space_style = line_style;
|
||||||
|
seg_col++;
|
||||||
|
} else {
|
||||||
|
last_space = q + 1;
|
||||||
|
last_space_style = line_style;
|
||||||
|
seg_col++;
|
||||||
|
q++;
|
||||||
|
}
|
||||||
|
g_ncells_at_space = g_ncells; /* снимок для усечения */
|
||||||
|
prev_ch = ' ';
|
||||||
|
} else {
|
||||||
|
gc_put(ch, styles_map[line_style]);
|
||||||
|
seg_col++;
|
||||||
|
q++; /* soft_break здесь всегда 0 (ch!=' ') */
|
||||||
|
prev_ch = ch;
|
||||||
|
}
|
||||||
|
if (seg_col >= SCREEN_W) {
|
||||||
|
uint8_t use_ls = (uint8_t)(last_space != 0xFFFFFFFFu && last_space > cur_seg_off);
|
||||||
|
uint32_t wrap_at = use_ls ? last_space : q;
|
||||||
|
if (wrap_at >= file_size || wrap_at == cur_seg_off) break;
|
||||||
|
uint8_t wrap_style = use_ls ? last_space_style : line_style;
|
||||||
|
if (use_ls) g_ncells = g_ncells_at_space; /* усечь до пробела */
|
||||||
|
emit_seg(wrap_at, wrap_style, ckind, 1);
|
||||||
|
/* Префикс continuation-сегмента: список и отступленный абзац (#4) —
|
||||||
|
* col пробелов выравнивания; цитата — отступ + маркер 0xB3 + пробел.
|
||||||
|
* Для обычного абзаца без отступа col==0 → префикса нет. */
|
||||||
|
if (ckind == CK_LIST || ckind == CK_PLAIN) {
|
||||||
|
gc_fill(' ', ATTR_TEXT, col);
|
||||||
|
} else if (ckind == CK_QUOTE) {
|
||||||
|
uint32_t content_off2 = para_start + col;
|
||||||
|
uint8_t qsp = 0;
|
||||||
|
while (qsp + 2 < content_off2 && qsp < SCREEN_W && fb(para_start + qsp) == ' ') qsp++;
|
||||||
|
gc_fill(' ', ATTR_TEXT, qsp);
|
||||||
|
gc_put(0xB3, ATTR_QUOTE_MARKER);
|
||||||
|
gc_put(' ', ATTR_TEXT);
|
||||||
|
}
|
||||||
|
seg_col = col;
|
||||||
|
last_space = 0xFFFFFFFFu;
|
||||||
|
prev_ch = ' ';
|
||||||
|
q = wrap_at;
|
||||||
|
/* Откатываем line_style к снимку на момент wrap_at: диапазон
|
||||||
|
* [wrap_at, q_old) будет пересканирован заново, и переключающие
|
||||||
|
* символы (обратная кавычка, звёздочка, подчёркивание, тильда)
|
||||||
|
* внутри него иначе применились бы дважды — именно так возникал
|
||||||
|
* баг: code-маркер на границе переноса ломает цвет следующего слова (mdview.c, отчёт пользователя
|
||||||
|
* 2026-06-23). */
|
||||||
|
line_style = wrap_style;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*io_line_style = line_style;
|
||||||
|
return q;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Отрисовка таблиц (row_end/table_*) — в модуле mdview2_table.c. */
|
||||||
|
|
||||||
|
/* Строит индекс сегментов рендера по загруженному файлу. */
|
||||||
|
void index_lines(void)
|
||||||
|
{
|
||||||
|
uint8_t in_block = 0;
|
||||||
|
n_lines = 0;
|
||||||
|
index_truncated = 0;
|
||||||
|
cur_seg_off = 0xFFFFFFFFu; /* пока не эмитили ни одного сегмента */
|
||||||
|
if (file_size == 0) return;
|
||||||
|
|
||||||
|
uint32_t p = 0;
|
||||||
|
while (p < file_size && n_lines < max_lines && !g_abort) {
|
||||||
|
|
||||||
|
/* UTF-8: держим конвертацию впереди позиции чтения (включая look-ahead
|
||||||
|
* склейки/классификации ~ один абзац). file_size при этом = доступная
|
||||||
|
* сконвертированная часть и растёт по ходу. */
|
||||||
|
if (g_utf_building && (uint32_t)(p + CONV_MARGIN) > file_size)
|
||||||
|
utf_convert_more((uint32_t)(p + CONV_MARGIN));
|
||||||
|
|
||||||
|
if (g_loading)
|
||||||
|
progress_tick(); /* первый экран + статус + скролл по готовым данным */
|
||||||
|
else if ((n_lines & 15) == 0)
|
||||||
|
spinner_tick();
|
||||||
|
|
||||||
|
/* Пустая строка — разделитель параграфов.
|
||||||
|
* В индекс добавляем не более одной пустой экранной строки подряд. */
|
||||||
|
if (is_line_blank(p)) {
|
||||||
|
while (p < file_size && fb(p) != '\n') p++;
|
||||||
|
if (p < file_size) p++;
|
||||||
|
/* Серия пустых строк схлопывается в одну визуальную строку. */
|
||||||
|
if (n_lines == 0 || !(cur_rec.flags & IF_BLANK)) {
|
||||||
|
emit_seg(p, INIT_STYLE_PLAIN, CK_PLAIN, 0);
|
||||||
|
set_blank_cur();
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- fenced code-блок (граница или тело) ---- */
|
||||||
|
if (is_fence_raw(p)) {
|
||||||
|
in_block = !in_block;
|
||||||
|
emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0);
|
||||||
|
set_nowrap_cur();
|
||||||
|
while (p < file_size && fb(p) != '\n') p++;
|
||||||
|
if (p < file_size) p++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (in_block) {
|
||||||
|
emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0);
|
||||||
|
set_nowrap_cur();
|
||||||
|
set_code_cur();
|
||||||
|
set_hscroll_cur(); /* код скроллится целым блоком */
|
||||||
|
{
|
||||||
|
uint8_t ccx = 0;
|
||||||
|
uint32_t pp = p;
|
||||||
|
while (pp < file_size && fb(pp) != '\n') {
|
||||||
|
char ch = fb(pp);
|
||||||
|
if (ch == '\t') {
|
||||||
|
uint8_t tgt = (uint8_t)((ccx & (uint8_t)~(TAB_STOP - 1)) + TAB_STOP);
|
||||||
|
gc_fill(' ', ATTR_TEXT_CODE, (uint8_t)(tgt - ccx));
|
||||||
|
ccx = tgt;
|
||||||
|
} else if (ch == '\r') {
|
||||||
|
gc_put(' ', ATTR_TEXT_CODE); ccx++; /* как render: \r → пробел */
|
||||||
|
} else {
|
||||||
|
gc_put(ch, ATTR_TEXT_CODE); ccx++;
|
||||||
|
}
|
||||||
|
pp++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (p < file_size && fb(p) != '\n') p++;
|
||||||
|
if (p < file_size) p++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- горизонтальный разделитель ---- */
|
||||||
|
if (is_hr_raw(p)) {
|
||||||
|
emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0);
|
||||||
|
set_nowrap_cur();
|
||||||
|
gc_fill(0xC4, ATTR_HR, SCREEN_W);
|
||||||
|
while (p < file_size && fb(p) != '\n') p++;
|
||||||
|
if (p < file_size) p++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- строка таблицы (| ... |): один nowrap-сегмент на исходную строку ---- */
|
||||||
|
if (is_table_raw(p)) {
|
||||||
|
/* Освобождаем g_cells под измерение ширин: коммитим pending-сегмент
|
||||||
|
* сейчас; первый emit_seg таблицы пропустит свой lag-флаш. */
|
||||||
|
if (n_lines >= 1) { seg_commit_cells((uint16_t)(n_lines - 1)); g_skip_flush = 1; }
|
||||||
|
g_ncells = 0;
|
||||||
|
|
||||||
|
/* Проход 1: границы блока + ОТРЕНДЕРЕННЫЕ ширины колонок (мерим тем же
|
||||||
|
* inline_scan, что и при отрисовке — невидимые маркеры стиля не считаются). */
|
||||||
|
uint8_t widths[TBL_MAX_COLS];
|
||||||
|
for (uint8_t i = 0; i < TBL_MAX_COLS; i++) widths[i] = 0;
|
||||||
|
uint8_t ncols = 0;
|
||||||
|
uint32_t r = p;
|
||||||
|
while (r < file_size && is_table_raw(r)) {
|
||||||
|
uint32_t le = row_end(r);
|
||||||
|
uint8_t sep = table_is_sep_row(r, le);
|
||||||
|
uint32_t cp = table_first_cell(r);
|
||||||
|
uint32_t cs, ce;
|
||||||
|
uint8_t c = 0;
|
||||||
|
while (c < TBL_MAX_COLS && table_next_cell(&cp, le, &cs, &ce)) {
|
||||||
|
if (!sep && ce > cs) {
|
||||||
|
g_ncells = 0;
|
||||||
|
(void)inline_scan(cs, ce, 0, CK_OTHER, INIT_STYLE_PLAIN, ATTR_TEXT, 1);
|
||||||
|
if (g_ncells > widths[c]) widths[c] = g_ncells;
|
||||||
|
}
|
||||||
|
c++;
|
||||||
|
}
|
||||||
|
if (c > ncols) ncols = c;
|
||||||
|
r = (le < file_size) ? le + 1 : le;
|
||||||
|
}
|
||||||
|
g_ncells = 0; /* чисто для верхней рамки */
|
||||||
|
uint32_t tbl_end = r;
|
||||||
|
if (ncols == 0) ncols = 1;
|
||||||
|
|
||||||
|
/* Проход 2: верхняя рамка → строки/разделитель → нижняя рамка. */
|
||||||
|
emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0);
|
||||||
|
set_nowrap_cur(); set_hscroll_cur();
|
||||||
|
table_border(widths, ncols, TBL_TL, TBL_TM, TBL_TR);
|
||||||
|
|
||||||
|
r = p;
|
||||||
|
while (r < tbl_end) {
|
||||||
|
uint32_t le = row_end(r);
|
||||||
|
emit_seg(r, INIT_STYLE_PLAIN, CK_OTHER, 0);
|
||||||
|
set_nowrap_cur(); set_hscroll_cur();
|
||||||
|
if (table_is_sep_row(r, le)) table_border(widths, ncols, TBL_ML, TBL_MM, TBL_MR);
|
||||||
|
else table_data_row(r, le, widths, ncols);
|
||||||
|
r = (le < file_size) ? le + 1 : le;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit_seg(tbl_end, INIT_STYLE_PLAIN, CK_OTHER, 0);
|
||||||
|
set_nowrap_cur(); set_hscroll_cur();
|
||||||
|
table_border(widths, ncols, TBL_BL, TBL_BM, TBL_BR);
|
||||||
|
|
||||||
|
p = tbl_end;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- классификация текущей строки ---- */
|
||||||
|
uint32_t content_off = p;
|
||||||
|
uint8_t kind = classify_line(p, &content_off);
|
||||||
|
|
||||||
|
/* ---- Заголовок: отдельная строка, не склеивается с параграфом ---- */
|
||||||
|
if (kind >= LK_H1 && kind <= LK_H4) {
|
||||||
|
uint8_t hbase; /* title-attr, как в render */
|
||||||
|
switch (kind) {
|
||||||
|
case LK_H1: hbase = ATTR_TEXT_TITLE1; break;
|
||||||
|
case LK_H2: hbase = ATTR_TEXT_TITLE2; break;
|
||||||
|
case LK_H3: hbase = ATTR_TEXT_TITLE3; break;
|
||||||
|
default: hbase = ATTR_TEXT_TITLE4; break;
|
||||||
|
}
|
||||||
|
emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0);
|
||||||
|
uint32_t line_end = content_off;
|
||||||
|
while (line_end < file_size && fb(line_end) != '\n') line_end++;
|
||||||
|
(void)inline_scan(content_off, line_end, 0, CK_OTHER, INIT_STYLE_PLAIN, hbase, 0);
|
||||||
|
while (p < file_size && fb(p) != '\n') p++;
|
||||||
|
if (p < file_size) p++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Пункт списка: может занимать несколько физических строк ---- */
|
||||||
|
if (kind == LK_ULIST || kind == LK_OLIST) {
|
||||||
|
uint8_t col = marker_visible_col(kind, p, content_off);
|
||||||
|
uint8_t line_style = INIT_STYLE_PLAIN;
|
||||||
|
uint32_t q = content_off;
|
||||||
|
|
||||||
|
emit_seg(p, line_style, CK_LIST, 0);
|
||||||
|
/* Префикс ПЕРВОГО сегмента: отступ + маркер + пробел (как render). */
|
||||||
|
{
|
||||||
|
uint32_t qq = p, qmax = p + SCREEN_W;
|
||||||
|
if (kind == LK_ULIST) {
|
||||||
|
while (qq + 2 < content_off && qq < qmax && fb(qq) == ' ') qq++;
|
||||||
|
gc_fill(' ', ATTR_TEXT, (uint8_t)(qq - p));
|
||||||
|
gc_put(0x07, ATTR_LIST_MARKER);
|
||||||
|
gc_put(' ', ATTR_TEXT);
|
||||||
|
} else {
|
||||||
|
while (qq < content_off && qq < qmax && fb(qq) == ' ') qq++;
|
||||||
|
gc_fill(' ', ATTR_TEXT, (uint8_t)(qq - p));
|
||||||
|
while (qq < content_off - 1) { gc_put(fb(qq), ATTR_LIST_MARKER); qq++; }
|
||||||
|
gc_put(' ', ATTR_TEXT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
q = scan_join_stream(q, p, col, CK_LIST, &line_style, JOIN_MODE_LIST);
|
||||||
|
|
||||||
|
/* Переходим к началу следующей физической строки после пункта. */
|
||||||
|
if (q < file_size) {
|
||||||
|
while (q < file_size && fb(q) != '\n') q++;
|
||||||
|
if (q < file_size) q++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Одна пустая строка между соседними маркерами списка подавляется.
|
||||||
|
* Две и более пустых строки оставляют видимый разделитель. */
|
||||||
|
if (q < file_size && is_line_blank(q)) {
|
||||||
|
uint32_t t = q;
|
||||||
|
uint8_t blanks = 0;
|
||||||
|
while (t < file_size && is_line_blank(t)) {
|
||||||
|
while (t < file_size && fb(t) != '\n') t++;
|
||||||
|
if (t < file_size) t++;
|
||||||
|
blanks++;
|
||||||
|
// if (blanks >= 2) break;
|
||||||
|
}
|
||||||
|
if (blanks == 1 && t < file_size) {
|
||||||
|
uint32_t d = t;
|
||||||
|
uint8_t nk = classify_line(t, &d);
|
||||||
|
if (nk == LK_ULIST || nk == LK_OLIST) {
|
||||||
|
/* #3: подавляем blank только если следующий пункт того же
|
||||||
|
* уровня или глубже; при МЕНЬШЕМ отступе (dedent) пустую
|
||||||
|
* строку оставляем — пункты должны визуально разделиться. */
|
||||||
|
if (leading_spaces(t) >= leading_spaces(p)) {
|
||||||
|
p = t; /* подавляем единственную пустую строку */
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p = q;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Параграф цитаты: может покрывать несколько физических строк ---- */
|
||||||
|
if (kind == LK_QUOTE) {
|
||||||
|
uint8_t col = marker_visible_col(kind, p, content_off);
|
||||||
|
uint8_t line_style = INIT_STYLE_PLAIN;
|
||||||
|
uint32_t q = content_off;
|
||||||
|
|
||||||
|
emit_seg(p, line_style, CK_QUOTE, 0);
|
||||||
|
/* Префикс ПЕРВОГО сегмента: отступ + маркер 0xB3 + пробел (как render LK_QUOTE). */
|
||||||
|
{
|
||||||
|
uint32_t qq = p, qmax = p + SCREEN_W;
|
||||||
|
while (qq + 1 < content_off && qq < qmax && fb(qq) == ' ') qq++;
|
||||||
|
gc_fill(' ', ATTR_TEXT, (uint8_t)(qq - p));
|
||||||
|
gc_put(0xB3, ATTR_QUOTE_MARKER);
|
||||||
|
gc_put(' ', ATTR_TEXT);
|
||||||
|
}
|
||||||
|
q = scan_join_stream(q, p, col, CK_QUOTE, &line_style, JOIN_MODE_QUOTE);
|
||||||
|
|
||||||
|
while (q < file_size && fb(q) != '\n') q++;
|
||||||
|
if (q < file_size) q++;
|
||||||
|
p = q;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Обычный текстовый параграф: склейка через soft-break ---- */
|
||||||
|
{
|
||||||
|
uint8_t line_style = INIT_STYLE_PLAIN;
|
||||||
|
/* #4: если абзац начинается с отступа — все его перенесённые строки
|
||||||
|
* получают такой же отступ (как continuation-префикс списка, но без
|
||||||
|
* маркера). Контент сканируем после отступа. Патологически большой
|
||||||
|
* отступ игнорируем. */
|
||||||
|
uint8_t indent = leading_spaces(p);
|
||||||
|
if (indent >= SCREEN_W / 2) indent = 0;
|
||||||
|
|
||||||
|
emit_seg(p, line_style, CK_PLAIN, 0);
|
||||||
|
if (indent) gc_fill(' ', ATTR_TEXT, indent); /* префикс первого сегмента */
|
||||||
|
|
||||||
|
uint32_t q = p + indent;
|
||||||
|
q = scan_join_stream(q, p, indent, CK_PLAIN, &line_style, JOIN_MODE_PLAIN);
|
||||||
|
|
||||||
|
if (q >= file_size) {
|
||||||
|
p = file_size;
|
||||||
|
} else {
|
||||||
|
/* q стоит на завершающем newline параграфа.
|
||||||
|
* Переходим на следующую строку, чтобы внешняя петля её
|
||||||
|
* корректно переклассифицировала. */
|
||||||
|
while (q < file_size && fb(q) != '\n') q++;
|
||||||
|
if (q < file_size) q++;
|
||||||
|
p = q;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* emit_seg() флашит ячейки с отставанием на одну строку (см. её
|
||||||
|
* комментарий) — последняя эмитированная строка файла так и остаётся
|
||||||
|
* незафлашенной, её ячейки ещё в g_cells; коммитим здесь. */
|
||||||
|
if (n_lines > 0) seg_commit_cells((uint16_t)(n_lines - 1));
|
||||||
|
}
|
||||||
|
#define EM_NONE 0
|
||||||
|
#define EM_BOLD 1
|
||||||
|
#define EM_ITALIC 2
|
||||||
|
#define EM_UNDER 3
|
||||||
|
#define EM_CODE 4
|
||||||
|
#define EM_STRIKE 5
|
||||||
|
|
||||||
|
/* Преобразует внутреннее состояние emphasis в экранный атрибут. */
|
||||||
|
static uint8_t emph_to_attr(uint8_t emph, uint8_t base_attr)
|
||||||
|
{
|
||||||
|
switch (emph) {
|
||||||
|
case EM_BOLD: return ATTR_TEXT_BOLD;
|
||||||
|
case EM_ITALIC: return ATTR_TEXT_ITALIC;
|
||||||
|
case EM_UNDER: return ATTR_TEXT_UNDERSORE;
|
||||||
|
case EM_CODE: return ATTR_TEXT_CODE;
|
||||||
|
case EM_STRIKE: return ATTR_TEXT_STRIKE;
|
||||||
|
default: return base_attr;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <stdint.h>
|
||||||
|
#include <conio.h> /* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <stdint.h>
|
||||||
|
#include <stdio.h> /* dec8/dec16 */
|
||||||
|
#include <conio.h> /* wrchar/gotoxy/cputs/textattr/scroll/COLOR */
|
||||||
|
#include <bios/text.h> /* 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();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user