1b78dda125
- mdview2_status.c: вынести статус-бар/меню/спиннер из ядра - mdview2.c: убрать retry-цикл в alloc_set_storage (fail-fast вместо ложной устойчивости — при нехватке EMM под индекс контент тоже не влезет) - mdview2.h: дополнить экспортами статус-модуля - mdview2_md.c / mdview2_raw.c: зачистка после расщепления - mdview/mdview.c: переименовать scroll_* → md_scroll_* (симметрия) - docs/fast_ram.md, docs/turboc.txt: добавить справочные доки - examples/mdview2/README.MD, READMEBG.MD: обновить описание Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2025 lines
88 KiB
C
2025 lines
88 KiB
C
/*
|
||
* 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 <stdint.h>
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
#include <conio.h>
|
||
#include <bios/text.h>
|
||
#include <fcntl.h>
|
||
#include <unistd.h>
|
||
#include <sprinter.h>
|
||
#include <sprinter_mem.h>
|
||
#include <palette.h>
|
||
|
||
/* ---- Геометрия экрана -------------------------------------------- */
|
||
|
||
#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)
|
||
|
||
/* ---- Атрибуты диалога справки ------------------------------------ */
|
||
|
||
#define ATTR_HELP_BG COLOR(COLOR_LIGHTGRAY, COLOR_BLACK)
|
||
#define ATTR_HELP_BDR COLOR(COLOR_WHITE, COLOR_BLACK)
|
||
#define ATTR_HELP_TIT COLOR(COLOR_YELLOW, COLOR_BLACK)
|
||
#define ATTR_HELP_HDR COLOR(COLOR_WHITE, COLOR_BLACK)
|
||
#define ATTR_HELP_HINT COLOR(COLOR_YELLOW, COLOR_BLACK)
|
||
|
||
/* Геометрия диалога справки (в символьных координатах 80×32). */
|
||
#define HELP_X 8u /* левая граница рамки */
|
||
#define HELP_Y 4u /* верхняя граница рамки */
|
||
#define HELP_W 64u /* ширина рамки (включая │) */
|
||
#define HELP_H 22u /* высота рамки (включая ─) */
|
||
|
||
/* ---- Глобальное состояние ---------------------------------------- */
|
||
|
||
/* Файл до 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),
|
||
* и останавливается на конце строки или правой границе экрана.
|
||
* `s` — литерал/статический буфер (CODE/DATA, окна 1/2), поэтому его
|
||
* можно передать в BIOS LP_PRINT_LN напрямую, без промежуточной копии. */
|
||
static void put_str_attr(uint8_t x, uint8_t y, const char *s, uint8_t attr)
|
||
{
|
||
uint8_t maxlen = (x < SCREEN_W) ? (uint8_t)(SCREEN_W - x) : 0;
|
||
uint8_t len = (uint8_t)strlen(s);
|
||
if (len > maxlen) len = maxlen;
|
||
if (len == 0) return;
|
||
bios_set_place(y, x);
|
||
bios_writeattr(s, len, attr);
|
||
}
|
||
|
||
|
||
/* Полностью очищает указанную строку экрана символами пробела с attr. */
|
||
static void fill_row(uint8_t y, uint8_t attr)
|
||
{
|
||
bios_set_place(y, 0);
|
||
bios_fillcharattr(' ', attr, SCREEN_W);
|
||
}
|
||
|
||
/* Продвигает спиннер на один кадр.
|
||
* Если спиннер выключен — функция ничего не делает. */
|
||
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.
|
||
* Проверка границ выполняется вызывающей стороной.
|
||
*/
|
||
|
||
static 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];
|
||
/*
|
||
// p = HLDE
|
||
(void)p;
|
||
|
||
__asm
|
||
; uint16_t off = (uint16_t)(((uint16_t)(pb[1] | 0xC0) << 8) | pb[0]);
|
||
ld a, d
|
||
or a, #0xC0
|
||
ld b, a
|
||
ld c, e
|
||
|
||
; uint8_t page = (uint8_t)((pb[1] >> 6) | (pb[2] << 2));
|
||
ld a, l ;; pb[2] << 2
|
||
add a, a
|
||
add a, a
|
||
ld e, a ;; e = pb[2] << 2
|
||
|
||
ld a, d ;; pb[1] >> 6
|
||
rlca
|
||
rlca
|
||
and a, #0x03
|
||
|
||
or a, e ;; a = (pb[2] << 2) | (pb[1] >> 6)
|
||
ld e, a
|
||
; map_page(page);
|
||
; if (page != cur_page) {
|
||
ld a, (_cur_page)
|
||
cp e
|
||
jr Z, __fbl0
|
||
; cur_page = page;
|
||
ld a, e
|
||
ld (_cur_page), a
|
||
; sprinter_page_w3(file_phys[page]);
|
||
ld hl, #_file_phys
|
||
ld d, #0x00
|
||
add hl, de
|
||
ld a, (hl)
|
||
out (__io_page_w3), a
|
||
; }
|
||
__fbl0:
|
||
; return FILE_BUF[off];
|
||
ld a, (bc)
|
||
ret
|
||
__endasm;
|
||
*/
|
||
}
|
||
|
||
/* Для проверки границ выделения считаем пробел/таб/конец строки/0 пробельными. */
|
||
static uint8_t ws_or_eol(char c)
|
||
{
|
||
return (uint8_t)(c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == 0);
|
||
}
|
||
|
||
static uint8_t ws_or_eol_or_delim(char c)
|
||
{
|
||
return (uint8_t)(c == ' ' || c == ',' || c == '.' || c == '!' || c == '?' || c == ':' || c == ';' || c == '/'
|
||
|| c == '\t' || c == '\n' || c == '\r' || c == 0);
|
||
}
|
||
|
||
/* Маркер inline-выделения (*, **, _) распознаётся только если
|
||
* пробельная граница есть ровно с одной стороны (XOR-правило).
|
||
* Это защищает выражения вроде "2 * 3", "2 ** 3" и внутрисловные
|
||
* случаи (например COLOR_YELLOW) от ложного форматирования. */
|
||
static uint8_t is_emph_flanked(char prev_ch, char next_ch)
|
||
{
|
||
return (uint8_t)(ws_or_eol(prev_ch) != ws_or_eol_or_delim(next_ch));
|
||
}
|
||
|
||
/* Тип сегмента-продолжения, передаваемый в 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;
|
||
uint8_t last_space_style = line_style; /* снимок line_style на момент last_space */
|
||
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;
|
||
last_space_style = line_style;
|
||
seg_col++; q++; prev_ch = ' ';
|
||
} else {
|
||
seg_col++; q++; prev_ch = ch;
|
||
}
|
||
if (seg_col >= SCREEN_W) {
|
||
uint8_t use_ls = (uint8_t)(last_space != 0xFFFFFFFFu && last_space > cur_seg_off);
|
||
uint32_t wrap_at = use_ls ? last_space : q;
|
||
if (wrap_at >= q_end || wrap_at == cur_seg_off) break;
|
||
uint8_t wrap_style = use_ls ? last_space_style : line_style;
|
||
emit_seg(wrap_at, wrap_style, ckind, 1);
|
||
seg_col = col; last_space = 0xFFFFFFFFu; prev_ch = ' '; q = wrap_at;
|
||
/* Откатываем line_style к снимку на момент wrap_at: диапазон
|
||
* [wrap_at, q_old) будет пересканирован заново, и переключающие
|
||
* символы (обратная кавычка, звёздочка, подчёркивание, тильда)
|
||
* внутри него иначе применились бы дважды — именно так возникал
|
||
* баг: code-маркер на границе переноса ломает цвет следующего слова. */
|
||
line_style = wrap_style;
|
||
}
|
||
}
|
||
return line_style;
|
||
}
|
||
|
||
#define JOIN_MODE_LIST 1u
|
||
#define JOIN_MODE_QUOTE 2u
|
||
#define JOIN_MODE_PLAIN 3u
|
||
|
||
/* Общий потоковый сканер для index_lines().
|
||
* Убирает дублирование между ветками list/quote/plain:
|
||
* - mode задаёт правила обработки перевода строки,
|
||
* - остальная inline-логика (emphasis/таб/перенос) едина.
|
||
* Возвращает позицию q, на которой сканер остановился, и обновляет стиль. */
|
||
static uint32_t scan_join_stream(uint32_t q, uint32_t para_start, uint8_t col,
|
||
uint8_t ckind, uint8_t *io_line_style, uint8_t mode)
|
||
{
|
||
uint32_t last_space = 0xFFFFFFFFu;
|
||
uint8_t last_space_style = *io_line_style; /* снимок line_style на момент last_space */
|
||
char prev_ch = ' ';
|
||
uint8_t seg_col = col;
|
||
uint8_t line_style = *io_line_style;
|
||
|
||
while (q < file_size && n_lines < max_lines) {
|
||
char ch = fb(q);
|
||
uint8_t soft_break = 0;
|
||
|
||
if (ch == '\n') {
|
||
uint32_t next = q + 1;
|
||
if (next >= file_size) break;
|
||
|
||
if (mode == JOIN_MODE_LIST) {
|
||
if (is_line_blank(next)) break;
|
||
if (is_fence_raw(next) || is_hr_raw(next) || is_table_raw(next)) break;
|
||
|
||
{
|
||
uint32_t next_content = next;
|
||
uint8_t next_kind = classify_line(next, &next_content);
|
||
if (next_kind == LK_ULIST || next_kind == LK_OLIST ||
|
||
next_kind == LK_QUOTE ||
|
||
(next_kind >= LK_H1 && next_kind <= LK_H4) ||
|
||
next_kind == LK_HR) {
|
||
break;
|
||
}
|
||
}
|
||
while (next < file_size) {
|
||
char ws = fb(next);
|
||
if (ws == ' ' || ws == '\t') next++;
|
||
else break;
|
||
}
|
||
q = next;
|
||
soft_break = 1;
|
||
ch = ' ';
|
||
} else if (mode == JOIN_MODE_QUOTE) {
|
||
if (is_line_blank(next)) break;
|
||
if (is_fence_raw(next) || is_hr_raw(next) || is_table_raw(next)) break;
|
||
|
||
{
|
||
uint32_t next_content = next;
|
||
uint8_t next_kind = classify_line(next, &next_content);
|
||
if (next_kind != LK_QUOTE) break;
|
||
|
||
{
|
||
uint32_t next_line_end = next_content;
|
||
while (next_line_end < file_size && fb(next_line_end) != '\n') next_line_end++;
|
||
|
||
{
|
||
uint8_t empty_quote = 1;
|
||
for (uint32_t z = next_content; z < next_line_end; z++) {
|
||
char c = fb(z);
|
||
if (c != ' ' && c != '\t') { empty_quote = 0; break; }
|
||
}
|
||
if (empty_quote) break;
|
||
}
|
||
|
||
{
|
||
uint32_t t = next_content;
|
||
while (t < next_line_end && (fb(t) == ' ' || fb(t) == '\t')) t++;
|
||
if (t < next_line_end && fb(t) == '>') break;
|
||
}
|
||
|
||
while (next_content < next_line_end) {
|
||
char ws = fb(next_content);
|
||
if (ws == ' ' || ws == '\t') next_content++;
|
||
else break;
|
||
}
|
||
if (next_content < next_line_end && fb(next_content) == '>') {
|
||
next_content++;
|
||
if (next_content < next_line_end && fb(next_content) == ' ') next_content++;
|
||
}
|
||
q = next_content;
|
||
soft_break = 1;
|
||
ch = ' ';
|
||
}
|
||
}
|
||
} else { /* JOIN_MODE_PLAIN */
|
||
if (fb(next) == '\n') break;
|
||
if (is_fence_raw(next) || is_hr_raw(next) || is_table_raw(next)) break;
|
||
{
|
||
uint32_t dummy;
|
||
if (classify_line(next, &dummy) != LK_PLAIN) break;
|
||
}
|
||
if (is_line_blank(next)) break;
|
||
|
||
{
|
||
uint8_t is_hard = 0;
|
||
if (q >= para_start + 1) {
|
||
char b1 = fb(q - 1);
|
||
if (b1 == '\\') {
|
||
is_hard = 1;
|
||
} else if (q >= para_start + 2) {
|
||
char b2 = fb(q - 2);
|
||
if ((b1 == ' ' || b1 == '\t') && (b2 == ' ' || b2 == '\t'))
|
||
is_hard = 1;
|
||
}
|
||
}
|
||
if (is_hard) {
|
||
q++;
|
||
emit_seg(q, line_style, ckind, 0);
|
||
last_space = 0xFFFFFFFFu;
|
||
prev_ch = ' ';
|
||
seg_col = col;
|
||
continue;
|
||
}
|
||
}
|
||
|
||
q++;
|
||
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;
|
||
last_space_style = line_style;
|
||
seg_col++;
|
||
} else {
|
||
last_space = q + 1;
|
||
last_space_style = line_style;
|
||
seg_col++;
|
||
q++;
|
||
}
|
||
prev_ch = ' ';
|
||
} else {
|
||
seg_col++;
|
||
if (!soft_break) q++; else q++;
|
||
prev_ch = ch;
|
||
}
|
||
if (seg_col >= SCREEN_W) {
|
||
uint8_t use_ls = (uint8_t)(last_space != 0xFFFFFFFFu && last_space > cur_seg_off);
|
||
uint32_t wrap_at = use_ls ? last_space : q;
|
||
if (wrap_at >= file_size || wrap_at == cur_seg_off) break;
|
||
uint8_t wrap_style = use_ls ? last_space_style : line_style;
|
||
emit_seg(wrap_at, wrap_style, ckind, 1);
|
||
seg_col = col;
|
||
last_space = 0xFFFFFFFFu;
|
||
prev_ch = ' ';
|
||
q = wrap_at;
|
||
/* Откатываем line_style к снимку на момент wrap_at: диапазон
|
||
* [wrap_at, q_old) будет пересканирован заново, и переключающие
|
||
* символы (обратная кавычка, звёздочка, подчёркивание, тильда)
|
||
* внутри него иначе применились бы дважды — именно так возникал
|
||
* баг: code-маркер на границе переноса ломает цвет следующего слова (mdview.c, отчёт пользователя
|
||
* 2026-06-23). */
|
||
line_style = wrap_style;
|
||
}
|
||
}
|
||
|
||
*io_line_style = line_style;
|
||
return q;
|
||
}
|
||
|
||
/* Строит индекс сегментов рендера по загруженному файлу. */
|
||
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;
|
||
}
|
||
|
||
/* Сбрасывает накопленный "прогон" из *col символов одного атрибута
|
||
* одним bios_writeattr() вместо посимвольных wrchar(). *need_place
|
||
* лениво ставит BIOS-курсор перед первым выводом в текущей строке —
|
||
* далее place продвигается сам (verified), повторный bios_set_place
|
||
* не нужен пока *col растёт монотонно. */
|
||
static void flush_run(uint8_t row, uint8_t *col, const char *buf,
|
||
uint8_t len, uint8_t attr, uint8_t *need_place)
|
||
{
|
||
if (len == 0) return;
|
||
/* *col уже указывает на конец прогона (вызывающий цикл инкрементит
|
||
* col на каждый символ ещё во время накопления в буфер — это нужно
|
||
* для верных проверок col < SCREEN_W до сброса). Поэтому начало
|
||
* прогона на экране = *col - len, а не *col; и *col второй раз
|
||
* наращивать НЕ нужно — он уже корректен. */
|
||
if (*need_place) {
|
||
bios_set_place(row, (uint8_t)(*col - len));
|
||
*need_place = 0;
|
||
}
|
||
bios_writeattr(buf, len, attr);
|
||
}
|
||
|
||
static void render_line(uint16_t line_idx, uint8_t row)
|
||
{
|
||
/* Один idx_get() на line_idx вместо ~13 отдельных is_blank/is_cont/
|
||
* is_nowrap/is_code_body/seg_off/get_init_style* вызовов. Каждый из
|
||
* них шёл через seg_flags()/seg_off() -> idx_get() -> bank_read(),
|
||
* а bank_read() ДВАЖДЫ переключает банк окна 3 (на страницу индекса
|
||
* и обратно) плюс memcpy — это оказалось главным узким местом
|
||
* рендера (≈700+ переключений банка на кадр), а не сам вывод на
|
||
* экран. Все нужные поля (off/flags/style) лежат в одной 8-байтовой
|
||
* записи idx_rec_t — читаем её один раз и используем дальше. */
|
||
uint8_t have_rec = (line_idx < n_lines);
|
||
idx_rec_t rec;
|
||
if (have_rec) idx_get(line_idx, &rec);
|
||
|
||
uint8_t col = 0;
|
||
uint8_t base_attr = ATTR_TEXT;
|
||
uint8_t is_code = (uint8_t)(have_rec && (rec.flags & IF_CODE));
|
||
uint8_t cur_attr = is_code ? ATTR_TEXT_CODE
|
||
: styles_map[(have_rec ? rec.style : INIT_STYLE_PLAIN) & 7u];
|
||
uint8_t emph = is_code ? EM_CODE
|
||
: (uint8_t)((have_rec ? rec.style : INIT_STYLE_PLAIN) & 7u);
|
||
uint8_t parse_inline = 1; /* 1: включён парсер выделений, 0: выводим код как есть */
|
||
|
||
if (have_rec && (rec.flags & IF_BLANK)) {
|
||
bios_set_place(row, col);
|
||
bios_fillcharattr(' ', ATTR_TEXT, (uint8_t)(SCREEN_W - col));
|
||
return;
|
||
}
|
||
|
||
if (have_rec) {
|
||
uint32_t p = rec.off;
|
||
uint32_t content_off = p;
|
||
uint8_t cont = (uint8_t)((rec.flags & IF_CONT) != 0);
|
||
/* Правая граница сегмента: начало следующего сегмента или конец файла. */
|
||
uint32_t seg_end;
|
||
if ((uint16_t)(line_idx + 1) < n_lines) {
|
||
idx_rec_t rec_next;
|
||
idx_get((uint16_t)(line_idx + 1), &rec_next);
|
||
seg_end = rec_next.off;
|
||
} else {
|
||
seg_end = file_size;
|
||
}
|
||
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;
|
||
idx_rec_t rec_first = rec; /* rec.flags здесь уже содержит IF_CONT */
|
||
/* Для любого сегмента-продолжения откатываемся к первому
|
||
* НЕ-cont сегменту этой логической строки — один idx_get()
|
||
* на шаг вместо двух (is_cont + повторный seg_off). */
|
||
while (first > 0 && (rec_first.flags & IF_CONT)) {
|
||
first--;
|
||
idx_get(first, &rec_first);
|
||
}
|
||
uint32_t p_start = rec_first.off;
|
||
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 && q < SCREEN_W && fb(p_start + q) == ' ') q++;
|
||
if (q > (uint8_t)(SCREEN_W - col)) q = (uint8_t)(SCREEN_W - col);
|
||
if (q > 0) {
|
||
bios_set_place(row, col);
|
||
bios_fillcharattr(' ', ATTR_TEXT, q);
|
||
col = (uint8_t)(col + 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 = indent;
|
||
if (q > (uint8_t)(SCREEN_W - col)) q = (uint8_t)(SCREEN_W - col);
|
||
if (q > 0) {
|
||
bios_set_place(row, col);
|
||
bios_fillcharattr(' ', ATTR_TEXT, q);
|
||
col = (uint8_t)(col + q);
|
||
}
|
||
}
|
||
/* p/content_off уже равны rec.off — внутри cont-блока они не
|
||
* менялись (там свои локальные тени с тем же именем),
|
||
* пересчитывать через seg_off() заново не нужно. */
|
||
}
|
||
|
||
/* Кодовый блок:
|
||
* - строка-разделитель показывается как пустая;
|
||
* - тело рисуется «как есть» с ATTR_TEXT_CODE без inline-парсинга.
|
||
* is_fence_delim(line_idx) разворачено на месте: cont и p уже
|
||
* известны, повторный idx_get()/is_cont() не нужен. */
|
||
if (!cont && p + 2 < file_size &&
|
||
fb(p) == '`' && fb(p + 1) == '`' && fb(p + 2) == '`') {
|
||
bios_set_place(row, col);
|
||
bios_fillcharattr(' ', ATTR_TEXT, (uint8_t)(SCREEN_W - col));
|
||
return;
|
||
}
|
||
if (is_code) {
|
||
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) {
|
||
bios_set_place(row, col);
|
||
bios_fillcharattr(0xC4, ATTR_HR, (uint8_t)(SCREEN_W - col));
|
||
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;
|
||
uint32_t qmax = p + SCREEN_W;
|
||
while (q + 2 < content_off && q < qmax && fb(q) == ' ') q++;
|
||
uint8_t nsp = (uint8_t)(q - p);
|
||
if (nsp > (uint8_t)(SCREEN_W - col)) nsp = (uint8_t)(SCREEN_W - col);
|
||
if (nsp > 0) {
|
||
bios_set_place(row, col);
|
||
bios_fillcharattr(' ', ATTR_TEXT, nsp);
|
||
col = (uint8_t)(col + nsp);
|
||
}
|
||
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;
|
||
uint32_t qmax = p + SCREEN_W;
|
||
while (q < content_off && q < qmax && fb(q) == ' ') q++;
|
||
uint8_t nsp = (uint8_t)(q - p);
|
||
if (nsp > (uint8_t)(SCREEN_W - col)) nsp = (uint8_t)(SCREEN_W - col);
|
||
if (nsp > 0) {
|
||
bios_set_place(row, col);
|
||
bios_fillcharattr(' ', ATTR_TEXT, nsp);
|
||
col = (uint8_t)(col + nsp);
|
||
}
|
||
/* Маркер (цифры/'.'/')') читается из fb() — это окно 3,
|
||
* BIOS принимает указатели только из #4000..#BFFF, поэтому
|
||
* сначала копируем во временный буфер на стеке (окно 2),
|
||
* чанками по 8 байт (маркер почти всегда короче, но длинный
|
||
* корректно не обрезается, просто шлётся несколькими
|
||
* bios_writeattr вместо одного). */
|
||
{
|
||
char mkbuf[8];
|
||
uint8_t first_chunk = 1;
|
||
while (q < content_off - 1 && col < SCREEN_W) {
|
||
uint8_t n = 0;
|
||
while (q < content_off - 1 &&
|
||
(uint8_t)(col + n) < SCREEN_W &&
|
||
n < sizeof(mkbuf)) {
|
||
mkbuf[n] = fb(q);
|
||
q++;
|
||
n++;
|
||
}
|
||
if (n == 0) break;
|
||
if (first_chunk) { bios_set_place(row, col); first_chunk = 0; }
|
||
bios_writeattr(mkbuf, n, ATTR_LIST_MARKER);
|
||
col = (uint8_t)(col + n);
|
||
}
|
||
}
|
||
if (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT);
|
||
p = content_off;
|
||
} else if (kind == LK_QUOTE) {
|
||
/* Отступ -> пробелы; затем quote-префикс '│' (0xB3),
|
||
* пробел и далее содержимое. */
|
||
uint32_t q = p;
|
||
uint32_t qmax = p + SCREEN_W;
|
||
while (q + 1 < content_off && q < qmax && fb(q) == ' ') q++;
|
||
uint8_t nsp = (uint8_t)(q - p);
|
||
if (nsp > (uint8_t)(SCREEN_W - col)) nsp = (uint8_t)(SCREEN_W - col);
|
||
if (nsp > 0) {
|
||
bios_set_place(row, col);
|
||
bios_fillcharattr(' ', ATTR_TEXT, nsp);
|
||
col = (uint8_t)(col + nsp);
|
||
}
|
||
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 = (rec.flags & IF_NOWRAP) ? viewport_x : 0;
|
||
/* cc — видимая колонка внутри области контента
|
||
* (после фиксированного префикса строки). */
|
||
uint8_t cc = 0;
|
||
char prev_ch = ' '; /* используется в проверке границ emphasis */
|
||
|
||
/* Буферизация "ровного" прогона одного атрибута: символ и атрибут
|
||
* внутри этого while переменные (emphasis/tab/soft-wrap), поэтому
|
||
* весь цикл одним BIOS-вызовом не свернуть — но между сменами
|
||
* атрибута почти всегда идёт протяжённый одноцветный текст, и
|
||
* именно его стоит копить в runbuf и сбрасывать одним
|
||
* bios_writeattr() вместо посимвольных wrchar().
|
||
* runbuf вмещает целую строку экрана — переполнение невозможно,
|
||
* т.к. весь цикл и так ограничен col < SCREEN_W. */
|
||
char runbuf[SCREEN_W];
|
||
uint8_t run_len = 0;
|
||
uint8_t run_attr = cur_attr;
|
||
uint8_t need_place = 1;
|
||
|
||
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) {
|
||
/* Атрибут меняется -> сбросить накопленный прогон
|
||
* со СТАРЫМ атрибутом, прежде чем переключиться. */
|
||
flush_run(row, &col, runbuf, run_len, run_attr, &need_place);
|
||
run_len = 0;
|
||
cur_attr = emph_to_attr(emph, base_attr);
|
||
run_attr = cur_attr;
|
||
continue;
|
||
}
|
||
if (mk == 2) {
|
||
if (cc >= effective_vx && col < SCREEN_W && run_len < sizeof(runbuf)) {
|
||
runbuf[run_len++] = lit_ch;
|
||
col++;
|
||
}
|
||
cc++;
|
||
if (cc >= effective_vx && col < SCREEN_W && run_len < sizeof(runbuf)) {
|
||
runbuf[run_len++] = lit_ch;
|
||
col++;
|
||
}
|
||
cc++;
|
||
prev_ch = lit_ch;
|
||
continue;
|
||
}
|
||
}
|
||
|
||
if (!soft_join) p++;
|
||
if (ch == '\t') {
|
||
/* Таб расширяется в координатах контента (cc).
|
||
* Сначала сбросить накопленный текстовый прогон — таб
|
||
* выводится отдельным bios_fillcharattr(), не через runbuf. */
|
||
flush_run(row, &col, runbuf, run_len, run_attr, &need_place);
|
||
run_len = 0;
|
||
|
||
uint8_t cc_start = cc;
|
||
uint8_t tgt = (uint8_t)((cc & (uint8_t)~(TAB_STOP - 1)) + TAB_STOP);
|
||
uint8_t n_total = (uint8_t)(tgt - cc_start);
|
||
uint8_t skip = (effective_vx > cc_start) ? (uint8_t)(effective_vx - cc_start) : 0;
|
||
if (skip > n_total) skip = n_total;
|
||
uint8_t want = (uint8_t)(n_total - skip);
|
||
if (want > (uint8_t)(SCREEN_W - col)) want = (uint8_t)(SCREEN_W - col);
|
||
if (want > 0) {
|
||
bios_set_place(row, col);
|
||
bios_fillcharattr(' ', cur_attr, want);
|
||
col = (uint8_t)(col + want);
|
||
need_place = 0;
|
||
}
|
||
cc = (uint8_t)(cc_start + skip + want);
|
||
prev_ch = ' ';
|
||
} else {
|
||
if (cc >= effective_vx && col < SCREEN_W && run_len < sizeof(runbuf)) {
|
||
runbuf[run_len++] = ch;
|
||
col++;
|
||
}
|
||
cc++;
|
||
prev_ch = ch;
|
||
}
|
||
}
|
||
flush_run(row, &col, runbuf, run_len, run_attr, &need_place);
|
||
|
||
/* Индикатор обрезки справа для nowrap-строки:
|
||
* если после текущей позиции есть ещё видимый контент,
|
||
* поверх последней колонки рисуем '>'. */
|
||
if ((rec.flags & IF_NOWRAP) && col >= SCREEN_W) {
|
||
uint32_t pp = p;
|
||
uint32_t left_bound = rec.off;
|
||
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 ((rec.flags & IF_NOWRAP) && viewport_x > 0) {
|
||
wrchar(0, row, '<', ATTR_TRUNC);
|
||
}
|
||
}
|
||
/* Дозаполняем остаток строки пробелами, чтобы стереть хвост
|
||
* от предыдущего кадра рендера. */
|
||
{
|
||
uint8_t pad_attr = (parse_inline ? ATTR_TEXT : ATTR_TEXT_CODE);
|
||
if (col < SCREEN_W) {
|
||
bios_set_place(row, col);
|
||
bios_fillcharattr(' ', pad_attr, (uint8_t)(SCREEN_W - col));
|
||
}
|
||
}
|
||
}
|
||
|
||
/* Рендерит все 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);
|
||
bios_set_place(0, 45);
|
||
bios_fillcharattr(' ', ATTR_BAR, (uint8_t)(SCREEN_W - 45));
|
||
|
||
wrchar(45, 0, 0xB3, ATTR_BAR); /* вертикальный разделитель */
|
||
gotoxy(47, 0);
|
||
cputs("L ");
|
||
dec16(top_line + 1);
|
||
cputs("-");
|
||
dec16(last);
|
||
cputs(" / ");
|
||
dec16(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 md_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 md_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 md_scroll_horizon(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();
|
||
}
|
||
|
||
/* ==================================================================
|
||
* Диалог справки (F1)
|
||
* ================================================================== */
|
||
|
||
/* Заполняет одну внутреннюю строку диалога (r=0 — первая строка за рамкой).
|
||
* Строка s в кодировке CP866; остаток до края дополняется пробелами. */
|
||
static void help_line(uint8_t r, const char *s, uint8_t attr)
|
||
{
|
||
uint8_t x = HELP_X + 1u;
|
||
uint8_t y = HELP_Y + 1u + r;
|
||
uint8_t len = (uint8_t)strlen(s);
|
||
if (len > HELP_W - 2u) len = HELP_W - 2u;
|
||
|
||
bios_set_place(y, x);
|
||
if (len > 0) bios_writeattr(s, len, attr);
|
||
/* place уже продвинут bios_writeattr на len колонок (verified). */
|
||
if (len < HELP_W - 2u) {
|
||
bios_fillcharattr(' ', ATTR_HELP_BG, (uint8_t)(HELP_W - 2u - len));
|
||
}
|
||
}
|
||
|
||
static void show_help(void)
|
||
{
|
||
/* " Помощь " в CP866 (8 байт) */
|
||
static const char title[] = " \x8F\xAE\xAC\xAE\xE9\xEC ";
|
||
uint8_t tlen = 8u;
|
||
uint8_t lft = (uint8_t)((HELP_W - 2u - tlen) / 2u); /* = 27 */
|
||
uint8_t rgt = (uint8_t)(HELP_W - 2u - tlen - lft);
|
||
/* Верхняя граница рамки с заголовком по центру.
|
||
* place продвигается каждым BIOS-вызовом (verified), поэтому
|
||
* достаточно одной установки места на всю строку рамки. */
|
||
wrchar(HELP_X, HELP_Y, 0xDA, ATTR_HELP_BDR); /* ┌ */
|
||
bios_set_place(HELP_Y, (uint8_t)(HELP_X + 1u));
|
||
if (lft > 0) bios_fillcharattr(0xC4, ATTR_HELP_BDR, lft);
|
||
if (tlen > 0) bios_writeattr(title, tlen, ATTR_HELP_TIT);
|
||
if (rgt > 0) bios_fillcharattr(0xC4, ATTR_HELP_BDR, rgt);
|
||
wrchar(HELP_X + HELP_W - 1u, HELP_Y, 0xBF, ATTR_HELP_BDR); /* ┐ */
|
||
|
||
/* Боковые границы (левый и правый │ для каждой строки тела) */
|
||
for (uint8_t r = 1u; r < HELP_H - 1u; r++) {
|
||
wrchar(HELP_X, HELP_Y + r, 0xB3, ATTR_HELP_BDR); /* │ */
|
||
wrchar(HELP_X + HELP_W - 1u, HELP_Y + r, 0xB3, ATTR_HELP_BDR); /* │ */
|
||
}
|
||
|
||
/* Нижняя граница рамки */
|
||
wrchar(HELP_X, HELP_Y + HELP_H - 1u, 0xC0, ATTR_HELP_BDR); /* └ */
|
||
bios_set_place((uint8_t)(HELP_Y + HELP_H - 1u), (uint8_t)(HELP_X + 1u));
|
||
bios_fillcharattr(0xC4, ATTR_HELP_BDR, (uint8_t)(HELP_W - 2u));
|
||
wrchar(HELP_X + HELP_W - 1u, HELP_Y + HELP_H - 1u, 0xD9, ATTR_HELP_BDR); /* ┘ */
|
||
|
||
/* Содержимое (20 внутренних строк) */
|
||
uint8_t r = 0;
|
||
help_line(r++, "", ATTR_HELP_BG);
|
||
help_line(r++, " MDView v0.2 -- Markdown Viewer for Sprinter", ATTR_HELP_HDR);
|
||
help_line(r++, " (c) 2026 Petrov A.G.", ATTR_HELP_BG);
|
||
help_line(r++, "", ATTR_HELP_BG);
|
||
help_line(r++, " Navigation:", ATTR_HELP_HDR);
|
||
help_line(r++, " \x18 \x19 Scroll one line up / down", ATTR_HELP_BG);
|
||
help_line(r++, " PgUp PgDn Scroll one page up / down", ATTR_HELP_BG);
|
||
help_line(r++, " Home End Jump to beginning / end of document", ATTR_HELP_BG);
|
||
help_line(r++, " \x1B \x1A Horizontal pan (code blocks/tables)", ATTR_HELP_BG);
|
||
help_line(r++, " Esc F10 Exit", ATTR_HELP_BG);
|
||
help_line(r++, "", ATTR_HELP_BG);
|
||
help_line(r++, " Markdown elements:", ATTR_HELP_HDR);
|
||
help_line(r++, " # ## ### Headings H1-H6", ATTR_HELP_BG);
|
||
help_line(r++, " **bold** *italic* `code` ~~strike~~", ATTR_HELP_BG);
|
||
help_line(r++, " > quote ``` ... ``` Fenced code block", ATTR_HELP_BG);
|
||
help_line(r++, " - * + item 1. 2. Ordered list", ATTR_HELP_BG);
|
||
help_line(r++, " |----|----| Tables", ATTR_HELP_BG);
|
||
help_line(r++, "", ATTR_HELP_BG);
|
||
help_line(r++, " File size: up to 128 KB (EMM). Lines: up to 16384.", ATTR_HELP_BG);
|
||
help_line(r++, "", ATTR_HELP_BG);
|
||
|
||
(void)getkey();
|
||
|
||
render_full_status();
|
||
render_viewport();
|
||
render_menu();
|
||
}
|
||
|
||
/* ==================================================================
|
||
* Точка входа
|
||
* ================================================================== */
|
||
|
||
int main(int argc, char **argv)
|
||
{
|
||
const char *path;
|
||
if (argc >= 2) {
|
||
path = argv[1];
|
||
} else {
|
||
path = "README.MD";
|
||
/* Дефолтный файл отсутствует — выводим подсказку без смены режима экрана. */
|
||
{
|
||
int chk = open(path, O_RDONLY);
|
||
if (chk < 0) {
|
||
cputs("MDView v0.2 -- Markdown Viewer for Sprinter\r\n");
|
||
cputs("(c) 2026 Petrov A.G.\r\n\r\n");
|
||
cputs("Usage: mdview <file.md>\r\n");
|
||
cputs(" Displays a Markdown document (CP866, up to 128 KB).\r\n");
|
||
return 1;
|
||
}
|
||
close(chk);
|
||
}
|
||
}
|
||
|
||
/* Копируем путь в static filename[] для отображения в статус-баре. */
|
||
{
|
||
uint8_t i = 0;
|
||
while (i < (uint8_t)(sizeof(filename) - 1) && path[i]) {
|
||
filename[i] = path[i];
|
||
i++;
|
||
}
|
||
filename[i] = 0;
|
||
}
|
||
|
||
/* Сначала поднимаем UI, чтобы пользователь видел шапку/меню/спиннер
|
||
* уже во время загрузки файла и построения индекса. */
|
||
settextmode(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_F1: show_help(); break;
|
||
case KEY_F10: goto exit_loop;
|
||
case KEY_UP: md_scroll_up(1); break;
|
||
case KEY_DOWN: md_scroll_down(1); break;
|
||
case KEY_LEFT: md_scroll_horizon(-(int8_t)HPAN_STEP); break;
|
||
case KEY_RIGHT: md_scroll_horizon(+(int8_t)HPAN_STEP); break;
|
||
case KEY_PGUP: md_scroll_up(VIEW_H); break;
|
||
case KEY_PGDN: md_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;
|
||
}
|