diff --git a/examples/mdview/Makefile b/examples/mdview/Makefile index 1353016..ce69405 100644 --- a/examples/mdview/Makefile +++ b/examples/mdview/Makefile @@ -6,5 +6,5 @@ PROJ_ROOT := $(abspath $(CURDIR)/../..) EXAMPLE := mdview MEMORY := small -EXTRA_DATA := SAMPLE.MD PLAN_866.md +EXTRA_DATA := SAMPLE.MD PLAN_866.md SAMPLEF.MD include $(PROJ_ROOT)/app.mk diff --git a/examples/mdview/SAMPLE.MD_ b/examples/mdview/SAMPLE.MD_ deleted file mode 100644 index 7107f7e..0000000 --- a/examples/mdview/SAMPLE.MD_ +++ /dev/null @@ -1,165 +0,0 @@ -# MDVIEW Sample Document - -This is a sample Markdown file for testing the Sprinter `mdview` text -viewer. Phase 3 adds inline emphasis: **bold**, *italic* and _underscore_ -runs render with distinct background colours. - ---- - -## Section: Inline emphasis - -Plain words mixed with **bold words**, *italic words*, _underscore -words_ and `code words` to verify all four styles render with their -own colours. - -A single **bold** stretch, then a single *italic* stretch, then a single -_underscore_ stretch, then a single `code` stretch, all on the same line. - -A *long italic run that spans multiple words and several columns before -it closes here* and continues plain. - -Inline code with punctuation: call `printf("%d\n", x)` then check the -result; or use `argv[0]` to grab the program name. - -Unclosed emphasis (open **bold left dangling) — should auto-close at the -end of the line so the next line starts clean. - -Conflict cases: **bold with a stray * inside** stays bold, and *italic -with a stray _ inside* stays italic, and `code with **bold** inside` -stays code. - -## Section: Lorem ipsum - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod -tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim -veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea -commodo consequat. - -Duis aute irure dolor in reprehenderit in voluptate velit esse cillum -dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non -proident, sunt in culpa qui officia deserunt mollit anim id est laborum. - -## Section: Lists - -Unordered (dash): - -- alpha -- bravo -- charlie - -Unordered (asterisk): - -* delta -* echo - -Unordered (plus): - -+ foxtrot -+ golf - -Numbered list (digits + period highlighted): - -1. first -2. second -3. third -10. tenth — verifies multi-digit numbering -99. ninety-ninth - -Numbered with parens: - -1) parenthesised one -2) parenthesised two - -Nested lists (light v1 — leading spaces enable detection at non-zero col; -no hanging indent in wrap continuation yet): - -- top level alpha - - nested level 1 bravo - - nested level 1 charlie - - nested level 2 delta - - nested level 2 echo - - nested level 3 foxtrot - - nested level 1 golf -- top level hotel - -Mixed nested: - -1. first top - 1. nested numeric - 2. nested numeric - - mixed bullet under numeric - - another bullet - 3. nested numeric -2. second top - -## Section: Blockquote - -> Single-line blockquote. - -> Multi-line blockquote starts here. -> Each line keeps the > prefix highlighted -> while the body stays plain text. - -## Section: Code - -Inline code: `printf("hello")`. - -Fenced block (lines inside should render with code attribute on the -full row, no inline parsing — note the **stars** and *italics* below -remain literal): - -```c -int main(void) { - /* **not bold**, *not italic*, `not nested` */ - return 42; -} -``` - -After the block, normal **bold** parsing resumes. - -## Section: Long line truncation - -This line is intentionally long to verify that the viewer truncates at 80 columns instead of wrapping or scrolling horizontally. Anything past the 80th visible column should simply not appear on screen. - -## Section: Tabs - -Indented with tabs: - - tab-indent level 1 - tab-indent level 2 - tab-indent level 3 - -## Section: Blockquote - -> Markdown blockquotes start with a greater-than sign at column zero. -> Multiple lines look like this. - -### Subsection: deeper heading (H3) - -The line above is an H3 — it should render in a colour distinct from -H1 and H2. - -#### Subsubsection: H4 and below - -H4 (and the rarely-seen H5/H6) all share the H4 colour slot. - -*** - -## Section: Filler - -The remaining content exists to make the document scroll past one viewport. - -Line 50 ........... approx. -Line 51 ........... . -Line 52 ........... .. -Line 53 ........... ... -Line 54 ........... .... -Line 55 ........... ..... -Line 56 ........... ...... -Line 57 ........... ....... -Line 58 ........... ........ -Line 59 ........... ......... -Line 60 ........... .......... - -End-of-document marker. If you can see this line you can use Home / End -to bounce between the start and finish of the file. diff --git a/examples/mdview/mdview.c b/examples/mdview/mdview.c index 01a3570..667aa04 100644 --- a/examples/mdview/mdview.c +++ b/examples/mdview/mdview.c @@ -1,19 +1,20 @@ /* - * mdview — markdown text viewer for Sprinter. + * mdview — просмотрщик Markdown для Sprinter. * - * Layout (80x32): - * row 0 — status bar (filename / line range / total / %) - * rows 1..30 — viewport (30 lines of document text) - * row 31 — menu bar (F1 Help / F2 Wrap / F10 Exit) + * Разметка экрана (80x32): + * строка 0 — статус-бар (имя файла / диапазон строк / всего / %) + * строки 1..30 — область документа (30 видимых строк текста) + * строка 31 — меню (F1 Help / F2 Reserved / F10 Exit) * - * Navigation: arrows, PgUp/PgDn, Home/End, F1 help, F2 wrap toggle, - * F10/Esc exit. In Unwrap mode ←/→ pan horizontally. + * Навигация: + * стрелки, PgUp/PgDn, Home/End, F1 (справка), F10/Esc (выход). + * Горизонтальный сдвиг доступен только для nowrap-строк. * - * Memory: - * CODE → W1 (sprinter-cc --memory small) + * Память: + * CODE → W1 (режим sprinter-cc --memory small) * STACK/HEAP/DATA → W2 - * File buffer → EMM pages (up to 8 × 16 KB = 128 KB) mapped - * into W3 at 0xC000 on demand via fb()/map_page(). + * Буфер файла → страницы EMM (до 8 × 16 КБ = 128 КБ), + * отображаемые в W3 по требованию через fb()/map_page(). */ #include @@ -26,26 +27,26 @@ #include #include -/* ---- screen geometry ---------------------------------------------- */ +/* ---- Геометрия экрана -------------------------------------------- */ #define SCREEN_W 80 #define SCREEN_H 32 #define VIEW_TOP_ROW 1 -#define VIEW_H 30 /* rows 1..30 inclusive */ +#define VIEW_H 30 /* видимая область: строки 1..30 включительно */ #define MENU_ROW 31 #define TAB_STOP 4 -/* ---- file/memory layout ------------------------------------------- */ +/* ---- Параметры файла и памяти ------------------------------------ */ #define PAGE_BITS 14u -#define PAGE_SIZE (1u << PAGE_BITS) /* 16 KB */ +#define PAGE_SIZE (1u << PAGE_BITS) /* размер EMM-страницы: 16 КБ */ #define PAGE_MASK ((uint16_t)(PAGE_SIZE - 1u)) -#define MAX_PAGES 8 /* 8 × 16 KB = 128 KB */ -#define MAX_FILE ((uint32_t)MAX_PAGES * PAGE_SIZE) /* 131072 */ +#define MAX_PAGES 8 /* 8 страниц × 16 КБ = 128 КБ */ +#define MAX_FILE ((uint32_t)MAX_PAGES * PAGE_SIZE) /* максимальный размер файла: 131072 байт */ #define MAX_LINES 2048 -#define FILE_BUF ((char *)0xC000) /* W3-mapped EMM page */ +#define FILE_BUF ((char *)0xC000) /* окно W3, куда мапится текущая EMM-страница */ -/* ---- attribute palette -------------------------------------------- */ +/* ---- Палитра атрибутов ------------------------------------------- */ static const uint8_t md_pallete[16 * 4] = { 0x00, 0x00, 0x00, 0x00, @@ -65,24 +66,6 @@ static const uint8_t md_pallete[16 * 4] = { 0xAA, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF }; -// static uint8_t md_pallete[16 * 4] = { -// 0x00,0x00,0x00, 0x00, -// 0x10,0x10,0x10, 0x00, -// 0x20,0x20,0x20, 0x00, -// 0x30,0x30,0x30, 0x00, -// 0x40,0x40,0x40, 0x00, -// 0x50,0x50,0x50, 0x00, -// 0x60,0x60,0x60, 0x00, -// 0x70,0x70,0x70, 0x00, -// 0x80,0x80,0x80, 0x00, -// 0x90,0x90,0x90, 0x00, -// 0xA0,0xA0,0xA0, 0x00, -// 0xB0,0xB0,0xB0, 0x00, -// 0xC0,0xC0,0xC0, 0x00, -// 0xD0,0xD0,0xD0, 0x00, -// 0xE0,0xE0,0xE0, 0x00, -// 0xF0,0xF0,0xF0, 0x00 -// }; #define ATTR_TEXT COLOR(COLOR_LIGHTGRAY, COLOR_BLUE) #define ATTR_TEXT_TITLE1 COLOR(COLOR_YELLOW, COLOR_BLUE) @@ -103,47 +86,47 @@ static const uint8_t md_pallete[16 * 4] = { #define ATTR_MENU_T COLOR(COLOR_BLACK, COLOR_LIGHTCYAN) #define ATTR_MENU_K COLOR(COLOR_YELLOW, COLOR_BLACK) -/* ---- global state ------------------------------------------------- */ +/* ---- Глобальное состояние ---------------------------------------- */ -/* Files up to MAX_FILE = 128 KB are supported by spreading content - * across up to 8 EMM pages. Only one page is mapped into W3 at any - * given time (FILE_BUF); fb() swaps pages on demand and tracks the - * currently-mapped one in cur_page to avoid redundant OUTs. +/* Файл до 128 КБ хранится в наборе EMM-страниц (до 8 шт. по 16 КБ). + * В окно W3 одновременно отображается только одна страница (FILE_BUF): + * функция fb() при необходимости переключает маппинг и кэширует текущую + * страницу в cur_page, чтобы не делать лишние переключения. * - * The screen-segment index is a separate EMM block of 8-byte records - * (one per VISIBLE viewport row), accessed through W3 via idx_get/idx_put. - * Each record holds the segment's start offset, flags (cont/nowrap/blank/ - * code) and the initial inline style; the right bound is the next record's - * offset. This frees ~11 KB of near RAM and lifts the old 2048-line cap. + * Индекс экранных сегментов хранится отдельно в EMM (записи по 8 байт, + * одна запись на видимую строку). Каждая запись содержит: + * - смещение первого байта сегмента в исходном файле, + * - флаги (continuation/nowrap/blank/code), + * - начальный inline-стиль сегмента. + * Правая граница сегмента определяется смещением следующей записи. + * Такой подход освобождает около 11 КБ ближней RAM и снимает старый + * лимит MAX_LINES для near-массивов. */ -/* NOTE: explicit `= 0` initialisers are MANDATORY on SDCC z80. - * Uninitialised static storage (`static T x;`) doesn't actually allocate - * space — multiple such declarations end up at the SAME address and - * silently overwrite each other. See memory/sdcc_static_storage_gotcha. */ -/* Screen-segment index. One compact record per VISIBLE viewport row, - * stored in a dedicated EMM block (through W3) rather than scarce near - * RAM. This both removes the old MAX_LINES near-array cap and frees - * ~11 KB of W2 for code/stack. */ +/* В SDCC/z80 критично явно инициализировать static-переменные (`= 0`). + * Неинициализированные static-объекты могут конфликтовать по размещению + * и перезаписывать друг друга. См. заметку memory/sdcc_static_storage_gotcha. */ +/* Компактная запись индекса сегмента. + * Каждый элемент соответствует одной строке рендера в viewport. */ typedef struct idx_rec_s { - uint32_t off; /* first source byte rendered by this segment */ - uint8_t flags; /* IF_* bits below */ - uint8_t style; /* INIT_STYLE_* active at segment start */ - uint8_t pad0; /* padding → 8-byte record (2048 records / 16 KB page) */ + uint32_t off; /* смещение первого байта исходного текста для сегмента */ + uint8_t flags; /* битовые флаги IF_* ниже */ + uint8_t style; /* начальный стиль INIT_STYLE_* в начале сегмента */ + uint8_t pad0; /* добивка до 8 байт (2048 записей на страницу 16 КБ) */ uint8_t pad1; } idx_rec_t; #define INDEX_REC_SIZE 8u -#define INDEX_RECS_PER_PAGE 2048u /* 16384 / 8 — records never straddle a page */ -#define MAX_INDEX_PAGES 8u /* 8 * 2048 = 16384 segments max */ +#define INDEX_RECS_PER_PAGE 2048u /* 16384 / 8; запись всегда целиком в одной странице */ +#define MAX_INDEX_PAGES 8u /* максимум 8 * 2048 = 16384 сегментов индекса */ -#define IF_CONT 0x01u /* continuation of a wrapped logical line */ -#define IF_NOWRAP 0x02u /* must not wrap (code block / HR / table) */ -#define IF_BLANK 0x04u /* visually blank row */ -#define IF_CODE 0x08u /* fenced code-block body (verbatim style) */ +#define IF_CONT 0x01u /* сегмент является продолжением перенесённой строки */ +#define IF_NOWRAP 0x02u /* строка не переносится (кодовый блок / HR / таблица) */ +#define IF_BLANK 0x04u /* визуально пустая строка */ +#define IF_CODE 0x08u /* тело fenced code-блока (verbatim-режим) */ static uint16_t n_lines = 0; -static uint16_t max_lines = 0; /* index capacity = index_pages * 2048 */ +static uint16_t max_lines = 0; /* ёмкость индекса: index_pages * 2048 */ static uint16_t top_line = 0; static uint32_t file_size = 0; static uint8_t file_blk = 0; @@ -152,27 +135,29 @@ static uint8_t file_phys[MAX_PAGES] = {0}; static uint8_t index_blk = 0; static uint8_t index_pages = 0; static uint8_t index_phys[MAX_INDEX_PAGES] = {0}; -static uint8_t index_truncated = 0; /* set when the index capacity is exhausted */ -static uint32_t cur_seg_off = 0; /* off of the most-recent emitted seg (near; no bank read-back) */ -static idx_rec_t cur_rec; /* mirror of the most-recent emitted record (near) */ -static uint8_t cur_page = 0xFF; /* 0xFF = no page mapped yet */ -static uint8_t viewport_x = 0; /* horizontal pan (for nowrap lines) */ +static uint8_t index_truncated = 0; /* выставляется при исчерпании ёмкости индекса */ +static uint32_t cur_seg_off = 0; /* смещение последнего emit_seg (кэш в near-памяти) */ +static idx_rec_t cur_rec; /* near-копия последней записанной idx-записи */ +static uint8_t cur_page = 0xFF; /* 0xFF = в W3 ещё не отображена ни одна страница */ +static uint8_t viewport_x = 0; /* горизонтальный сдвиг для nowrap-строк */ static char filename[64] = {0}; #define HPAN_STEP 8u -/* Spinner on the title bar (col=8, row=0): a small animated glyph - * shown while load_file / index_lines run, so the user knows the - * viewer is busy and not frozen. */ +/* Спиннер в статус-баре (col=8,row=0). + * Включается во время загрузки/индексации, чтобы показать, + * что программа занята и не зависла. */ #define SPINNER_COL 8 static const char spinner_chars[4] = { '|', '/', '-', '\\' }; static uint8_t spinner_phase = 0; static uint8_t spinner_active = 0; /* ================================================================== - * tiny helpers + * Вспомогательные функции * ================================================================== */ +/* Печатает строку с указанным атрибутом, начиная с позиции (x,y), + * и останавливается на конце строки или правой границе экрана. */ static void put_str_attr(uint8_t x, uint8_t y, const char *s, uint8_t attr) { while (*s && x < SCREEN_W) { @@ -180,14 +165,15 @@ static void put_str_attr(uint8_t x, uint8_t y, const char *s, uint8_t attr) } } +/* Упрощённый вывод строки через gotoxy/puts без явного атрибута. */ static void put_str(uint8_t x, uint8_t y, const char *s) { gotoxy(x, y); puts(s); } -/* Convert uint16 to decimal text without printf. - * Returns number of produced digits in out[] (1..5). */ +/* Преобразует uint16 в десятичную строку без printf. + * Возвращает число записанных цифр в out[] (1..5). */ static uint8_t u16_to_dec(uint16_t v, char *out) { char tmp[5]; @@ -204,7 +190,7 @@ static uint8_t u16_to_dec(uint16_t v, char *out) return n; } -/* Draw uint16 in decimal, right-aligned to `width` cells, using attr. */ +/* Рисует uint16 в десятичном виде, с выравниванием вправо по ширине width. */ static void put_u16_right_attr(uint8_t x, uint8_t y, uint16_t v, uint8_t width, uint8_t attr) { char buf[5]; @@ -214,12 +200,13 @@ static void put_u16_right_attr(uint8_t x, uint8_t y, uint16_t v, uint8_t width, for (uint8_t i = 0; i < len && x < SCREEN_W; i++) wrchar(x++, y, buf[i], attr); } -/* Draw uint8 in decimal, right-aligned to `width` cells, using attr. */ +/* Рисует uint8 в десятичном виде, с выравниванием вправо по ширине width. */ static void put_u8_right_attr(uint8_t x, uint8_t y, uint8_t v, uint8_t width, uint8_t attr) { put_u16_right_attr(x, y, (uint16_t)v, width, attr); } +/* Полностью очищает указанную строку экрана символами пробела с attr. */ static void fill_row(uint8_t y, uint8_t attr) { for (uint8_t c = 0; c < SCREEN_W; c++) { @@ -227,8 +214,8 @@ static void fill_row(uint8_t y, uint8_t attr) } } -/* Advance the spinner by one frame. No-op when inactive — safe to call - * unconditionally from any loop that wants to show "still working". */ +/* Продвигает спиннер на один кадр. + * Если спиннер выключен — функция ничего не делает. */ static void spinner_tick(void) { if (!spinner_active) return; @@ -236,32 +223,31 @@ static void spinner_tick(void) spinner_phase++; } -/* Enable / disable the spinner. Disabling also wipes the glyph from the - * title bar so the slot looks like a plain space. */ +/* Включает/выключает спиннер. + * При выключении очищает его позицию пробелом в статус-баре. */ static void spinner_show(uint8_t on) { spinner_active = on; if (!on) wrchar(SPINNER_COL, 0, ' ', ATTR_BAR); } -/* Map the EMM page that holds file offset >= page * PAGE_SIZE into W3. - * No-op if already mapped. */ +/* Отображает указанную EMM-страницу файла в окно W3. + * Если эта страница уже активна, переключение не выполняется. */ static inline void map_page(uint8_t page) { if (page != cur_page) { - /* file_phys[] saves a BIOS mem_get_page() call on every page swap. */ + /* file_phys[] позволяет не вызывать mem_get_page() на каждом свопе. */ sprinter_page_w3(file_phys[page]); cur_page = page; } } - -/* Read one byte from the logical file at byte offset `p`. Swaps the W3 - * mapping if `p` falls into a different page than the one currently - * mapped. Bounds checking is the caller's job. +/* Читает один байт логического файла по смещению p. + * При переходе между страницами автоматически переключает W3. + * Проверка границ выполняется вызывающей стороной. * - * Hot path: index_lines() calls this for almost every input byte. On z80 - * 32-bit shifts/masks are helper calls, so we decode page/offset from the - * little-endian byte layout of `p`: page = bits 14..16, offset = bits 0..13. */ + * Это горячий путь (вызывается очень часто из index_lines()), поэтому + * декодирование page/off сделано через little-endian представление p, + * чтобы избежать тяжёлых 32-битных сдвигов/масок на z80. */ static inline char fb(uint32_t p) { uint8_t *pb = (uint8_t *)&p; @@ -271,7 +257,7 @@ static inline char fb(uint32_t p) return FILE_BUF[off]; } -/* Treat space/tab/EOL/NUL as whitespace for emphasis flanking checks. */ +/* Для проверки границ выделения считаем пробел/таб/конец строки/0 пробельными. */ static uint8_t ws_or_eol(char c) { return (uint8_t)(c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == 0); @@ -283,29 +269,30 @@ static uint8_t ws_or_eol_or_delim(char c) || c == '\t' || c == '\n' || c == '\r' || c == 0); } -/* Inline emphasis marker (asterisk, double-asterisk, underscore) is - * recognised only when ONE side is whitespace/EOL and the other is not - * (XOR). This treats "2 * 3" and "2 ** 3" (whitespace both sides) as - * literal, as well as intraword cases like COLOR_YELLOW (no whitespace - * either side), while keeping *italic*, **bold**, _under_ working. */ +/* Маркер 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)); } -/* Continuation kind passed to emit_seg(); no longer stored (render derives - * the kind from classify_line()), kept only as call-site argument names. */ +/* Тип сегмента-продолжения, передаваемый в emit_seg(). + * Явно в индексе не хранится: рендер при необходимости восстанавливает + * тип через classify_line(), но параметр оставлен для читаемости вызовов. */ #define CK_PLAIN 0 #define CK_QUOTE 1 #define CK_LIST 2 #define CK_OTHER 3 -/* EMM index accessors. Records are 8 bytes and aligned within an index - * page, so bank_read/bank_write never split a record across a page. */ +/* Доступ к индексу в 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 */ + 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); } @@ -316,7 +303,7 @@ static void idx_put(uint16_t idx, const idx_rec_t *r) bank_write(index_phys[page], off, r, INDEX_REC_SIZE); } -/* Field readers used by render & navigation (per-row, not per-byte). */ +/* Чтение полей индекса для рендера и навигации (построчный доступ). */ static uint8_t seg_flags(uint16_t idx) { idx_rec_t r; @@ -338,12 +325,13 @@ static uint8_t is_nowrap(uint16_t idx) { return (uint8_t)((seg_flags(idx) & IF static uint8_t is_blank(uint16_t idx) { return (uint8_t)((seg_flags(idx) & IF_BLANK) != 0); } static uint8_t is_code_body(uint16_t idx){ return (uint8_t)((seg_flags(idx) & IF_CODE) != 0); } -/* Flag setters target the MOST-RECENT emitted seg (n_lines-1) through the - * near mirror cur_rec, so the scan never reads the index back from the bank. */ +/* Установка флагов всегда идёт в последний 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); } +/* Загружает 16 текстовых атрибутов в палитры ink/paper/blink. */ static void set_pallete(void) { uint8_t buff[64]; for(int i = 0; i < 16; i++) { @@ -352,10 +340,6 @@ static void set_pallete(void) { buff[j * 4 + 1] = md_pallete[i * 4 + 1]; buff[j * 4 + 2] = md_pallete[i * 4 + 2]; buff[j * 4 + 3] = md_pallete[i * 4 + 3]; - // buff[j * 4] = i * 16; - // buff[j * 4 + 1] = i * 16; - // buff[j * 4 + 2] = i * 16; - // buff[j * 4 + 3] = 0; } text_pal_load(TEXT_PAL_INK, i * 16, 16, md_pallete); @@ -367,9 +351,11 @@ static void set_pallete(void) { } /* ================================================================== - * file loading + * Загрузка/выгрузка файла * ================================================================== */ +/* Загружает файл в EMM-страницы и подготавливает EMM-индекс. + * Возврат: 0 — успех, отрицательное значение — код ошибки. */ static int load_file(const char *path) { int fd = open(path, O_RDONLY); @@ -383,7 +369,7 @@ static int load_file(const char *path) file_size = (uint32_t)sz; lseek(fd, 0, SEEK_SET); - /* Allocate exactly the number of EMM pages we need (1..MAX_PAGES). */ + /* Выделяем ровно столько EMM-страниц, сколько нужно под файл. */ file_pages = (uint8_t)((file_size + PAGE_SIZE - 1u) / PAGE_SIZE); if (file_pages == 0) file_pages = 1; file_blk = mem_alloc_pages(file_pages); @@ -392,9 +378,8 @@ static int load_file(const char *path) return -3; } - /* Read the file 16 KB at a time, mapping each page into W3 in turn. - * `remaining` decreases by exactly PAGE_SIZE on each non-final iter; - * the final iter reads whatever is left. */ + /* Читаем файл кусками по 16 КБ, последовательно маппя страницы в W3. + * На последней итерации читаем остаток меньше PAGE_SIZE. */ uint32_t remaining = file_size; for (uint8_t page = 0; page < file_pages; page++) { spinner_tick(); @@ -414,9 +399,9 @@ static int load_file(const char *path) } close(fd); - /* Allocate the EMM index block (8 B/segment, 2048 records per 16 KB - * page). Size it a little larger than the file so even dense files - * index fully; back off if EMM is tight. max_lines becomes the cap. */ + /* Выделяем блок индекса в EMM (8 байт на сегмент, 2048 записей/страница). + * Размер берем с запасом относительно файла; если памяти мало — + * уменьшаем количество страниц индекса до доступного значения. */ { uint8_t want = (uint8_t)(file_pages + 1u); if (want > MAX_INDEX_PAGES) want = MAX_INDEX_PAGES; @@ -435,6 +420,7 @@ static int load_file(const char *path) return 0; } +/* Освобождает выделенные EMM-блоки файла и индекса. */ static void unload_file(void) { if (file_blk) { @@ -448,12 +434,12 @@ static void unload_file(void) } /* ================================================================== - * line indexing + * Индексация строк * ================================================================== */ -/* Returns 1 iff the seg at index `idx` is the start of a logical line - * that begins with ``` (a fenced-code block delimiter). Continuation - * segs are never fence delimiters. */ +/* Возвращает 1, если сегмент idx — это старт логической строки, + * и эта строка начинается с ``` (граница блока кода). + * Сегменты-продолжения никогда не считаются такими границами. */ static uint8_t is_fence_delim(uint16_t idx) { if (idx >= n_lines) return 0; @@ -472,11 +458,13 @@ static uint8_t is_fence_delim(uint16_t idx) #define INIT_STYLE_CODE 0x4 #define INIT_STYLE_STRIKE 0x5 -/* Indexed by init_style & 7; order must match INIT_STYLE_* / EM_* below. */ +/* Таблица сопоставления 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 }; +/* Возвращает стартовый атрибут сегмента из индексной записи. */ static uint8_t get_init_style(uint16_t idx) { idx_rec_t r; @@ -485,6 +473,7 @@ static uint8_t get_init_style(uint16_t idx) return styles_map[r.style & 7u]; } +/* Возвращает «сырой» INIT_STYLE_* из индексной записи. */ static uint8_t get_init_style_raw(uint16_t idx) { idx_rec_t r; @@ -493,10 +482,9 @@ static uint8_t get_init_style_raw(uint16_t idx) return (uint8_t)(r.style & 7u); } -/* Returns 1 iff the logical line at file offset `p_start` should NOT be - * wrapped — it must render on a single seg even if longer than SCREEN_W. - * Currently covers fence delimiters and horizontal rules; tables will be - * added when Phase 4 tables / Phase 6 table layout lands. */ +/* Возвращает 1, если строку нельзя переносить: + * она должна рендериться одним сегментом даже если длиннее SCREEN_W. + * Сейчас учитываются границы блока кода и горизонтальные линии. */ static uint8_t is_nowrap_line(uint32_t p_start) { if (p_start + 2 < file_size && @@ -525,36 +513,34 @@ static uint8_t is_nowrap_line(uint32_t p_start) return 0; } -/* Line kind returned by classify_line(). */ +/* Тип строки, который возвращает classify_line(). */ #define LK_PLAIN 0 #define LK_H1 1 #define LK_H2 2 #define LK_H3 3 -#define LK_H4 4 /* also used for H5/H6 */ +#define LK_H4 4 /* также используется для H5/H6 */ #define LK_HR 5 -#define LK_ULIST 6 /* "- ", "* ", "+ " bullet */ -#define LK_OLIST 7 /* "12. " or "12) " number */ -#define LK_QUOTE 8 /* "> " blockquote */ +#define LK_ULIST 6 /* маркер ненумерованного списка */ +#define LK_OLIST 7 /* маркер нумерованного списка */ +#define LK_QUOTE 8 /* маркер цитаты */ -/* Classify a logical line. - * - Headers: 1..4 leading '#' followed by space/tab (H5+/H6 collapse to H4). - * - Horizontal rule: line containing only '-', '*' or '_' (one kind, >=3 - * of them), optionally separated by spaces/tabs. Matches `---`, `***`, - * `___`, `- - -`, etc. Checked BEFORE ulist so that `- - -` wins. - * - Unordered list: "- ", "* " or "+ " followed by content. - * - Ordered list: one or more digits followed by '.' or ')' then space. - * - Blockquote: '>' optionally followed by space. - * - Otherwise plain text. - * For headers/lists/quotes, *out_content is updated to the offset of the - * first content byte (past the marker and its trailing space). - */ +/* Классифицирует логическую строку. + * - Заголовки: 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); - /* Header? */ + /* Заголовок? */ if (c0 == '#') { uint8_t lvl = 0; uint32_t p = p_start; @@ -570,10 +556,11 @@ static uint8_t classify_line(uint32_t p_start, uint32_t *out_content) return lvl; /* LK_H1 .. LK_H4 */ } } - /* Bare '#' with no space → not a header, fall through. */ + /* Символ '#' без пробела после него не считается заголовком. */ } - /* Horizontal rule? (must precede ulist so `- - -` wins over `- `) */ + /* Горизонтальный разделитель. + * Проверяется до списков, чтобы `- - -` не стало маркером списка. */ if (c0 == '-' || c0 == '*' || c0 == '_') { char marker = 0; uint8_t count = 0; @@ -591,14 +578,13 @@ static uint8_t classify_line(uint32_t p_start, uint32_t *out_content) if (ok && count >= 3) return LK_HR; } - /* For lists and blockquotes we additionally allow leading spaces - * (light nested-list support). HR / header / fence delim stay - * strict col-0 — done above with `c0`. */ + /* Для списков/цитат дополнительно допускаем ведущие пробелы + * (базовая поддержка вложенности). */ uint32_t lp = p_start; while (lp < file_size && fb(lp) == ' ') lp++; char cl = (lp < file_size) ? fb(lp) : 0; - /* Unordered list? */ + /* Ненумерованный список? */ if ((cl == '-' || cl == '*' || cl == '+') && (lp + 1) < file_size && fb(lp + 1) == ' ') { @@ -606,7 +592,7 @@ static uint8_t classify_line(uint32_t p_start, uint32_t *out_content) return LK_ULIST; } - /* Ordered list? */ + /* Нумерованный список? */ if (cl >= '0' && cl <= '9') { uint32_t p = lp; while (p < file_size) { @@ -624,7 +610,7 @@ static uint8_t classify_line(uint32_t p_start, uint32_t *out_content) } } - /* Blockquote? */ + /* Цитата? */ if (cl == '>') { uint32_t p = lp + 1; if (p < file_size && fb(p) == ' ') p++; @@ -635,26 +621,23 @@ static uint8_t classify_line(uint32_t p_start, uint32_t *out_content) return LK_PLAIN; } -/* Visible column count for the marker prefix when rendered, including - * any leading indent for nested lists/quotes (the render side keeps the - * 1 byte = 1 visible col invariant for those, so content_off - p_start - * is the visible width). */ +/* Возвращает видимую ширину префикса (отступ + маркер) в колонках. */ 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 (skipped) */ + default: return 0; /* plain/headers спец-префикса не имеют */ } } -/* Emit a new segment entry into the EMM index. cur_rec (near) mirrors the - * just-written record so set_*_cur() can add flags without a bank read-back, - * and cur_seg_off feeds the per-byte wrap logic without reading the index. */ +/* Добавляет запись сегмента в EMM-индекс. + * near-копии cur_rec/cur_seg_off используются для быстрых последующих + * операций без повторного чтения последней записи из банка. */ static void emit_seg(uint32_t off, uint8_t style, uint8_t ckind, uint8_t cont) { - (void)ckind; /* kind is derived at render time */ + (void)ckind; /* тип сегмента восстанавливается на этапе рендера */ if (n_lines >= max_lines) { index_truncated = 1; return; } cur_rec.off = off; cur_rec.flags = (uint8_t)(cont ? IF_CONT : 0); @@ -666,6 +649,7 @@ static void emit_seg(uint32_t off, uint8_t style, uint8_t ckind, uint8_t cont) n_lines++; } +/* Проверяет, что физическая строка пуста (только пробелы/таб + перевод строки). */ static uint8_t is_line_blank(uint32_t p) { while (p < file_size) { @@ -677,12 +661,14 @@ static uint8_t is_line_blank(uint32_t p) return 1; } +/* Быстрая проверка начала блока кода по маркеру ``` . */ static uint8_t is_fence_raw(uint32_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); @@ -702,8 +688,8 @@ static uint8_t is_hr_raw(uint32_t p) return ok && count >= 3; } -/* A table row: the first non-space character on the line is '|'. Leading - * spaces are allowed. Table rows are nowrap and never merged. */ +/* Строка таблицы: первый непустой символ — '|'. + * Ведущие пробелы разрешены; такие строки не склеиваются и не переносятся. */ static uint8_t is_table_raw(uint32_t p) { while (p < file_size) { @@ -714,12 +700,9 @@ static uint8_t is_table_raw(uint32_t p) return 0; } -/* Shared inline-emphasis / wrap scanner for a single line or paragraph stream. - * Starts at `q`, processes until `q_end` (or paragraph break for plain text), - * emits continuation segs as needed. `col` is the starting visible column - * (0 for plain/headers, marker width for list/quote). `ckind` is the - * continuation-kind for wrap segs. Returns the final `line_style` (for - * paragraph streams that carry emphasis across soft/hard breaks). */ +/* Общий сканер inline-форматирования и переносов. + * Идёт от q до q_end, учитывает стартовую колонку col и при необходимости + * эмитит continuation-сегменты. Возвращает итоговый line_style. */ static uint8_t inline_scan(uint32_t q, uint32_t q_end, uint8_t col, uint8_t ckind, uint8_t line_style) { @@ -731,7 +714,7 @@ static uint8_t inline_scan(uint32_t q, uint32_t q_end, uint8_t col, char ch = fb(q); if (ch == '\n') { - /* hard break (already checked by caller) or stream end */ + /* Жёсткий перенос (уже проверен снаружи) или конец потока. */ q++; emit_seg(q, line_style, ckind, (ckind == CK_LIST || ckind == CK_QUOTE) ? 1 : 0); last_space = 0xFFFFFFFFu; prev_ch = ' '; seg_col = col; @@ -799,24 +782,25 @@ static uint8_t inline_scan(uint32_t q, uint32_t q_end, uint8_t col, return line_style; } +/* Строит индекс сегментов рендера по загруженному файлу. */ static void index_lines(void) { uint8_t in_block = 0; n_lines = 0; index_truncated = 0; - cur_seg_off = 0xFFFFFFFFu; /* no segment emitted yet */ + cur_seg_off = 0xFFFFFFFFu; /* пока не эмитили ни одного сегмента */ if (file_size == 0) return; uint32_t p = 0; while (p < file_size && n_lines < max_lines) { if ((n_lines & 15) == 0) spinner_tick(); - /* Blank line = paragraph separator. Emit one empty screen row - * (only if the previous emitted line was not already blank). */ + /* Пустая строка — разделитель параграфов. + * В индекс добавляем не более одной пустой экранной строки подряд. */ if (is_line_blank(p)) { while (p < file_size && fb(p) != '\n') p++; if (p < file_size) p++; - /* Collapse a run of blank lines to a single blank row. */ + /* Серия пустых строк схлопывается в одну визуальную строку. */ if (n_lines == 0 || !(cur_rec.flags & IF_BLANK)) { emit_seg(p, INIT_STYLE_PLAIN, CK_PLAIN, 0); set_blank_cur(); @@ -824,7 +808,7 @@ static void index_lines(void) continue; } - /* ---- fenced code block (delimiter or body) ---- */ + /* ---- fenced code-блок (граница или тело) ---- */ if (is_fence_raw(p)) { in_block = !in_block; emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0); @@ -842,7 +826,7 @@ static void index_lines(void) continue; } - /* ---- horizontal rule ---- */ + /* ---- горизонтальный разделитель ---- */ if (is_hr_raw(p)) { emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0); set_nowrap_cur(); @@ -851,7 +835,7 @@ static void index_lines(void) continue; } - /* ---- table row (| ... |): one nowrap seg per source line ---- */ + /* ---- строка таблицы (| ... |): один nowrap-сегмент на исходную строку ---- */ if (is_table_raw(p)) { emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0); set_nowrap_cur(); @@ -860,11 +844,11 @@ static void index_lines(void) continue; } - /* ---- classify current line ---- */ + /* ---- классификация текущей строки ---- */ uint32_t content_off = p; uint8_t kind = classify_line(p, &content_off); - /* ---- Header: single line, not merged into paragraphs ---- */ + /* ---- Заголовок: отдельная строка, не склеивается с параграфом ---- */ if (kind >= LK_H1 && kind <= LK_H4) { emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0); uint32_t line_end = content_off; @@ -875,7 +859,7 @@ static void index_lines(void) continue; } - /* ---- List item: may span multiple source lines ---- */ + /* ---- Пункт списка: может занимать несколько физических строк ---- */ if (kind == LK_ULIST || kind == LK_OLIST) { uint8_t col = marker_visible_col(kind, p, content_off); uint8_t line_style = INIT_STYLE_PLAIN; @@ -895,12 +879,12 @@ static void index_lines(void) uint32_t next = q + 1; if (next >= file_size) break; - /* Blank line ends the current list item. */ + /* Пустая строка завершает текущий пункт списка. */ if (is_line_blank(next)) break; - /* Block boundaries also end the item. */ + /* Границы блоков тоже завершают пункт. */ if (is_fence_raw(next) || is_hr_raw(next) || is_table_raw(next)) break; - /* Next list marker starts a new item; same for quote/header/hr. */ + /* Новый маркер списка/цитаты/заголовка/разделителя начинает новый блок. */ uint32_t next_content = next; uint8_t next_kind = classify_line(next, &next_content); if (next_kind == LK_ULIST || next_kind == LK_OLIST || @@ -910,8 +894,8 @@ static void index_lines(void) break; } - /* Lazy continuation: join the next non-marker line with - * one space; trim its leading indentation first. */ + /* Ленивая склейка: соединяем с немаркерной строкой + * через один пробел, убирая её ведущий отступ. */ while (next < file_size) { char ws = fb(next); if (ws == ' ' || ws == '\t') next++; @@ -989,8 +973,8 @@ static void index_lines(void) } if (ch == ' ') { if (soft_break) { - /* Virtual separator space between joined lines: - * do NOT consume source byte at q here. */ + /* Виртуальный разделитель между склеенными строками: + * исходный байт по q здесь не потребляем. */ last_space = q; seg_col++; } else { @@ -1013,14 +997,14 @@ static void index_lines(void) } } - /* Move to start of the next physical line after the item. */ + /* Переходим к началу следующей физической строки после пункта. */ if (q < file_size) { while (q < file_size && fb(q) != '\n') q++; if (q < file_size) q++; } - /* One blank line between adjacent list markers is suppressed - * (same list). Two+ blank lines keep a visible separator. */ + /* Одна пустая строка между соседними маркерами списка подавляется. + * Две и более пустых строки оставляют видимый разделитель. */ if (q < file_size && is_line_blank(q)) { uint32_t t = q; uint8_t blanks = 0; @@ -1034,7 +1018,7 @@ static void index_lines(void) uint32_t d = t; uint8_t nk = classify_line(t, &d); if (nk == LK_ULIST || nk == LK_OLIST) { - p = t; /* suppress the single blank row */ + p = t; /* подавляем единственную пустую строку */ continue; } } @@ -1045,7 +1029,7 @@ static void index_lines(void) continue; } - /* ---- Quote paragraph: may span multiple source quote lines ---- */ + /* ---- Параграф цитаты: может покрывать несколько физических строк ---- */ if (kind == LK_QUOTE) { uint8_t col = marker_visible_col(kind, p, content_off); uint8_t line_style = INIT_STYLE_PLAIN; @@ -1064,21 +1048,21 @@ static void index_lines(void) uint32_t next = q + 1; if (next >= file_size) break; - /* Real blank line ends the quote block. */ + /* Реальная пустая строка завершает блок цитаты. */ if (is_line_blank(next)) break; if (is_fence_raw(next) || is_hr_raw(next) || is_table_raw(next)) break; - /* Only the next quote line may continue this paragraph. */ + /* Продолжать параграф может только следующая quote-строка. */ uint32_t next_content = next; uint8_t next_kind = classify_line(next, &next_content); if (next_kind != LK_QUOTE) break; - /* Determine the physical end of that quote line. */ + /* Ищем физический конец следующей quote-строки. */ uint32_t next_line_end = next_content; while (next_line_end < file_size && fb(next_line_end) != '\n') next_line_end++; - /* An empty quote line (`>`/`> `) is a paragraph separator: - * stop here; the outer loop will emit it as its own quoted row. */ + /* Пустая quote-строка (`>`/`> `) разделяет параграфы: + * останавливаем склейку, внешняя петля обработает её отдельно. */ uint8_t empty_quote = 1; for (uint32_t z = next_content; z < next_line_end; z++) { char c = fb(z); @@ -1086,23 +1070,23 @@ static void index_lines(void) } if (empty_quote) break; - /* Nested quote (`> > ...`) starts a new quote paragraph; - * keep it as a separate row instead of joining into text. */ + /* Вложенная цитата (`> > ...`) начинает новый quote-параграф, + * поэтому не склеиваем её с текущим текстом. */ { uint32_t t = next_content; while (t < next_line_end && (fb(t) == ' ' || fb(t) == '\t')) t++; if (t < next_line_end && fb(t) == '>') break; } - /* Soft-join with one separating space; trim leading - * indentation in the continued quote content. */ + /* Мягкая склейка через один пробел + удаление отступа + * у продолжения quote-текста. */ while (next_content < next_line_end) { char ws = fb(next_content); if (ws == ' ' || ws == '\t') next_content++; else break; } - /* Страховка: если в точке склейки остался маркер quote, - * поглощаем его (и один пробел после него). */ + /* Страховка: если в точке склейки всё ещё есть `>`, + * поглощаем маркер и один пробел после него. */ if (next_content < next_line_end && fb(next_content) == '>') { next_content++; if (next_content < next_line_end && fb(next_content) == ' ') next_content++; @@ -1209,7 +1193,7 @@ static void index_lines(void) continue; } - /* ---- Plain text paragraph: merge across soft breaks ---- */ + /* ---- Обычный текстовый параграф: склейка через soft-break ---- */ { uint8_t line_style = INIT_STYLE_PLAIN; emit_seg(p, line_style, CK_PLAIN, 0); @@ -1224,9 +1208,9 @@ static void index_lines(void) uint8_t soft_break = 0; if (ch == '\n') { - /* Paragraph break? */ + /* Проверяем, не завершился ли параграф. */ if (q + 1 >= file_size) break; - if (fb(q + 1) == '\n') break; /* blank line */ + if (fb(q + 1) == '\n') break; /* пустая строка */ uint32_t next = q + 1; if (is_fence_raw(next)) break; if (is_hr_raw(next)) break; @@ -1235,9 +1219,9 @@ static void index_lines(void) if (classify_line(next, &dummy) != LK_PLAIN) break; if (is_line_blank(next)) break; - /* Wide break? Two trailing spaces/tabs OR a trailing - * backslash before the newline force a line break inside - * the paragraph (the inline style is preserved). */ + /* Жёсткий перенос: + * два пробела/таба в конце строки ИЛИ завершающий '\' + * перед newline. При этом inline-стиль сохраняется. */ uint8_t is_hard = 0; if (q >= p + 1) { char b1 = fb(q - 1); @@ -1257,7 +1241,7 @@ static void index_lines(void) continue; } - /* Soft break — treat as a single space. */ + /* Мягкий перенос: считаем его одиночным пробелом. */ q++; soft_break = 1; ch = ' '; @@ -1357,10 +1341,9 @@ static void index_lines(void) if (q >= file_size) { p = file_size; } else { - /* q sits on the paragraph's trailing newline; resume at the - * next line so the outer loop classifies it. A following - * blank line then becomes the single separating blank row - * (previously it was swallowed, gluing paragraphs together). */ + /* q стоит на завершающем newline параграфа. + * Переходим на следующую строку, чтобы внешняя петля её + * корректно переклассифицировала. */ while (q < file_size && fb(q) != '\n') q++; if (q < file_size) q++; p = q; @@ -1379,9 +1362,9 @@ static void render_line(uint16_t line_idx, uint8_t row) { uint8_t col = 0; uint8_t base_attr = ATTR_TEXT; - uint8_t cur_attr = is_code_body(line_idx) ? ATTR_TEXT_CODE : get_init_style(line_idx); // ATTR_TEXT; + uint8_t cur_attr = is_code_body(line_idx) ? ATTR_TEXT_CODE : get_init_style(line_idx); uint8_t emph = is_code_body(line_idx) ? EM_CODE : get_init_style_raw(line_idx); - uint8_t parse_inline = 1; /* skip inline parsing for fenced code body */ + uint8_t parse_inline = 1; /* 1: включён парсер выделений, 0: выводим код как есть */ if (is_blank(line_idx)) { while (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT); @@ -1392,17 +1375,17 @@ static void render_line(uint16_t line_idx, uint8_t row) uint32_t p = seg_off(line_idx); uint32_t content_off = p; uint8_t cont = is_cont(line_idx); - /* Right bound for this seg: start of next seg, or file end. */ + /* Правая граница сегмента: начало следующего сегмента или конец файла. */ uint32_t seg_end = seg_off((uint16_t)(line_idx + 1)); if (seg_end > file_size) seg_end = file_size; uint8_t kind; - uint8_t quote_stream = 0; /* 1: этот сегмент принадлежит quote-параграфу */ + uint8_t quote_stream = 0; /* 1: текущий сегмент относится к потоку цитаты */ - /* Continuation seg → repeat prefix/indent from the first non-CONT - * segment of this logical line, then render plain text. */ + /* Для сегмента-продолжения повторяем префикс/отступ + * от первого не-CONT сегмента логической строки. */ if (cont) { uint16_t first = line_idx; - /* Для любого continuation-сегмента откатываемся к первому + /* Для любого сегмента-продолжения откатываемся к первому * НЕ-cont сегменту этой логической строки. */ while (first > 0 && is_cont(first)) first--; uint32_t p_start = seg_off(first); @@ -1410,7 +1393,7 @@ static void render_line(uint16_t line_idx, uint8_t row) uint8_t first_kind = classify_line(p_start, &content_off); uint8_t indent = marker_visible_col(first_kind, p_start, content_off); if (first_kind == LK_QUOTE) { - /* Для continuation quote нужно помнить тип потока, + /* Для продолжения цитаты нужно помнить тип потока, * чтобы далее на newline корректно пропускать сырой `>`. */ quote_stream = 1; uint8_t q = 0; @@ -1427,14 +1410,15 @@ static void render_line(uint16_t line_idx, uint8_t row) q++; } } - /* start rendering from the wrapped segment offset */ + /* Дальше рендерим контент именно с текущего сегмента-продолжения. */ p = seg_off(line_idx); content_off = p; - /* For cont segs, inline parsing continues normally. */ + /* Для сегмента-продолжения парсер работает как обычно. */ } - /* Fenced code block: delimiter line → blank row; body lines render - * verbatim in ATTR_TEXT_CODE without any inline-emphasis parsing. */ + /* Кодовый блок: + * - строка-разделитель показывается как пустая; + * - тело рисуется «как есть» с ATTR_TEXT_CODE без inline-парсинга. */ if (is_fence_delim(line_idx)) { while (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT); return; @@ -1446,10 +1430,9 @@ static void render_line(uint16_t line_idx, uint8_t row) kind = LK_PLAIN; quote_stream = 0; } else if (cont) { - /* Continuation row: the parent prefix/indent is already drawn - * above. Render the wrapped content as plain text — do NOT - * re-classify it, or a wrapped word beginning with '-', '#', - * '>' etc. would be mis-drawn as a new marker. */ + /* Строка-продолжение: префикс уже нарисован выше. + * Содержимое рисуем как plain, без повторной классификации, + * иначе начало слова могло бы ложно стать маркером. */ kind = LK_PLAIN; } else { kind = classify_line(p, &content_off); @@ -1470,20 +1453,19 @@ static void render_line(uint16_t line_idx, uint8_t row) cur_attr = base_attr; p = content_off; } else if (kind == LK_ULIST) { - /* Light nested-list support: any leading spaces before '-' / '*' - * / '+' render as plain spaces (preserves nesting visually); - * then a bullet + a content space. */ + /* Базовая поддержка вложенности: ведущие пробелы сохраняем, + * затем рисуем маркер списка и пробел перед содержимым. */ uint32_t q = p; while (q + 2 < content_off && col < SCREEN_W && fb(q) == ' ') { wrchar(col++, row, ' ', ATTR_TEXT); q++; } - if (col < SCREEN_W) wrchar(col++, row, 0x07, ATTR_LIST_MARKER); /* • */ + if (col < SCREEN_W) wrchar(col++, row, 0x07, ATTR_LIST_MARKER); /* символ маркера списка */ if (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT); p = content_off; } else if (kind == LK_OLIST) { - /* Indent (if any) → spaces; digits and '.' or ')' in marker - * colour; trailing space in plain. */ + /* Отступ -> пробелы; цифры и '.' / ')' рисуем как маркер; + * завершающий пробел — обычным атрибутом текста. */ uint32_t q = p; while (q < content_off && col < SCREEN_W && fb(q) == ' ') { wrchar(col++, row, ' ', ATTR_TEXT); @@ -1496,8 +1478,8 @@ static void render_line(uint16_t line_idx, uint8_t row) if (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT); p = content_off; } else if (kind == LK_QUOTE) { - /* Indent (if any) → spaces; '│' (0xB3) prefix in marker - * colour, then a space, then content. */ + /* Отступ -> пробелы; затем quote-префикс '│' (0xB3), + * пробел и далее содержимое. */ uint32_t q = p; while (q + 1 < content_off && col < SCREEN_W && fb(q) == ' ') { wrchar(col++, row, ' ', ATTR_TEXT); @@ -1508,57 +1490,72 @@ static void render_line(uint16_t line_idx, uint8_t row) p = content_off; } - /* Effective horizontal offset: only nowrap lines honour viewport_x. */ + /* Эффективный горизонтальный сдвиг: применяется только к nowrap-строкам. */ uint8_t effective_vx = is_nowrap(line_idx) ? viewport_x : 0; - /* `cc` is the VISIBLE column inside the content area (the part - * after any fixed marker). For nowrap lines, chars at cc < viewport_x - * are skipped so the user can pan horizontally. */ + /* cc — видимая колонка внутри области контента + * (после фиксированного префикса строки). */ uint8_t cc = 0; - char prev_ch = ' '; /* for `_` flanking check */ + char prev_ch = ' '; /* используется в проверке границ emphasis */ while (p < seg_end && col < SCREEN_W) { - uint8_t soft_join = 0; /* 1: вставлен виртуальный пробел при склейке quote-строк */ + uint8_t soft_join = 0; /* 1: вставлен виртуальный пробел мягкой склейки, p уже сдвинут */ char ch = fb(p); - /* A backslash immediately before this segment's end-of-line is a - * wide-break marker (index_lines split here) — consume it without - * drawing. Not applied to verbatim code bodies. */ + /* '\' + * перед концом сегмента — это маркер wide-break: + * поглощаем его без вывода (кроме verbatim code-body). */ if (parse_inline && ch == '\\' && (p + 1) < seg_end) { char nb = fb(p + 1); if (nb == '\n' || nb == '\r') { p++; continue; } } - /* Soft break (newline inside a paragraph segment). - * Для quote-потока нужно не только отрисовать пробел, но и - * пропустить сырой quote-маркер следующей физической строки. */ - if (parse_inline && quote_stream && (ch == '\n' || ch == '\r')) { + /* Мягкий перенос (перевод строки внутри одного сегмента). + * Рендер должен повторить логику индексатора: + * - quote: пропустить сырой `> ` следующей строки; + * - list/plain: схлопнуть перенос + ведущий отступ в 1 пробел. */ + if (parse_inline && (ch == '\n' || ch == '\r')) { uint32_t next = p + 1; - if (next < seg_end) { + if (quote_stream && next < seg_end) { uint32_t next_content = next; if (classify_line(next, &next_content) == LK_QUOTE) { - /* Мягкая склейка quote-строк: newline -> ' ', - * а `> ` следующей строки не попадает в вывод. */ + /* Мягкая склейка quote-строк: `> ` не рендерим. */ p = next_content; ch = ' '; soft_join = 1; } else { + /* Защита: если внутри quote-сегмента встретился + * не-цитатный перенос, работаем как обычная мягкая склейка. */ + while (next < seg_end) { + char ws = fb(next); + if (ws == ' ' || ws == '\t') next++; + else break; + } + if (next > (p + 1)) { + p = next; + soft_join = 1; + } ch = ' '; } } else { + while (next < seg_end) { + char ws = fb(next); + if (ws == ' ' || ws == '\t') next++; + else break; + } + if (next > (p + 1)) { + /* Схлопываем перенос + отступ (ленивое продолжение). */ + p = next; + soft_join = 1; + } ch = ' '; } } else if (ch == '\n' || ch == '\r') { ch = ' '; } - /* --- inline emphasis markers (consumed, not rendered) --- - * Asterisk / double-asterisk / underscore are markers only - * when XOR-flanked (whitespace on exactly one side). Keeps - * COLOR_YELLOW, intraword slashes, "2 * 3", "2 ** 3" etc. - * as literal text. A marker that would conflict with the - * currently-active style is still consumed (zero-width) - * so the index_lines column count stays in sync with the - * rendered output. Skipped entirely inside fenced code - * blocks (parse_inline=0). */ + /* --- маркеры inline-выделения (поглощаются, но не рисуются) --- + * Маркер активен только при XOR-границах (пробел с одной + * стороны). Это защищает выражения вроде "2 * 3" от ложного + * форматирования и синхронизирует ширину рендера с индексатором. */ if (parse_inline) { - /* `**` (bold) */ + /* `**` (жирный) */ if (ch == '*' && (p + 1) < seg_end && fb(p + 1) == '*') { char next_ch = ((p + 2) < seg_end) ? fb(p + 2) : '\n'; if (is_emph_flanked(prev_ch, next_ch)) { @@ -1570,8 +1567,8 @@ static void render_line(uint16_t line_idx, uint8_t row) p += 2; continue; } } - /* literal `**` — render BOTH stars, do NOT fall - * through to single-`*` handler. */ + /* Литерал `**`: рисуем обе звезды, в ветку одиночной + * `*` не проваливаемся. */ p += 2; if (cc >= effective_vx && col < SCREEN_W) wrchar(col++, row, '*', cur_attr); cc++; @@ -1580,7 +1577,7 @@ static void render_line(uint16_t line_idx, uint8_t row) prev_ch = '*'; continue; } - /* `~~` (strikethrough) — same flanking rule as `**` */ + /* `~~` (зачёркнутый) — те же правила границ, что у `**`. */ if (ch == '~' && (p + 1) < seg_end && fb(p + 1) == '~') { char next_ch = ((p + 2) < seg_end) ? fb(p + 2) : '\n'; if (is_emph_flanked(prev_ch, next_ch)) { @@ -1592,7 +1589,7 @@ static void render_line(uint16_t line_idx, uint8_t row) p += 2; continue; } } - /* literal `~~` — render both tildes */ + /* Литерал `~~`: рисуем обе тильды. */ p += 2; if (cc >= effective_vx && col < SCREEN_W) wrchar(col++, row, '~', cur_attr); cc++; @@ -1601,7 +1598,7 @@ static void render_line(uint16_t line_idx, uint8_t row) prev_ch = '~'; continue; } - /* `*` (italic) and `_` (underline) — same flanking rule */ + /* `*` (курсив) и `_` (подчёркивание) — то же XOR-правило. */ if (ch == '*' || ch == '_') { char next_ch = ((p + 1) < seg_end) ? fb(p + 1) : '\n'; if (is_emph_flanked(prev_ch, next_ch)) { @@ -1615,7 +1612,7 @@ static void render_line(uint16_t line_idx, uint8_t row) p++; continue; } } - /* intraword / both-flanked — fall through, literal */ + /* Внутрисловной или двусторонний случай — оставляем литерал. */ } if (ch == '`') { if (emph == EM_NONE) { @@ -1631,8 +1628,7 @@ static void render_line(uint16_t line_idx, uint8_t row) if (!soft_join) p++; if (ch == '\t') { - /* Tab expands in CONTENT-col space (cc), each generated - * space is then conditionally rendered through hpan. */ + /* Таб расширяется в координатах контента (cc). */ uint8_t tgt = (uint8_t)((cc & (uint8_t)~(TAB_STOP - 1)) + TAB_STOP); while (cc < tgt && col < SCREEN_W) { if (cc >= effective_vx) wrchar(col++, row, ' ', cur_attr); @@ -1648,10 +1644,9 @@ static void render_line(uint16_t line_idx, uint8_t row) } } - /* Right-edge truncation indicator for nowrap lines: when the screen - * ran out (col == SCREEN_W) and this row is nowrap, peek forward - * past any zero-width inline markers — if there's still real visible - * content before the end of the line, overlay the last cell with '>'. */ + /* Индикатор обрезки справа для nowrap-строки: + * если после текущей позиции есть ещё видимый контент, + * поверх последней колонки рисуем '>'. */ if (is_nowrap(line_idx) && col >= SCREEN_W) { uint32_t pp = p; uint32_t left_bound = seg_off(line_idx); @@ -1663,7 +1658,7 @@ static void render_line(uint16_t line_idx, uint8_t row) char pc = (pp > left_bound) ? fb(pp - 1) : ' '; char nc = (pp + 2 < seg_end) ? fb(pp + 2) : '\n'; if (is_emph_flanked(pc, nc)) { pp += 2; continue; } - /* literal `**` — there IS visible content */ + /* Литерал `**` тоже считается видимым контентом справа. */ } else if (c == '*' || c == '_') { char pc = (pp > left_bound) ? fb(pp - 1) : ' '; char nc = (pp + 1 < seg_end) ? fb(pp + 1) : '\n'; @@ -1674,21 +1669,20 @@ static void render_line(uint16_t line_idx, uint8_t row) } } - /* Left-edge indicator: when panned right, show that nowrap content - * is hidden off the left side. Overlays column 0. */ + /* Индикатор скрытого контента слева при горизонтальном сдвиге. */ if (is_nowrap(line_idx) && viewport_x > 0) { wrchar(0, row, '<', ATTR_TRUNC); } } - /* Pad rest of the row so previous render content is wiped. For code- - * body rows use the code attribute so the block extends to the right - * edge — visually distinguishes the block from surrounding text. */ + /* Дозаполняем остаток строки пробелами, чтобы стереть хвост + * от предыдущего кадра рендера. */ { uint8_t pad_attr = (parse_inline ? ATTR_TEXT : ATTR_TEXT_CODE); while (col < SCREEN_W) wrchar(col++, row, ' ', pad_attr); } } +/* Рендерит все 30 строк окна, начиная от top_line. */ static void render_viewport(void) { for (uint8_t i = 0; i < VIEW_H; i++) { @@ -1696,6 +1690,7 @@ static void render_viewport(void) } } +/* Возвращает процент прокрутки (0..100) относительно доступного диапазона. */ static uint8_t calc_pct(void) { uint8_t pct; @@ -1706,8 +1701,8 @@ static uint8_t calc_pct(void) return pct; } -/* Render dynamic counters on the status bar without printf. - * Rewrites cols 45..79 fully to avoid stale digits when widths shrink. */ +/* Рисует динамическую часть статус-бара без printf. + * Полностью перезаписывает колонки 45..79, чтобы не оставлять «хвосты». */ static void render_status_numbers(void) { uint16_t last = top_line + VIEW_H; @@ -1716,7 +1711,7 @@ static void render_status_numbers(void) for (uint8_t c = 45; c < SCREEN_W; c++) wrchar(c, 0, ' ', ATTR_BAR); - wrchar(45, 0, 0xB3, ATTR_BAR); /* │ */ + wrchar(45, 0, 0xB3, ATTR_BAR); /* вертикальный разделитель */ wrchar(47, 0, 'L', ATTR_BAR); put_u16_right_attr(49, 0, (uint16_t)(top_line + 1), 5, ATTR_BAR); wrchar(54, 0, '-', ATTR_BAR); @@ -1724,41 +1719,44 @@ static void render_status_numbers(void) put_str_attr(60, 0, " / ", ATTR_BAR); put_u16_right_attr(63, 0, n_lines, 5, ATTR_BAR); - wrchar(70, 0, 0xB3, ATTR_BAR); /* │ */ + wrchar(70, 0, 0xB3, ATTR_BAR); /* вертикальный разделитель */ put_u8_right_attr(73, 0, pct, 3, ATTR_BAR); wrchar(76, 0, '%', ATTR_BAR); } +/* Полностью перерисовывает статус-бар (заголовок + числа). */ static void render_full_status(void) { fill_row(0, ATTR_BAR); - /* "MDVIEW", then col 7 = space, col 8 = spinner slot, col 9 = space, - * col 10+ = filename. Spinner_tick draws over col 8 in ATTR_BAR. */ + /* Размещение в шапке: "MDVIEW", затем слот спиннера и имя файла. */ put_str_attr(1, 0, "MDVIEW", ATTR_BAR); put_str_attr(10, 0, filename, ATTR_BAR); render_status_numbers(); } +/* Обновляет только изменяемую числовую часть статус-бара. */ static void render_updated_status(void) { render_status_numbers(); } +/* Отрисовывает нижнюю строку меню. */ static void render_menu(void) { fill_row(MENU_ROW, ATTR_MENU_T); put_str_attr(1, MENU_ROW, "F1", ATTR_MENU_K); put_str_attr(4, MENU_ROW, "Help", ATTR_MENU_T); - /* F2 slot removed — no wrap/unwrap toggle any more. */ + /* F2 зарезервирован: режимов wrap/unwrap больше нет. */ put_str_attr(SCREEN_W - 10, MENU_ROW, "F10", ATTR_MENU_K); put_str_attr(SCREEN_W - 5, MENU_ROW, "Exit", ATTR_MENU_T); } /* ================================================================== - * scrolling + * Прокрутка * ================================================================== */ +/* Ограничивает top_line допустимым диапазоном с учётом высоты окна. */ static void clamp_top(void) { if (n_lines <= VIEW_H) { @@ -1768,6 +1766,7 @@ static void clamp_top(void) } } +/* Прокрутка вверх на n строк с частичной перерисовкой при n == 1. */ static void scroll_up(uint16_t n) { uint16_t new_top_line = (top_line >= n) ? (uint16_t)(top_line - n) : 0; @@ -1782,6 +1781,7 @@ static void scroll_up(uint16_t n) } } +/* Прокрутка вниз на n строк с частичной перерисовкой при n == 1. */ static void scroll_down(uint16_t n) { uint16_t new_top_line = (top_line + n < n_lines - VIEW_H) ? @@ -1798,9 +1798,8 @@ static void scroll_down(uint16_t n) } } -/* Horizontal pan (only when the viewport contains nowrap lines). The pan - * is bounded by the widest nowrap segment currently on screen, so the user - * cannot scroll past the end of the longest code/table line. */ +/* Горизонтальный сдвиг (только если в окне есть nowrap-строки). + * Максимум сдвига ограничен самой широкой nowrap-строкой на экране. */ static void scroll_h(int8_t delta) { uint16_t maxw = 0; @@ -1811,12 +1810,12 @@ static void scroll_h(int8_t delta) uint32_t a = seg_off(li); uint32_t b = seg_off((uint16_t)(li + 1)); if (b > file_size) b = file_size; - uint16_t w = (b > a) ? (uint16_t)(b - a) : 0; /* ~visible width */ + uint16_t w = (b > a) ? (uint16_t)(b - a) : 0; /* приблизительная видимая ширина */ if (w > maxw) maxw = w; } - if (maxw == 0) return; /* no nowrap content in view */ + if (maxw == 0) return; /* в окне нет nowrap-контента */ - /* Max pan = width beyond the screen, capped to the uint8 pan range. */ + /* Максимальный сдвиг = ширина за пределами экрана, в границах 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; @@ -1830,19 +1829,16 @@ static void scroll_h(int8_t delta) } } -/* Toggle wrap/truncate and rebuild the seg index. Tries to preserve - * the user's scroll position by remembering the byte offset of the - * current top seg and finding the seg that contains that offset after - * the rebuild. Also resets horizontal pan — wrap mode doesn't use it. */ +/* Историческая заглушка от режима wrap/unwrap. + * Функция оставлена только для совместимости по символу. */ static void toggle_wrap(void) { - /* Removed — no longer needed. Kept as stub to avoid - * linker warnings if something still calls it. */ + /* Функционально не используется. */ (void)0; } /* ================================================================== - * help screen + * Справка * ================================================================== */ static const char * const help_lines[] = { @@ -1866,6 +1862,7 @@ static const char * const help_lines[] = { (const char *)0 }; +/* Показывает экран справки и возвращает пользователя обратно в просмотрщик. */ static void help_screen(void) { clrscr_attr(ATTR_TEXT); @@ -1880,9 +1877,10 @@ static void help_screen(void) } /* ================================================================== - * error + * Вывод ошибки * ================================================================== */ +/* Печатает сообщение об ошибке и ждёт нажатия клавиши. */ static void die(const char *msg) { clrscr_attr(ATTR_TEXT); @@ -1892,7 +1890,7 @@ static void die(const char *msg) } /* ================================================================== - * main + * Точка входа * ================================================================== */ int main(int argc, char **argv) @@ -1901,7 +1899,7 @@ int main(int argc, char **argv) if (argc >= 2) path = argv[1]; else path = "SAMPLE.MD"; - /* Copy path → static filename[] for the status bar. */ + /* Копируем путь в static filename[] для отображения в статус-баре. */ { uint8_t i = 0; while (i < (uint8_t)(sizeof(filename) - 1) && path[i]) { @@ -1911,9 +1909,8 @@ int main(int argc, char **argv) filename[i] = 0; } - /* Bring the UI up FIRST so the user sees the menu + title bar (with - * a spinner) immediately, instead of a black screen while we read - * the file and index lines. */ + /* Сначала поднимаем UI, чтобы пользователь видел шапку/меню/спиннер + * уже во время загрузки файла и построения индекса. */ set_videotextmode(TEXT_MODE_80x32); clrscr_attr(ATTR_TEXT); set_pallete(); @@ -1955,7 +1952,7 @@ int main(int argc, char **argv) uint8_t scan = (uint8_t)((k >> 8) & 0x7F); if (ascii) { - if (ascii == 0x1B) break; /* Esc */ + if (ascii == 0x1B) break; /* клавиша Esc */ continue; } switch (scan) {