/* * mdview — просмотрщик Markdown для Sprinter. * * Разметка экрана (80x32): * строка 0 — статус-бар (имя файла / диапазон строк / всего / %) * строки 1..30 — область документа (30 видимых строк текста) * строка 31 — меню (F1 Help / F2 Reserved / F10 Exit) * * Навигация: * стрелки, PgUp/PgDn, Home/End, F1 (справка), F10/Esc (выход). * Горизонтальный сдвиг доступен только для nowrap-строк. * * Память: * CODE → W1 (режим sprinter-cc --memory small) * STACK/HEAP/DATA → W2 * Буфер файла → страницы EMM (до 8 × 16 КБ = 128 КБ), * отображаемые в W3 по требованию через fb()/map_page(). */ #include #include #include #include #include #include #include #include #include /* ---- Геометрия экрана -------------------------------------------- */ #define SCREEN_W 80 #define SCREEN_H 32 #define VIEW_TOP_ROW 1 #define VIEW_H 30 /* видимая область: строки 1..30 включительно */ #define MENU_ROW 31 #define TAB_STOP 4 /* ---- Параметры файла и памяти ------------------------------------ */ #define PAGE_BITS 14u #define PAGE_SIZE (1u << PAGE_BITS) /* размер EMM-страницы: 16 КБ */ #define PAGE_MASK ((uint16_t)(PAGE_SIZE - 1u)) #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, куда мапится текущая EMM-страница */ /* ---- Палитра атрибутов ------------------------------------------- */ static const uint8_t md_pallete[16 * 4] = { 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x00, 0x60, 0x60, 0x00, 0xC8, 0xC8, 0xC8, 0x00, 0xA0, 0xA0, 0x80, 0x00, 0xFF, 0xAA, 0xAA, 0x00, 0xAA, 0xFF, 0xAA, 0x00, 0xFF, 0xFF, 0xAA, 0x00, 0xAA, 0xAA, 0xFF, 0x00, 0xFF, 0xAA, 0xFF, 0x00, 0xAA, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFF }; #define ATTR_RESET COLOR(COLOR_LIGHTGRAY, COLOR_BLACK) #define ATTR_TEXT COLOR(COLOR_LIGHTGRAY, COLOR_BLUE) #define ATTR_TEXT_TITLE1 COLOR(COLOR_YELLOW, COLOR_BLUE) #define ATTR_TEXT_TITLE2 COLOR(COLOR_LIGHTBLUE, COLOR_BLUE) #define ATTR_TEXT_TITLE3 COLOR(COLOR_LIGHTGREEN, COLOR_BLUE) #define ATTR_TEXT_TITLE4 COLOR(COLOR_LIGHTGREEN, COLOR_BLUE) #define ATTR_TEXT_BOLD COLOR(COLOR_LIGHTRED, COLOR_BLUE) #define ATTR_TEXT_ITALIC COLOR(COLOR_LIGHTGREEN, COLOR_BLUE) #define ATTR_TEXT_UNDERSORE COLOR(COLOR_LIGHTMAGENTA, COLOR_BLUE) #define ATTR_TEXT_CODE COLOR(COLOR_WHITE, COLOR_BLUE) #define ATTR_TEXT_STRIKE COLOR(COLOR_DARKGRAY, COLOR_BLUE) #define ATTR_LIST_MARKER COLOR(COLOR_LIGHTCYAN, COLOR_BLUE) #define ATTR_QUOTE_MARKER COLOR(COLOR_CYAN, COLOR_BLUE) #define ATTR_HR COLOR(COLOR_LIGHTGRAY, COLOR_BLUE) #define ATTR_TRUNC COLOR(COLOR_YELLOW, COLOR_BLUE) #define ATTR_BAR COLOR(COLOR_BLACK, COLOR_LIGHTCYAN) #define ATTR_BAR_SPINNER COLOR(COLOR_WHITE, COLOR_LIGHTCYAN) #define ATTR_MENU_T COLOR(COLOR_BLACK, COLOR_LIGHTCYAN) #define ATTR_MENU_K COLOR(COLOR_YELLOW, COLOR_BLACK) /* ---- Глобальное состояние ---------------------------------------- */ /* Файл до 128 КБ хранится в наборе EMM-страниц (до 8 шт. по 16 КБ). * В окно W3 одновременно отображается только одна страница (FILE_BUF): * функция fb() при необходимости переключает маппинг и кэширует текущую * страницу в cur_page, чтобы не делать лишние переключения. * * Индекс экранных сегментов хранится отдельно в EMM (записи по 8 байт, * одна запись на видимую строку). Каждая запись содержит: * - смещение первого байта сегмента в исходном файле, * - флаги (continuation/nowrap/blank/code), * - начальный inline-стиль сегмента. * Правая граница сегмента определяется смещением следующей записи. * Такой подход освобождает около 11 КБ ближней RAM и снимает старый * лимит MAX_LINES для near-массивов. */ /* В SDCC/z80 критично явно инициализировать static-переменные (`= 0`). * Неинициализированные static-объекты могут конфликтовать по размещению * и перезаписывать друг друга. См. заметку memory/sdcc_static_storage_gotcha. */ /* Компактная запись индекса сегмента. * Каждый элемент соответствует одной строке рендера в viewport. */ typedef struct idx_rec_s { uint32_t off; /* смещение первого байта исходного текста для сегмента */ uint8_t flags; /* битовые флаги IF_* ниже */ uint8_t style; /* начальный стиль INIT_STYLE_* в начале сегмента */ uint8_t pad0; /* добивка до 8 байт (2048 записей на страницу 16 КБ) */ uint8_t pad1; } idx_rec_t; #define INDEX_REC_SIZE 8u #define INDEX_RECS_PER_PAGE 2048u /* 16384 / 8; запись всегда целиком в одной странице */ #define MAX_INDEX_PAGES 8u /* максимум 8 * 2048 = 16384 сегментов индекса */ #define IF_CONT 0x01u /* сегмент является продолжением перенесённой строки */ #define IF_NOWRAP 0x02u /* строка не переносится (кодовый блок / HR / таблица) */ #define IF_BLANK 0x04u /* визуально пустая строка */ #define IF_CODE 0x08u /* тело fenced code-блока (verbatim-режим) */ static uint16_t n_lines = 0; 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; static uint8_t file_pages = 0; static uint8_t file_phys[MAX_PAGES] = {0}; static uint8_t index_blk = 0; static uint8_t index_pages = 0; static uint8_t index_phys[MAX_INDEX_PAGES] = {0}; static uint8_t index_truncated = 0; /* выставляется при исчерпании ёмкости индекса */ static uint32_t cur_seg_off = 0; /* смещение последнего emit_seg (кэш в near-памяти) */ static idx_rec_t cur_rec; /* near-копия последней записанной idx-записи */ static uint8_t cur_page = 0xFF; /* 0xFF = в W3 ещё не отображена ни одна страница */ static uint8_t viewport_x = 0; /* горизонтальный сдвиг для nowrap-строк */ static char filename[64] = {0}; #define HPAN_STEP 8u /* Спиннер в статус-баре (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; /* ================================================================== * Вспомогательные функции * ================================================================== */ /* Печатает строку с указанным атрибутом, начиная с позиции (x,y), * и останавливается на конце строки или правой границе экрана. */ static void put_str_attr(uint8_t x, uint8_t y, const char *s, uint8_t attr) { while (*s && x < SCREEN_W) { wrchar(x++, y, *s++, attr); } } /* Полностью очищает указанную строку экрана символами пробела с attr. */ static void fill_row(uint8_t y, uint8_t attr) { for (uint8_t c = 0; c < SCREEN_W; c++) { wrchar(c, y, ' ', attr); } } /* Продвигает спиннер на один кадр. * Если спиннер выключен — функция ничего не делает. */ static void spinner_tick(void) { if (!spinner_active) return; wrchar(SPINNER_COL, 0, spinner_chars[spinner_phase & 3], ATTR_BAR_SPINNER); spinner_phase++; } /* Включает/выключает спиннер. * При выключении очищает его позицию пробелом в статус-баре. */ static void spinner_show(uint8_t on) { spinner_active = on; if (!on) wrchar(SPINNER_COL, 0, ' ', ATTR_BAR); } /* Отображает указанную EMM-страницу файла в окно W3. * Если эта страница уже активна, переключение не выполняется. */ static inline void map_page(uint8_t page) { if (page != cur_page) { /* file_phys[] позволяет не вызывать mem_get_page() на каждом свопе. */ sprinter_page_w3(file_phys[page]); cur_page = page; } } /* Читает один байт логического файла по смещению p. * При переходе между страницами автоматически переключает W3. * Проверка границ выполняется вызывающей стороной. * * Это горячий путь (вызывается очень часто из index_lines()), поэтому * декодирование page/off сделано через little-endian представление p, * чтобы избежать тяжёлых 32-битных сдвигов/масок на z80. */ static inline char fb(uint32_t p) { uint8_t *pb = (uint8_t *)&p; uint8_t page = (uint8_t)((pb[1] >> 6) | (pb[2] << 2)); uint16_t off = (uint16_t)(((uint16_t)(pb[1] & 0x3F) << 8) | pb[0]); map_page(page); return FILE_BUF[off]; } /* Для проверки границ выделения считаем пробел/таб/конец строки/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)); } /* Тип сегмента-продолжения, передаваемый в emit_seg(). * Явно в индексе не хранится: рендер при необходимости восстанавливает * тип через classify_line(), но параметр оставлен для читаемости вызовов. */ #define CK_PLAIN 0 #define CK_QUOTE 1 #define CK_LIST 2 #define CK_OTHER 3 /* Доступ к индексу в EMM. * Размер записи 8 байт, поэтому запись не пересекает границу страницы * и bank_read/bank_write работают с цельным блоком. */ static void idx_get(uint16_t idx, idx_rec_t *r) { uint8_t page = (uint8_t)(idx >> 11); /* номер страницы: idx / 2048 */ uint16_t off = (uint16_t)((idx & 2047u) << 3); /* смещение записи: (idx % 2048) * 8 */ bank_read(index_phys[page], off, r, INDEX_REC_SIZE); } static void idx_put(uint16_t idx, const idx_rec_t *r) { uint8_t page = (uint8_t)(idx >> 11); uint16_t off = (uint16_t)((idx & 2047u) << 3); bank_write(index_phys[page], off, r, INDEX_REC_SIZE); } /* Чтение полей индекса для рендера и навигации (построчный доступ). */ static uint8_t seg_flags(uint16_t idx) { idx_rec_t r; if (idx >= n_lines) return 0; idx_get(idx, &r); return r.flags; } static 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; } static uint8_t is_cont(uint16_t idx) { return (uint8_t)((seg_flags(idx) & IF_CONT) != 0); } static uint8_t is_nowrap(uint16_t idx) { return (uint8_t)((seg_flags(idx) & IF_NOWRAP) != 0); } 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); } /* Установка флагов всегда идёт в последний 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++) { for(int j = 0; j < 16; j++) { buff[j * 4] = md_pallete[i * 4]; 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]; } text_pal_load(TEXT_PAL_INK, i * 16, 16, md_pallete); text_pal_load(TEXT_PAL_BLINK_INK, i * 16, 16, md_pallete); text_pal_load(TEXT_PAL_PAPER, i * 16, 16, buff); text_pal_load(TEXT_PAL_BLINK_PAPER, i * 16, 8, buff + 32); text_pal_load(TEXT_PAL_BLINK_PAPER, i * 16 + 8, 8, buff + 32); } } /* ================================================================== * Загрузка/выгрузка файла * ================================================================== */ /* Загружает файл в EMM-страницы и подготавливает EMM-индекс. * Возврат: 0 — успех, отрицательное значение — код ошибки. */ static int load_file(const char *path) { int fd = open(path, O_RDONLY); if (fd < 0) return -1; long sz = lseek(fd, 0, SEEK_END); if (sz < 0 || (uint32_t)sz > MAX_FILE) { close(fd); return -2; } file_size = (uint32_t)sz; lseek(fd, 0, SEEK_SET); /* Выделяем ровно столько 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); if (file_blk == 0) { close(fd); return -3; } /* Читаем файл кусками по 16 КБ, последовательно маппя страницы в W3. * На последней итерации читаем остаток меньше PAGE_SIZE. */ uint32_t remaining = file_size; for (uint8_t page = 0; page < file_pages; page++) { spinner_tick(); uint8_t phys = mem_get_page(file_blk, page); file_phys[page] = phys; sprinter_page_w3(phys); cur_page = page; uint16_t chunk = (remaining > PAGE_SIZE) ? (uint16_t)PAGE_SIZE : (uint16_t)remaining; int n = read(fd, FILE_BUF, chunk); if (n != (int)chunk) { close(fd); return -4; } remaining -= chunk; } close(fd); /* Выделяем блок индекса в EMM (8 байт на сегмент, 2048 записей/страница). * Размер берем с запасом относительно файла; если памяти мало — * уменьшаем количество страниц индекса до доступного значения. */ { uint8_t want = (uint8_t)(file_pages + 1u); if (want > MAX_INDEX_PAGES) want = MAX_INDEX_PAGES; index_blk = 0; while (want >= 1u) { index_blk = mem_alloc_pages(want); if (index_blk) break; want--; } if (index_blk == 0) return -5; index_pages = want; for (uint8_t i = 0; i < index_pages; i++) index_phys[i] = mem_get_page(index_blk, i); max_lines = (uint16_t)index_pages * INDEX_RECS_PER_PAGE; } return 0; } /* Освобождает выделенные EMM-блоки файла и индекса. */ static void unload_file(void) { if (file_blk) { mem_free_block(file_blk); file_blk = 0; } if (index_blk) { mem_free_block(index_blk); index_blk = 0; } } /* ================================================================== * Индексация строк * ================================================================== */ /* Возвращает 1, если сегмент idx — это старт логической строки, * и эта строка начинается с ``` (граница блока кода). * Сегменты-продолжения никогда не считаются такими границами. */ static uint8_t is_fence_delim(uint16_t idx) { if (idx >= n_lines) return 0; if (is_cont(idx)) return 0; uint32_t off = seg_off(idx); if (off + 2 >= file_size) return 0; return (uint8_t)(fb(off) == '`' && fb(off + 1) == '`' && fb(off + 2) == '`'); } #define INIT_STYLE_PLAIN 0x0 #define INIT_STYLE_BOLD 0x1 #define INIT_STYLE_ITALIC 0x2 #define INIT_STYLE_UNDER 0x3 #define INIT_STYLE_CODE 0x4 #define INIT_STYLE_STRIKE 0x5 /* Таблица сопоставления init-style -> атрибут. * Порядок должен совпадать с INIT_STYLE_* / EM_* ниже. */ static const uint8_t styles_map[] = { ATTR_TEXT, ATTR_TEXT_BOLD, ATTR_TEXT_ITALIC, ATTR_TEXT_UNDERSORE, ATTR_TEXT_CODE, ATTR_TEXT_STRIKE }; /* Возвращает стартовый атрибут сегмента из индексной записи. */ static uint8_t get_init_style(uint16_t idx) { idx_rec_t r; if (idx >= n_lines) return styles_map[INIT_STYLE_PLAIN]; idx_get(idx, &r); return styles_map[r.style & 7u]; } /* Возвращает «сырой» INIT_STYLE_* из индексной записи. */ static uint8_t get_init_style_raw(uint16_t idx) { idx_rec_t r; if (idx >= n_lines) return INIT_STYLE_PLAIN; idx_get(idx, &r); return (uint8_t)(r.style & 7u); } /* Тип строки, который возвращает 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 спец-префикса не имеют */ } } /* Добавляет запись сегмента в 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; /* тип сегмента восстанавливается на этапе рендера */ 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++; } /* Проверяет, что физическая строка пуста (только пробелы/таб + перевод строки). */ 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; } /* Быстрая проверка начала блока кода по маркеру ``` . */ 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); 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-форматирования и переносов. * Идёт от 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) { uint32_t last_space = 0xFFFFFFFFu; char prev_ch = ' '; uint8_t seg_col = col; 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); last_space = 0xFFFFFFFFu; prev_ch = ' '; seg_col = col; continue; } if (ch == '`') { if (line_style == INIT_STYLE_CODE) line_style = INIT_STYLE_PLAIN; else line_style = INIT_STYLE_CODE; q++; continue; } if (ch == '*' && (q + 1) < q_end && fb(q + 1) == '*') { char next_ch = ((q + 2) < q_end) ? fb(q + 2) : '\n'; if (is_emph_flanked(prev_ch, next_ch)) { if (line_style == INIT_STYLE_PLAIN && ws_or_eol(prev_ch)) { line_style = INIT_STYLE_BOLD; q += 2; continue; } else if (line_style == INIT_STYLE_BOLD && ws_or_eol_or_delim(next_ch)) { line_style = INIT_STYLE_PLAIN; q += 2; continue; } } seg_col += 2; q += 2; prev_ch = '*'; continue; } if (ch == '~' && (q + 1) < q_end && fb(q + 1) == '~') { char next_ch = ((q + 2) < q_end) ? fb(q + 2) : '\n'; if (is_emph_flanked(prev_ch, next_ch)) { if (line_style == INIT_STYLE_PLAIN && ws_or_eol(prev_ch)) { line_style = INIT_STYLE_STRIKE; q += 2; continue; } else if (line_style == INIT_STYLE_STRIKE && ws_or_eol_or_delim(next_ch)) { line_style = INIT_STYLE_PLAIN; q += 2; continue; } } seg_col += 2; q += 2; prev_ch = '~'; continue; } if (ch == '*' || ch == '_') { char next_ch = ((q + 1) < q_end) ? fb(q + 1) : '\n'; if (is_emph_flanked(prev_ch, next_ch)) { uint8_t new_style = (ch == '*') ? INIT_STYLE_ITALIC : INIT_STYLE_UNDER; if (line_style == INIT_STYLE_PLAIN && ws_or_eol(prev_ch)) { line_style = new_style; q++; continue; } else if (line_style == new_style && ws_or_eol_or_delim(next_ch)) { line_style = INIT_STYLE_PLAIN; q++; continue; } } seg_col++; q++; prev_ch = ch; continue; } if (ch == '\t') { uint8_t tgt = (uint8_t)((seg_col & (uint8_t)~(TAB_STOP - 1)) + TAB_STOP); if (tgt > SCREEN_W) tgt = SCREEN_W; seg_col = tgt; q++; prev_ch = ' '; continue; } if (ch == ' ') { last_space = q + 1; seg_col++; q++; prev_ch = ' '; } else { seg_col++; q++; prev_ch = ch; } if (seg_col >= SCREEN_W) { uint32_t wrap_at = (last_space != 0xFFFFFFFFu && last_space > cur_seg_off) ? last_space : q; if (wrap_at >= q_end || wrap_at == cur_seg_off) break; emit_seg(wrap_at, line_style, ckind, 1); seg_col = col; last_space = 0xFFFFFFFFu; prev_ch = ' '; q = wrap_at; } } 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; 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++; soft_break = 1; ch = ' '; } } if (ch == '`') { if (line_style == INIT_STYLE_CODE) line_style = INIT_STYLE_PLAIN; else line_style = INIT_STYLE_CODE; if (!soft_break) q++; continue; } if (ch == '*' && (q + 1) < file_size && fb(q + 1) == '*') { char next_ch = ((q + 2) < file_size) ? fb(q + 2) : '\n'; if (is_emph_flanked(prev_ch, next_ch)) { if (line_style == INIT_STYLE_PLAIN && ws_or_eol(prev_ch)) { line_style = INIT_STYLE_BOLD; if (!soft_break) q += 2; else q++; continue; } else if (line_style == INIT_STYLE_BOLD && ws_or_eol_or_delim(next_ch)) { line_style = INIT_STYLE_PLAIN; if (!soft_break) q += 2; else q++; continue; } } seg_col += 2; if (!soft_break) q += 2; else q++; prev_ch = '*'; continue; } if (ch == '~' && (q + 1) < file_size && fb(q + 1) == '~') { char next_ch = ((q + 2) < file_size) ? fb(q + 2) : '\n'; if (is_emph_flanked(prev_ch, next_ch)) { if (line_style == INIT_STYLE_PLAIN && ws_or_eol(prev_ch)) { line_style = INIT_STYLE_STRIKE; if (!soft_break) q += 2; else q++; continue; } else if (line_style == INIT_STYLE_STRIKE && ws_or_eol_or_delim(next_ch)) { line_style = INIT_STYLE_PLAIN; if (!soft_break) q += 2; else q++; continue; } } seg_col += 2; if (!soft_break) q += 2; else q++; prev_ch = '~'; continue; } if (ch == '*' || ch == '_') { char next_ch = ((q + 1) < file_size) ? fb(q + 1) : '\n'; if (is_emph_flanked(prev_ch, next_ch)) { uint8_t new_style = (ch == '*') ? INIT_STYLE_ITALIC : INIT_STYLE_UNDER; if (line_style == INIT_STYLE_PLAIN && ws_or_eol(prev_ch)) { line_style = new_style; if (!soft_break) q++; else q++; continue; } else if (line_style == new_style && ws_or_eol_or_delim(next_ch)) { line_style = INIT_STYLE_PLAIN; if (!soft_break) q++; else q++; continue; } } seg_col++; if (!soft_break) q++; else q++; prev_ch = ch; continue; } if (ch == '\t') { uint8_t tgt = (uint8_t)((seg_col & (uint8_t)~(TAB_STOP - 1)) + TAB_STOP); if (tgt > SCREEN_W) tgt = SCREEN_W; seg_col = tgt; if (!soft_break) q++; else q++; prev_ch = ' '; continue; } if (ch == ' ') { if (soft_break) { last_space = q; seg_col++; } else { last_space = q + 1; seg_col++; q++; } prev_ch = ' '; } else { seg_col++; if (!soft_break) q++; else q++; prev_ch = ch; } if (seg_col >= SCREEN_W) { uint32_t wrap_at = (last_space != 0xFFFFFFFFu && last_space > cur_seg_off) ? last_space : q; if (wrap_at >= file_size || wrap_at == cur_seg_off) break; emit_seg(wrap_at, line_style, ckind, 1); seg_col = col; last_space = 0xFFFFFFFFu; prev_ch = ' '; q = wrap_at; } } *io_line_style = line_style; return q; } /* Строит индекс сегментов рендера по загруженному файлу. */ static void index_lines(void) { uint8_t in_block = 0; n_lines = 0; index_truncated = 0; cur_seg_off = 0xFFFFFFFFu; /* пока не эмитили ни одного сегмента */ if (file_size == 0) return; uint32_t p = 0; while (p < file_size && n_lines < max_lines) { 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(); 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(); while (p < file_size && fb(p) != '\n') p++; if (p < file_size) p++; continue; } /* ---- строка таблицы (| ... |): один nowrap-сегмент на исходную строку ---- */ if (is_table_raw(p)) { 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; } /* ---- классификация текущей строки ---- */ uint32_t content_off = p; uint8_t kind = classify_line(p, &content_off); /* ---- Заголовок: отдельная строка, не склеивается с параграфом ---- */ if (kind >= LK_H1 && kind <= LK_H4) { 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); 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); 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) { 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); 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; emit_seg(p, line_style, CK_PLAIN, 0); uint32_t q = p; q = scan_join_stream(q, p, 0, 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; } } } } #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; } } /* Единая обработка inline-маркеров в render_line(). * Возврат: * 0 = маркер не поглощён (символ выводится как есть), * 1 = маркер поглощён как управляющий (ничего не рисуем), * 2 = литеральный двойной маркер (`**` или `~~`), нужно вывести две копии out_ch. */ static uint8_t handle_inline_marker(char ch, uint32_t *io_p, uint32_t seg_end, char prev_ch, uint8_t *io_emph, char *out_ch) { uint32_t p = *io_p; *out_ch = 0; if (ch == '`') { if (*io_emph == EM_NONE) { *io_emph = EM_CODE; *io_p = p + 1; return 1; } if (*io_emph == EM_CODE) { *io_emph = EM_NONE; *io_p = p + 1; return 1; } return 0; } if ((ch == '*' || ch == '~') && (p + 1) < seg_end && fb(p + 1) == ch) { uint8_t pair_emph = (ch == '*') ? EM_BOLD : EM_STRIKE; char next_ch = ((p + 2) < seg_end) ? fb(p + 2) : '\n'; if (is_emph_flanked(prev_ch, next_ch)) { if (*io_emph == EM_NONE && ws_or_eol(prev_ch)) { *io_emph = pair_emph; *io_p = p + 2; return 1; } else if (*io_emph == pair_emph && ws_or_eol_or_delim(next_ch)) { *io_emph = EM_NONE; *io_p = p + 2; return 1; } } /* Невалидный по границам двойной маркер отображаем как литерал. */ *io_p = p + 2; *out_ch = ch; return 2; } if (ch == '*' || ch == '_') { char next_ch = ((p + 1) < seg_end) ? fb(p + 1) : '\n'; if (is_emph_flanked(prev_ch, next_ch)) { uint8_t active = (ch == '*') ? EM_ITALIC : EM_UNDER; if (*io_emph == EM_NONE && ws_or_eol(prev_ch)) { *io_emph = active; *io_p = p + 1; return 1; } else if (*io_emph != EM_NONE && ws_or_eol_or_delim(next_ch)) { *io_emph = EM_NONE; *io_p = p + 1; return 1; } } } return 0; } 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); uint8_t emph = is_code_body(line_idx) ? EM_CODE : get_init_style_raw(line_idx); uint8_t parse_inline = 1; /* 1: включён парсер выделений, 0: выводим код как есть */ if (is_blank(line_idx)) { while (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT); return; } if (line_idx < n_lines) { uint32_t p = seg_off(line_idx); uint32_t content_off = p; uint8_t cont = is_cont(line_idx); /* Правая граница сегмента: начало следующего сегмента или конец файла. */ 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: текущий сегмент относится к потоку цитаты */ /* Для сегмента-продолжения повторяем префикс/отступ * от первого не-CONT сегмента логической строки. */ if (cont) { uint16_t first = line_idx; /* Для любого сегмента-продолжения откатываемся к первому * НЕ-cont сегменту этой логической строки. */ while (first > 0 && is_cont(first)) first--; uint32_t p_start = seg_off(first); uint32_t content_off = p_start; 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) { /* Для продолжения цитаты нужно помнить тип потока, * чтобы далее на newline корректно пропускать сырой `>`. */ quote_stream = 1; uint8_t q = 0; while (q + 2 < content_off && col < SCREEN_W && fb(p_start + q) == ' ') { wrchar(col++, row, ' ', ATTR_TEXT); q++; } if (col < SCREEN_W) wrchar(col++, row, 0xB3, ATTR_QUOTE_MARKER); if (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT); } else if (first_kind == LK_ULIST || first_kind == LK_OLIST) { uint8_t q = 0; while (q < indent && col < SCREEN_W) { wrchar(col++, row, ' ', ATTR_TEXT); q++; } } /* Дальше рендерим контент именно с текущего сегмента-продолжения. */ p = seg_off(line_idx); content_off = p; /* Для сегмента-продолжения парсер работает как обычно. */ } /* Кодовый блок: * - строка-разделитель показывается как пустая; * - тело рисуется «как есть» с ATTR_TEXT_CODE без inline-парсинга. */ if (is_fence_delim(line_idx)) { while (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT); return; } if (is_code_body(line_idx)) { base_attr = ATTR_TEXT_CODE; cur_attr = ATTR_TEXT_CODE; parse_inline = 0; kind = LK_PLAIN; quote_stream = 0; } else if (cont) { /* Строка-продолжение: префикс уже нарисован выше. * Содержимое рисуем как plain, без повторной классификации, * иначе начало слова могло бы ложно стать маркером. */ kind = LK_PLAIN; } else { kind = classify_line(p, &content_off); if (kind == LK_QUOTE) quote_stream = 1; } if (kind == LK_HR) { while (col < SCREEN_W) wrchar(col++, row, 0xC4, ATTR_HR); return; } if (kind >= LK_H1 && kind <= LK_H4) { switch (kind) { case LK_H1: base_attr = ATTR_TEXT_TITLE1; break; case LK_H2: base_attr = ATTR_TEXT_TITLE2; break; case LK_H3: base_attr = ATTR_TEXT_TITLE3; break; default: base_attr = ATTR_TEXT_TITLE4; break; } cur_attr = base_attr; p = content_off; } else if (kind == LK_ULIST) { /* Базовая поддержка вложенности: ведущие пробелы сохраняем, * затем рисуем маркер списка и пробел перед содержимым. */ uint32_t q = p; 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, ' ', ATTR_TEXT); p = content_off; } else if (kind == LK_OLIST) { /* Отступ -> пробелы; цифры и '.' / ')' рисуем как маркер; * завершающий пробел — обычным атрибутом текста. */ uint32_t q = p; while (q < content_off && col < SCREEN_W && fb(q) == ' ') { wrchar(col++, row, ' ', ATTR_TEXT); q++; } while (q < content_off - 1 && col < SCREEN_W) { wrchar(col++, row, fb(q), ATTR_LIST_MARKER); q++; } if (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT); p = content_off; } else if (kind == LK_QUOTE) { /* Отступ -> пробелы; затем quote-префикс '│' (0xB3), * пробел и далее содержимое. */ uint32_t q = p; while (q + 1 < content_off && col < SCREEN_W && fb(q) == ' ') { wrchar(col++, row, ' ', ATTR_TEXT); q++; } if (col < SCREEN_W) wrchar(col++, row, 0xB3, ATTR_QUOTE_MARKER); if (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT); p = content_off; } /* Эффективный горизонтальный сдвиг: применяется только к nowrap-строкам. */ uint8_t effective_vx = is_nowrap(line_idx) ? viewport_x : 0; /* cc — видимая колонка внутри области контента * (после фиксированного префикса строки). */ uint8_t cc = 0; char prev_ch = ' '; /* используется в проверке границ emphasis */ while (p < seg_end && col < SCREEN_W) { uint8_t soft_join = 0; /* 1: вставлен виртуальный пробел мягкой склейки, p уже сдвинут */ char ch = fb(p); /* '\' * перед концом сегмента — это маркер 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; } } /* Мягкий перенос (перевод строки внутри одного сегмента). * Рендер должен повторить логику индексатора: * - quote: пропустить сырой `> ` следующей строки; * - list/plain: схлопнуть перенос + ведущий отступ в 1 пробел. */ if (parse_inline && (ch == '\n' || ch == '\r')) { uint32_t next = p + 1; if (quote_stream && next < seg_end) { uint32_t next_content = next; if (classify_line(next, &next_content) == LK_QUOTE) { /* Мягкая склейка 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-маркеры (**, ~~, *, _, `): * helper синхронизирует состояние emphasis и возвращает режим * обработки текущего токена (поглотить/вывести литерал/обычный символ). */ if (parse_inline) { char lit_ch = 0; uint8_t mk = handle_inline_marker(ch, &p, seg_end, prev_ch, &emph, &lit_ch); if (mk == 1) { cur_attr = emph_to_attr(emph, base_attr); continue; } if (mk == 2) { if (cc >= effective_vx && col < SCREEN_W) { wrchar(col++, row, lit_ch, cur_attr); } cc++; if (cc >= effective_vx && col < SCREEN_W) { wrchar(col++, row, lit_ch, cur_attr); } cc++; prev_ch = lit_ch; continue; } } if (!soft_join) p++; if (ch == '\t') { /* Таб расширяется в координатах контента (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); cc++; } prev_ch = ' '; } else { if (cc >= effective_vx && col < SCREEN_W) { wrchar(col++, row, ch, cur_attr); } cc++; prev_ch = ch; } } /* Индикатор обрезки справа для nowrap-строки: * если после текущей позиции есть ещё видимый контент, * поверх последней колонки рисуем '>'. */ if (is_nowrap(line_idx) && col >= SCREEN_W) { uint32_t pp = p; uint32_t left_bound = seg_off(line_idx); while (pp < seg_end) { char c = fb(pp); if (c == '\n' || c == '\r') break; if (c == '`') { pp++; continue; } if (c == '*' && (pp + 1) < seg_end && fb(pp + 1) == '*') { 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; } /* Литерал `**` тоже считается видимым контентом справа. */ } else if (c == '*' || c == '_') { char pc = (pp > left_bound) ? fb(pp - 1) : ' '; char nc = (pp + 1 < seg_end) ? fb(pp + 1) : '\n'; if (is_emph_flanked(pc, nc)) { pp++; continue; } } wrchar(SCREEN_W - 1, row, '>', ATTR_TRUNC); break; } } /* Индикатор скрытого контента слева при горизонтальном сдвиге. */ if (is_nowrap(line_idx) && viewport_x > 0) { wrchar(0, row, '<', ATTR_TRUNC); } } /* Дозаполняем остаток строки пробелами, чтобы стереть хвост * от предыдущего кадра рендера. */ { 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++) { render_line((uint16_t)(top_line + i), (uint8_t)(VIEW_TOP_ROW + i)); } } /* Возвращает процент прокрутки (0..100) относительно доступного диапазона. */ static uint8_t calc_pct(void) { uint8_t pct; if (n_lines <= VIEW_H) pct = 100; else pct = (uint8_t)((uint32_t)top_line * 100UL / (uint32_t)(n_lines - VIEW_H)); return pct; } /* Рисует динамическую часть статус-бара без printf. * Полностью перезаписывает колонки 45..79, чтобы не оставлять «хвосты». */ static void render_status_numbers(void) { uint16_t last = top_line + VIEW_H; if (last > n_lines) last = n_lines; uint8_t pct = calc_pct(); textattr(ATTR_BAR); for (uint8_t c = 45; c < SCREEN_W; c++) wrchar(c, 0, ' ', ATTR_BAR); wrchar(45, 0, 0xB3, ATTR_BAR); /* вертикальный разделитель */ gotoxy(47, 0); cputs("L "); dec16(top_line + 1); cputs("-"); dec16(last); cputs(" / "); dec16(n_lines); wrchar(70, 0, 0xB3, ATTR_BAR); /* вертикальный разделитель */ gotoxy( 73, 0); dec8(pct); cputs("%"); } /* Полностью перерисовывает статус-бар (заголовок + числа). */ static void render_full_status(void) { fill_row(0, ATTR_BAR); /* Размещение в шапке: "MDVIEW", затем слот спиннера и имя файла. */ put_str_attr(1, 0, "MDVIEW", ATTR_BAR); put_str_attr(10, 0, filename, ATTR_BAR); 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 зарезервирован: режимов 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); } /* ================================================================== * Прокрутка * ================================================================== */ /* Ограничивает top_line допустимым диапазоном с учётом высоты окна. */ static void clamp_top(void) { if (n_lines <= VIEW_H) { top_line = 0; } else if (top_line > n_lines - VIEW_H) { top_line = (uint16_t)(n_lines - VIEW_H); } } /* Прокрутка вверх на n строк с частичной перерисовкой при n == 1. */ static void scroll_up(uint16_t n) { uint16_t new_top_line = (top_line >= n) ? (uint16_t)(top_line - n) : 0; if( new_top_line != top_line) { top_line = new_top_line; if (n == 1) { scroll(0, VIEW_TOP_ROW, SCREEN_W, VIEW_H, 2, 0); render_line((uint16_t)(top_line), (uint8_t)(VIEW_TOP_ROW)); } else { render_viewport(); } } } /* Прокрутка вниз на n строк с частичной перерисовкой при n == 1. */ static void scroll_down(uint16_t n) { uint16_t new_top_line = (top_line + n < n_lines - VIEW_H) ? (uint16_t)(top_line + n) : n_lines - VIEW_H; if( new_top_line != top_line) { top_line = new_top_line; if (n == 1) { scroll(0, VIEW_TOP_ROW, SCREEN_W, VIEW_H, 1, 0); render_line((uint16_t)(top_line + VIEW_H - 1), (uint8_t)(VIEW_TOP_ROW + VIEW_H - 1)); } else { clamp_top(); render_viewport(); } } } /* Горизонтальный сдвиг (только если в окне есть nowrap-строки). * Максимум сдвига ограничен самой широкой nowrap-строкой на экране. */ static void scroll_h(int8_t delta) { uint16_t maxw = 0; for (uint8_t i = 0; i < VIEW_H; i++) { uint16_t li = (uint16_t)(top_line + i); if (li >= n_lines) break; if (!is_nowrap(li)) continue; 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; /* приблизительная видимая ширина */ if (w > maxw) maxw = w; } if (maxw == 0) return; /* в окне нет nowrap-контента */ /* Максимальный сдвиг = ширина за пределами экрана, в границах 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; if ((uint8_t)nx != viewport_x) { viewport_x = (uint8_t)nx; render_viewport(); } } /* ================================================================== * Вывод ошибки * ================================================================== */ /* Печатает сообщение об ошибке и ждёт нажатия клавиши. */ static void die(const char *msg) { clrscr_attr(ATTR_TEXT); puts(msg); puts("Press any key to exit..."); (void)getkey(); } /* ================================================================== * Точка входа * ================================================================== */ int main(int argc, char **argv) { const char *path; if (argc >= 2) path = argv[1]; else path = "SAMPLE.MD"; /* Копируем путь в static filename[] для отображения в статус-баре. */ { uint8_t i = 0; while (i < (uint8_t)(sizeof(filename) - 1) && path[i]) { filename[i] = path[i]; i++; } filename[i] = 0; } /* Сначала поднимаем UI, чтобы пользователь видел шапку/меню/спиннер * уже во время загрузки файла и построения индекса. */ set_videotextmode(TEXT_MODE_80x32); clrscr_attr(ATTR_TEXT); set_pallete(); render_menu(); render_full_status(); spinner_show(1); int rc = load_file(path); if (rc < 0) { spinner_show(0); clrscr_attr(ATTR_RESET); textattr(ATTR_RESET); cputs("MDView: cannot load file '"); cputs(path); cputs("'\n\rError: "); switch (rc) { case -1: cputs("open failed"); break; case -2: cputs("size > 128K or seek failed"); break; case -3: cputs("mem_alloc_pages failed"); break; case -4: cputs("short read"); break; case -5: cputs("index alloc failed"); break; } cputs("\n\r\n\r"); return 1; } index_lines(); spinner_show(0); if (n_lines == 0) { die("mdview: empty file"); unload_file(); return 1; } render_full_status(); render_viewport(); for (;;) { uint16_t k = getkey(); uint8_t ascii = (uint8_t)(k & 0xFF); uint8_t scan = (uint8_t)((k >> 8) & 0x7F); if (ascii) { if (ascii == 0x1B) break; /* клавиша Esc */ continue; } switch (scan) { case KEY_F10: goto exit_loop; case KEY_UP: scroll_up(1); break; case KEY_DOWN: scroll_down(1); break; case KEY_LEFT: scroll_h(-(int8_t)HPAN_STEP); break; case KEY_RIGHT: scroll_h(+(int8_t)HPAN_STEP); break; case KEY_PGUP: scroll_up(VIEW_H); break; case KEY_PGDN: scroll_down(VIEW_H); break; case KEY_HOME: top_line = 0; viewport_x = 0; render_viewport(); break; case KEY_END: top_line = (n_lines > VIEW_H) ? (uint16_t)(n_lines - VIEW_H) : 0; viewport_x = 0; render_viewport(); break; default: continue; } render_updated_status(); } exit_loop: unload_file(); pal_reset(PAL_CGA); clrscr_attr(ATTR_TEXT); return 0; }