Files
snark13 858e5755ad ChangeLog:
- big commit.
2026-06-10 10:35:48 +03:00

1867 lines
78 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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 <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),
* и останавливается на конце строки или правой границе экрана. */
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.
* Проверка границ выполняется вызывающей стороной.
*/
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;
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();
}
/* ==================================================================
* Диалог справки (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 i = 0;
while (s[i] && i < HELP_W - 2u) {
wrchar((uint8_t)(x + i), y, s[i], attr);
i++;
}
while (i < HELP_W - 2u) {
wrchar((uint8_t)(x + i), y, ' ', ATTR_HELP_BG);
i++;
}
}
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);
uint8_t cx, i;
/* Верхняя граница рамки с заголовком по центру */
wrchar(HELP_X, HELP_Y, 0xDA, ATTR_HELP_BDR); /* ┌ */
cx = HELP_X + 1u;
for (i = 0; i < lft; i++, cx++) wrchar(cx, HELP_Y, 0xC4, ATTR_HELP_BDR);
for (i = 0; i < tlen; i++, cx++) wrchar(cx, HELP_Y, title[i], ATTR_HELP_TIT);
for (i = 0; i < rgt; i++, cx++) wrchar(cx, HELP_Y, 0xC4, ATTR_HELP_BDR);
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); /* └ */
for (i = 1u; i < HELP_W - 1u; i++)
wrchar(HELP_X + i, HELP_Y + HELP_H - 1u, 0xC4, ATTR_HELP_BDR);
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, чтобы пользователь видел шапку/меню/спиннер
* уже во время загрузки файла и построения индекса. */
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_F1: show_help(); break;
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;
}