ca5f30b332
- Index is now an 8-byte record per visible segment in a dedicated EMM block (idx_get/idx_put), freeing ~11 KB of near RAM and lifting the old 2048-line cap (dynamic max_lines = index_pages * 2048). - The per-byte scan keeps the previous segment offset in a near var (cur_seg_off) and mirrors the last record (cur_rec), so it never reads the index back from the bank. - fb()/map_page() are inlined now that there is code headroom, removing per-byte call + 32-bit argument marshalling overhead. Co-Authored-By: Oz <oz-agent@warp.dev>
1479 lines
54 KiB
C
1479 lines
54 KiB
C
/*
|
||
* mdview — markdown text viewer for Sprinter.
|
||
*
|
||
* Layout (80x32):
|
||
* row 0 — status bar (filename / line range / total / %)
|
||
* rows 1..30 — viewport (30 lines of document text)
|
||
* row 31 — menu bar (F1 Help / F2 Wrap / F10 Exit)
|
||
*
|
||
* Navigation: arrows, PgUp/PgDn, Home/End, F1 help, F2 wrap toggle,
|
||
* F10/Esc exit. In Unwrap mode ←/→ pan horizontally.
|
||
*
|
||
* Memory:
|
||
* CODE → W1 (sprinter-cc --memory small)
|
||
* STACK/HEAP/DATA → W2
|
||
* File buffer → EMM pages (up to 8 × 16 KB = 128 KB) mapped
|
||
* into W3 at 0xC000 on demand via fb()/map_page().
|
||
*/
|
||
|
||
#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>
|
||
|
||
/* ---- screen geometry ---------------------------------------------- */
|
||
|
||
#define SCREEN_W 80
|
||
#define SCREEN_H 32
|
||
#define VIEW_TOP_ROW 1
|
||
#define VIEW_H 30 /* rows 1..30 inclusive */
|
||
#define MENU_ROW 31
|
||
#define TAB_STOP 4
|
||
|
||
/* ---- file/memory layout ------------------------------------------- */
|
||
|
||
#define PAGE_BITS 14u
|
||
#define PAGE_SIZE (1u << PAGE_BITS) /* 16 KB */
|
||
#define PAGE_MASK ((uint16_t)(PAGE_SIZE - 1u))
|
||
#define MAX_PAGES 8 /* 8 × 16 KB = 128 KB */
|
||
#define MAX_FILE ((uint32_t)MAX_PAGES * PAGE_SIZE) /* 131072 */
|
||
#define MAX_LINES 2048
|
||
#define FILE_BUF ((char *)0xC000) /* W3-mapped EMM page */
|
||
|
||
/* ---- attribute palette -------------------------------------------- */
|
||
|
||
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
|
||
};
|
||
// static uint8_t md_pallete[16 * 4] = {
|
||
// 0x00,0x00,0x00, 0x00,
|
||
// 0x10,0x10,0x10, 0x00,
|
||
// 0x20,0x20,0x20, 0x00,
|
||
// 0x30,0x30,0x30, 0x00,
|
||
// 0x40,0x40,0x40, 0x00,
|
||
// 0x50,0x50,0x50, 0x00,
|
||
// 0x60,0x60,0x60, 0x00,
|
||
// 0x70,0x70,0x70, 0x00,
|
||
// 0x80,0x80,0x80, 0x00,
|
||
// 0x90,0x90,0x90, 0x00,
|
||
// 0xA0,0xA0,0xA0, 0x00,
|
||
// 0xB0,0xB0,0xB0, 0x00,
|
||
// 0xC0,0xC0,0xC0, 0x00,
|
||
// 0xD0,0xD0,0xD0, 0x00,
|
||
// 0xE0,0xE0,0xE0, 0x00,
|
||
// 0xF0,0xF0,0xF0, 0x00
|
||
// };
|
||
|
||
#define ATTR_TEXT COLOR(COLOR_LIGHTGRAY, COLOR_BLUE)
|
||
#define ATTR_TEXT_TITLE1 COLOR(COLOR_YELLOW, COLOR_BLUE)
|
||
#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_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)
|
||
|
||
/* ---- global state ------------------------------------------------- */
|
||
|
||
/* Files up to MAX_FILE = 128 KB are supported by spreading content
|
||
* across up to 8 EMM pages. Only one page is mapped into W3 at any
|
||
* given time (FILE_BUF); fb() swaps pages on demand and tracks the
|
||
* currently-mapped one in cur_page to avoid redundant OUTs.
|
||
*
|
||
* The screen-segment index is a separate EMM block of 8-byte records
|
||
* (one per VISIBLE viewport row), accessed through W3 via idx_get/idx_put.
|
||
* Each record holds the segment's start offset, flags (cont/nowrap/blank/
|
||
* code) and the initial inline style; the right bound is the next record's
|
||
* offset. This frees ~11 KB of near RAM and lifts the old 2048-line cap.
|
||
*/
|
||
|
||
/* NOTE: explicit `= 0` initialisers are MANDATORY on SDCC z80.
|
||
* Uninitialised static storage (`static T x;`) doesn't actually allocate
|
||
* space — multiple such declarations end up at the SAME address and
|
||
* silently overwrite each other. See memory/sdcc_static_storage_gotcha. */
|
||
/* Screen-segment index. One compact record per VISIBLE viewport row,
|
||
* stored in a dedicated EMM block (through W3) rather than scarce near
|
||
* RAM. This both removes the old MAX_LINES near-array cap and frees
|
||
* ~11 KB of W2 for code/stack. */
|
||
typedef struct idx_rec_s {
|
||
uint32_t off; /* first source byte rendered by this segment */
|
||
uint8_t flags; /* IF_* bits below */
|
||
uint8_t style; /* INIT_STYLE_* active at segment start */
|
||
uint8_t pad0; /* padding → 8-byte record (2048 records / 16 KB page) */
|
||
uint8_t pad1;
|
||
} idx_rec_t;
|
||
|
||
#define INDEX_REC_SIZE 8u
|
||
#define INDEX_RECS_PER_PAGE 2048u /* 16384 / 8 — records never straddle a page */
|
||
#define MAX_INDEX_PAGES 8u /* 8 * 2048 = 16384 segments max */
|
||
|
||
#define IF_CONT 0x01u /* continuation of a wrapped logical line */
|
||
#define IF_NOWRAP 0x02u /* must not wrap (code block / HR / table) */
|
||
#define IF_BLANK 0x04u /* visually blank row */
|
||
#define IF_CODE 0x08u /* fenced code-block body (verbatim style) */
|
||
|
||
static uint16_t n_lines = 0;
|
||
static uint16_t max_lines = 0; /* index capacity = 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; /* set when the index capacity is exhausted */
|
||
static uint32_t cur_seg_off = 0; /* off of the most-recent emitted seg (near; no bank read-back) */
|
||
static idx_rec_t cur_rec; /* mirror of the most-recent emitted record (near) */
|
||
static uint8_t cur_page = 0xFF; /* 0xFF = no page mapped yet */
|
||
static uint8_t viewport_x = 0; /* horizontal pan (for nowrap lines) */
|
||
static char filename[64] = {0};
|
||
|
||
#define HPAN_STEP 8u
|
||
|
||
/* Spinner on the title bar (col=8, row=0): a small animated glyph
|
||
* shown while load_file / index_lines run, so the user knows the
|
||
* viewer is busy and not frozen. */
|
||
#define SPINNER_COL 8
|
||
static const char spinner_chars[4] = { '|', '/', '-', '\\' };
|
||
static uint8_t spinner_phase = 0;
|
||
static uint8_t spinner_active = 0;
|
||
|
||
/* ==================================================================
|
||
* tiny helpers
|
||
* ================================================================== */
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
static void put_str(uint8_t x, uint8_t y, const char *s)
|
||
{
|
||
gotoxy(x, y);
|
||
puts(s);
|
||
}
|
||
|
||
static void fill_row(uint8_t y, uint8_t attr)
|
||
{
|
||
for (uint8_t c = 0; c < SCREEN_W; c++) {
|
||
wrchar(c, y, ' ', attr);
|
||
}
|
||
}
|
||
|
||
/* Advance the spinner by one frame. No-op when inactive — safe to call
|
||
* unconditionally from any loop that wants to show "still working". */
|
||
static void spinner_tick(void)
|
||
{
|
||
if (!spinner_active) return;
|
||
wrchar(SPINNER_COL, 0, spinner_chars[spinner_phase & 3], ATTR_BAR_SPINNER);
|
||
spinner_phase++;
|
||
}
|
||
|
||
/* Enable / disable the spinner. Disabling also wipes the glyph from the
|
||
* title bar so the slot looks like a plain space. */
|
||
static void spinner_show(uint8_t on)
|
||
{
|
||
spinner_active = on;
|
||
if (!on) wrchar(SPINNER_COL, 0, ' ', ATTR_BAR);
|
||
}
|
||
|
||
/* Map the EMM page that holds file offset >= page * PAGE_SIZE into W3.
|
||
* No-op if already mapped. */
|
||
static inline void map_page(uint8_t page)
|
||
{
|
||
if (page != cur_page) {
|
||
/* file_phys[] saves a BIOS mem_get_page() call on every page swap. */
|
||
sprinter_page_w3(file_phys[page]);
|
||
cur_page = page;
|
||
}
|
||
}
|
||
|
||
/* Read one byte from the logical file at byte offset `p`. Swaps the W3
|
||
* mapping if `p` falls into a different page than the one currently
|
||
* mapped. Bounds checking is the caller's job.
|
||
*
|
||
* Hot path: index_lines() calls this for almost every input byte. On z80
|
||
* 32-bit shifts/masks are helper calls, so we decode page/offset from the
|
||
* little-endian byte layout of `p`: page = bits 14..16, offset = bits 0..13. */
|
||
static inline char fb(uint32_t p)
|
||
{
|
||
uint8_t *pb = (uint8_t *)&p;
|
||
uint8_t page = (uint8_t)((pb[1] >> 6) | (pb[2] << 2));
|
||
uint16_t off = (uint16_t)(((uint16_t)(pb[1] & 0x3F) << 8) | pb[0]);
|
||
map_page(page);
|
||
return FILE_BUF[off];
|
||
}
|
||
|
||
/* Treat space/tab/EOL/NUL as whitespace for emphasis flanking checks. */
|
||
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 emphasis marker (asterisk, double-asterisk, underscore) is
|
||
* recognised only when ONE side is whitespace/EOL and the other is not
|
||
* (XOR). This treats "2 * 3" and "2 ** 3" (whitespace both sides) as
|
||
* literal, as well as intraword cases like COLOR_YELLOW (no whitespace
|
||
* either side), while keeping *italic*, **bold**, _under_ working. */
|
||
static uint8_t is_emph_flanked(char prev_ch, char next_ch)
|
||
{
|
||
return (uint8_t)(ws_or_eol(prev_ch) != ws_or_eol_or_delim(next_ch));
|
||
}
|
||
|
||
/* Continuation kind passed to emit_seg(); no longer stored (render derives
|
||
* the kind from classify_line()), kept only as call-site argument names. */
|
||
#define CK_PLAIN 0
|
||
#define CK_QUOTE 1
|
||
#define CK_LIST 2
|
||
#define CK_OTHER 3
|
||
|
||
/* EMM index accessors. Records are 8 bytes and aligned within an index
|
||
* page, so bank_read/bank_write never split a record across a page. */
|
||
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);
|
||
}
|
||
|
||
/* Field readers used by render & navigation (per-row, not per-byte). */
|
||
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); }
|
||
|
||
/* Flag setters target the MOST-RECENT emitted seg (n_lines-1) through the
|
||
* near mirror cur_rec, so the scan never reads the index back from the bank. */
|
||
static void set_nowrap_cur(void) { cur_rec.flags |= IF_NOWRAP; idx_put((uint16_t)(n_lines - 1), &cur_rec); }
|
||
static void set_blank_cur(void) { cur_rec.flags |= IF_BLANK; idx_put((uint16_t)(n_lines - 1), &cur_rec); }
|
||
static void set_code_cur(void) { cur_rec.flags |= IF_CODE; idx_put((uint16_t)(n_lines - 1), &cur_rec); }
|
||
|
||
static void set_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];
|
||
// buff[j * 4] = i * 16;
|
||
// buff[j * 4 + 1] = i * 16;
|
||
// buff[j * 4 + 2] = i * 16;
|
||
// buff[j * 4 + 3] = 0;
|
||
}
|
||
|
||
text_pal_load(TEXT_PAL_INK, i * 16, 16, md_pallete);
|
||
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);
|
||
}
|
||
}
|
||
|
||
/* ==================================================================
|
||
* file loading
|
||
* ================================================================== */
|
||
|
||
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);
|
||
|
||
/* Allocate exactly the number of EMM pages we need (1..MAX_PAGES). */
|
||
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;
|
||
}
|
||
|
||
/* Read the file 16 KB at a time, mapping each page into W3 in turn.
|
||
* `remaining` decreases by exactly PAGE_SIZE on each non-final iter;
|
||
* the final iter reads whatever is left. */
|
||
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);
|
||
|
||
/* Allocate the EMM index block (8 B/segment, 2048 records per 16 KB
|
||
* page). Size it a little larger than the file so even dense files
|
||
* index fully; back off if EMM is tight. max_lines becomes the cap. */
|
||
{
|
||
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;
|
||
}
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
/* ==================================================================
|
||
* line indexing
|
||
* ================================================================== */
|
||
|
||
/* Returns 1 iff the seg at index `idx` is the start of a logical line
|
||
* that begins with ``` (a fenced-code block delimiter). Continuation
|
||
* segs are never fence delimiters. */
|
||
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
|
||
|
||
static const uint8_t styles_map[] = {
|
||
ATTR_TEXT, ATTR_TEXT_BOLD, ATTR_TEXT_ITALIC, ATTR_TEXT_UNDERSORE, ATTR_TEXT_CODE
|
||
};
|
||
|
||
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];
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
/* Returns 1 iff the logical line at file offset `p_start` should NOT be
|
||
* wrapped — it must render on a single seg even if longer than SCREEN_W.
|
||
* Currently covers fence delimiters and horizontal rules; tables will be
|
||
* added when Phase 4 tables / Phase 6 table layout lands. */
|
||
static uint8_t is_nowrap_line(uint32_t p_start)
|
||
{
|
||
if (p_start + 2 < file_size &&
|
||
fb(p_start) == '`' &&
|
||
fb(p_start + 1) == '`' &&
|
||
fb(p_start + 2) == '`') {
|
||
return 1;
|
||
}
|
||
char c0 = fb(p_start);
|
||
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 1;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/* Line kind returned by classify_line(). */
|
||
#define LK_PLAIN 0
|
||
#define LK_H1 1
|
||
#define LK_H2 2
|
||
#define LK_H3 3
|
||
#define LK_H4 4 /* also used for H5/H6 */
|
||
#define LK_HR 5
|
||
#define LK_ULIST 6 /* "- ", "* ", "+ " bullet */
|
||
#define LK_OLIST 7 /* "12. " or "12) " number */
|
||
#define LK_QUOTE 8 /* "> " blockquote */
|
||
|
||
/* Classify a logical line.
|
||
* - Headers: 1..4 leading '#' followed by space/tab (H5+/H6 collapse to H4).
|
||
* - Horizontal rule: line containing only '-', '*' or '_' (one kind, >=3
|
||
* of them), optionally separated by spaces/tabs. Matches `---`, `***`,
|
||
* `___`, `- - -`, etc. Checked BEFORE ulist so that `- - -` wins.
|
||
* - Unordered list: "- ", "* " or "+ " followed by content.
|
||
* - Ordered list: one or more digits followed by '.' or ')' then space.
|
||
* - Blockquote: '>' optionally followed by space.
|
||
* - Otherwise plain text.
|
||
* For headers/lists/quotes, *out_content is updated to the offset of the
|
||
* first content byte (past the marker and its trailing space).
|
||
*/
|
||
static uint8_t classify_line(uint32_t p_start, uint32_t *out_content)
|
||
{
|
||
if (p_start >= file_size) return LK_PLAIN;
|
||
|
||
char c0 = fb(p_start);
|
||
|
||
/* Header? */
|
||
if (c0 == '#') {
|
||
uint8_t lvl = 0;
|
||
uint32_t p = p_start;
|
||
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 */
|
||
}
|
||
}
|
||
/* Bare '#' with no space → not a header, fall through. */
|
||
}
|
||
|
||
/* Horizontal rule? (must precede ulist so `- - -` wins over `- `) */
|
||
if (c0 == '-' || c0 == '*' || c0 == '_') {
|
||
char marker = 0;
|
||
uint8_t count = 0;
|
||
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;
|
||
}
|
||
|
||
/* For lists and blockquotes we additionally allow leading spaces
|
||
* (light nested-list support). HR / header / fence delim stay
|
||
* strict col-0 — done above with `c0`. */
|
||
uint32_t lp = p_start;
|
||
while (lp < file_size && fb(lp) == ' ') lp++;
|
||
char cl = (lp < file_size) ? fb(lp) : 0;
|
||
|
||
/* Unordered list? */
|
||
if ((cl == '-' || cl == '*' || cl == '+') &&
|
||
(lp + 1) < file_size &&
|
||
fb(lp + 1) == ' ') {
|
||
*out_content = lp + 2;
|
||
return LK_ULIST;
|
||
}
|
||
|
||
/* Ordered list? */
|
||
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;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* Blockquote? */
|
||
if (cl == '>') {
|
||
uint32_t p = lp + 1;
|
||
if (p < file_size && fb(p) == ' ') p++;
|
||
*out_content = p;
|
||
return LK_QUOTE;
|
||
}
|
||
|
||
return LK_PLAIN;
|
||
}
|
||
|
||
/* Visible column count for the marker prefix when rendered, including
|
||
* any leading indent for nested lists/quotes (the render side keeps the
|
||
* 1 byte = 1 visible col invariant for those, so content_off - p_start
|
||
* is the visible width). */
|
||
static uint8_t marker_visible_col(uint8_t kind, uint32_t p_start, uint32_t content_off)
|
||
{
|
||
switch (kind) {
|
||
case LK_ULIST:
|
||
case LK_QUOTE:
|
||
case LK_OLIST: return (uint8_t)(content_off - p_start);
|
||
default: return 0; /* PLAIN / headers (skipped) */
|
||
}
|
||
}
|
||
|
||
/* Emit a new segment entry into the EMM index. cur_rec (near) mirrors the
|
||
* just-written record so set_*_cur() can add flags without a bank read-back,
|
||
* and cur_seg_off feeds the per-byte wrap logic without reading the index. */
|
||
static void emit_seg(uint32_t off, uint8_t style, uint8_t ckind, uint8_t cont)
|
||
{
|
||
(void)ckind; /* kind is derived at render time */
|
||
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;
|
||
}
|
||
|
||
/* Shared inline-emphasis / wrap scanner for a single line or paragraph stream.
|
||
* Starts at `q`, processes until `q_end` (or paragraph break for plain text),
|
||
* emits continuation segs as needed. `col` is the starting visible column
|
||
* (0 for plain/headers, marker width for list/quote). `ckind` is the
|
||
* continuation-kind for wrap segs. Returns the final `line_style` (for
|
||
* paragraph streams that carry emphasis across soft/hard breaks). */
|
||
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') {
|
||
/* hard break (already checked by caller) or stream end */
|
||
q++;
|
||
emit_seg(q, line_style, ckind, (ckind == CK_LIST || ckind == CK_QUOTE) ? 1 : 0);
|
||
last_space = 0xFFFFFFFFu; prev_ch = ' '; seg_col = col;
|
||
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 == '*' || 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;
|
||
}
|
||
|
||
static void index_lines(void)
|
||
{
|
||
uint8_t in_block = 0;
|
||
n_lines = 0;
|
||
index_truncated = 0;
|
||
cur_seg_off = 0xFFFFFFFFu; /* no segment emitted yet */
|
||
if (file_size == 0) return;
|
||
|
||
uint32_t p = 0;
|
||
while (p < file_size && n_lines < max_lines) {
|
||
if ((n_lines & 15) == 0) spinner_tick();
|
||
|
||
/* Blank line = paragraph separator. Emit one empty screen row
|
||
* (only if the previous emitted line was not already blank). */
|
||
if (is_line_blank(p)) {
|
||
while (p < file_size && fb(p) != '\n') p++;
|
||
if (p < file_size) p++;
|
||
if (n_lines == 0 || cur_seg_off != p) {
|
||
emit_seg(p, INIT_STYLE_PLAIN, CK_PLAIN, 0);
|
||
set_blank_cur();
|
||
}
|
||
continue;
|
||
}
|
||
|
||
/* ---- fenced code block (delimiter or body) ---- */
|
||
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;
|
||
}
|
||
|
||
/* ---- horizontal rule ---- */
|
||
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;
|
||
}
|
||
|
||
/* ---- classify current line ---- */
|
||
uint32_t content_off = p;
|
||
uint8_t kind = classify_line(p, &content_off);
|
||
|
||
/* ---- Header: single line, not merged into paragraphs ---- */
|
||
if (kind >= LK_H1 && kind <= LK_H4) {
|
||
emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0);
|
||
uint32_t line_end = content_off;
|
||
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;
|
||
}
|
||
|
||
/* ---- List item: single line, not merged ---- */
|
||
if (kind == LK_ULIST || kind == LK_OLIST) {
|
||
uint8_t col = marker_visible_col(kind, p, content_off);
|
||
emit_seg(p, INIT_STYLE_PLAIN, CK_LIST, 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, col, CK_LIST, INIT_STYLE_PLAIN);
|
||
while (p < file_size && fb(p) != '\n') p++;
|
||
if (p < file_size) p++;
|
||
continue;
|
||
}
|
||
|
||
/* ---- Quote line: single line, not merged ---- */
|
||
if (kind == LK_QUOTE) {
|
||
uint8_t col = marker_visible_col(kind, p, content_off);
|
||
emit_seg(p, INIT_STYLE_PLAIN, CK_QUOTE, 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, col, CK_QUOTE, INIT_STYLE_PLAIN);
|
||
while (p < file_size && fb(p) != '\n') p++;
|
||
if (p < file_size) p++;
|
||
continue;
|
||
}
|
||
|
||
/* ---- Plain text paragraph: merge across soft breaks ---- */
|
||
{
|
||
uint8_t line_style = INIT_STYLE_PLAIN;
|
||
emit_seg(p, line_style, CK_PLAIN, 0);
|
||
|
||
uint32_t q = p;
|
||
uint32_t last_space = 0xFFFFFFFFu;
|
||
char prev_ch = ' ';
|
||
uint8_t seg_col = 0;
|
||
|
||
while (q < file_size && n_lines < max_lines) {
|
||
char ch = fb(q);
|
||
uint8_t soft_break = 0;
|
||
|
||
if (ch == '\n') {
|
||
/* Paragraph break? */
|
||
if (q + 1 >= file_size) break;
|
||
if (fb(q + 1) == '\n') break; /* blank line */
|
||
uint32_t next = q + 1;
|
||
if (is_fence_raw(next)) break;
|
||
if (is_hr_raw(next)) break;
|
||
uint32_t dummy;
|
||
if (classify_line(next, &dummy) != LK_PLAIN) break;
|
||
if (is_line_blank(next)) break;
|
||
|
||
/* Hard break? */
|
||
uint8_t is_hard = 0;
|
||
if (q >= p + 2) {
|
||
char b1 = fb(q - 1);
|
||
char b2 = fb(q - 2);
|
||
if ((b1 == ' ' || b1 == '\t') && (b2 == ' ' || b2 == '\t'))
|
||
is_hard = 1;
|
||
}
|
||
|
||
if (is_hard) {
|
||
q++;
|
||
emit_seg(q, line_style, CK_PLAIN, 0);
|
||
last_space = 0xFFFFFFFFu; prev_ch = ' '; seg_col = 0;
|
||
continue;
|
||
}
|
||
|
||
/* Soft break — treat as a single space. */
|
||
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 == '*' || 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;
|
||
else last_space = q + 1;
|
||
seg_col++;
|
||
if (!soft_break) q++; else 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, CK_PLAIN, 1);
|
||
seg_col = 0; last_space = 0xFFFFFFFFu; prev_ch = ' '; q = wrap_at;
|
||
}
|
||
}
|
||
|
||
if (q >= file_size) {
|
||
p = file_size;
|
||
} else {
|
||
while (q < file_size && fb(q) != '\n') q++;
|
||
if (q + 1 < file_size && fb(q + 1) == '\n') q += 2;
|
||
else 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
|
||
|
||
static void render_line(uint16_t line_idx, uint8_t row)
|
||
{
|
||
uint8_t col = 0;
|
||
uint8_t base_attr = ATTR_TEXT;
|
||
uint8_t cur_attr = is_code_body(line_idx) ? ATTR_TEXT_CODE : get_init_style(line_idx); // ATTR_TEXT;
|
||
uint8_t emph = is_code_body(line_idx) ? EM_CODE : get_init_style_raw(line_idx);
|
||
uint8_t parse_inline = 1; /* skip inline parsing for fenced code body */
|
||
|
||
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);
|
||
/* Right bound for this seg: start of next seg, or file end. */
|
||
uint32_t seg_end = seg_off((uint16_t)(line_idx + 1));
|
||
if (seg_end > file_size) seg_end = file_size;
|
||
uint8_t kind;
|
||
|
||
/* Continuation seg → repeat prefix/indent from the first non-CONT
|
||
* segment of this logical line, then render plain text. */
|
||
if (cont) {
|
||
uint16_t first = line_idx;
|
||
while (first > 0 && is_cont((uint16_t)(first - 1))) first--;
|
||
uint32_t p_start = seg_off(first);
|
||
uint32_t content_off = p_start;
|
||
uint8_t kind = classify_line(p_start, &content_off);
|
||
uint8_t indent = marker_visible_col(kind, p_start, content_off);
|
||
if (kind == LK_QUOTE) {
|
||
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 (kind == LK_ULIST || kind == LK_OLIST) {
|
||
uint8_t q = 0;
|
||
while (q < indent && col < SCREEN_W) {
|
||
wrchar(col++, row, ' ', ATTR_TEXT);
|
||
q++;
|
||
}
|
||
}
|
||
/* start rendering from the wrapped segment offset */
|
||
p = seg_off(line_idx);
|
||
content_off = p;
|
||
/* For cont segs, inline parsing continues normally. */
|
||
}
|
||
|
||
/* Fenced code block: delimiter line → blank row; body lines render
|
||
* verbatim in ATTR_TEXT_CODE without any inline-emphasis parsing. */
|
||
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;
|
||
} else {
|
||
kind = classify_line(p, &content_off);
|
||
}
|
||
|
||
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) {
|
||
/* Light nested-list support: any leading spaces before '-' / '*'
|
||
* / '+' render as plain spaces (preserves nesting visually);
|
||
* then a bullet + a content space. */
|
||
uint32_t q = p;
|
||
while (q + 2 < content_off && col < SCREEN_W && fb(q) == ' ') {
|
||
wrchar(col++, row, ' ', ATTR_TEXT);
|
||
q++;
|
||
}
|
||
if (col < SCREEN_W) wrchar(col++, row, 0x07, ATTR_LIST_MARKER); /* • */
|
||
if (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT);
|
||
p = content_off;
|
||
} else if (kind == LK_OLIST) {
|
||
/* Indent (if any) → spaces; digits and '.' or ')' in marker
|
||
* colour; trailing space in plain. */
|
||
uint32_t q = p;
|
||
while (q < content_off && col < SCREEN_W && fb(q) == ' ') {
|
||
wrchar(col++, row, ' ', ATTR_TEXT);
|
||
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) {
|
||
/* Indent (if any) → spaces; '│' (0xB3) prefix in marker
|
||
* colour, then a space, then content. */
|
||
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;
|
||
}
|
||
|
||
/* Effective horizontal offset: only nowrap lines honour viewport_x. */
|
||
uint8_t effective_vx = is_nowrap(line_idx) ? viewport_x : 0;
|
||
/* `cc` is the VISIBLE column inside the content area (the part
|
||
* after any fixed marker). For nowrap lines, chars at cc < viewport_x
|
||
* are skipped so the user can pan horizontally. */
|
||
uint8_t cc = 0;
|
||
char prev_ch = ' '; /* for `_` flanking check */
|
||
while (p < seg_end && col < SCREEN_W) {
|
||
char ch = fb(p);
|
||
/* Soft break (newline inside a paragraph segment) — render as a
|
||
* single space. Hard breaks are already separate segments. */
|
||
if (ch == '\n' || ch == '\r') ch = ' ';
|
||
|
||
/* --- inline emphasis markers (consumed, not rendered) ---
|
||
* Asterisk / double-asterisk / underscore are markers only
|
||
* when XOR-flanked (whitespace on exactly one side). Keeps
|
||
* COLOR_YELLOW, intraword slashes, "2 * 3", "2 ** 3" etc.
|
||
* as literal text. A marker that would conflict with the
|
||
* currently-active style is still consumed (zero-width)
|
||
* so the index_lines column count stays in sync with the
|
||
* rendered output. Skipped entirely inside fenced code
|
||
* blocks (parse_inline=0). */
|
||
if (parse_inline) {
|
||
/* `**` (bold) */
|
||
if (ch == '*' && (p + 1) < seg_end && fb(p + 1) == '*') {
|
||
char next_ch = ((p + 2) < seg_end) ? fb(p + 2) : '\n';
|
||
if (is_emph_flanked(prev_ch, next_ch)) {
|
||
if (emph == EM_NONE && ws_or_eol(prev_ch)) {
|
||
emph = EM_BOLD; cur_attr = ATTR_TEXT_BOLD;
|
||
p += 2; continue;
|
||
} else if (emph == EM_BOLD && ws_or_eol_or_delim(next_ch)) {
|
||
emph = EM_NONE; cur_attr = base_attr;
|
||
p += 2; continue;
|
||
}
|
||
}
|
||
/* literal `**` — render BOTH stars, do NOT fall
|
||
* through to single-`*` handler. */
|
||
p += 2;
|
||
if (cc >= effective_vx && col < SCREEN_W) wrchar(col++, row, '*', cur_attr);
|
||
cc++;
|
||
if (cc >= effective_vx && col < SCREEN_W) wrchar(col++, row, '*', cur_attr);
|
||
cc++;
|
||
prev_ch = '*';
|
||
continue;
|
||
}
|
||
/* `*` (italic) and `_` (underline) — same flanking rule */
|
||
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;
|
||
uint8_t active_attr = (ch == '*') ? ATTR_TEXT_ITALIC : ATTR_TEXT_UNDERSORE;
|
||
if (emph == EM_NONE && ws_or_eol(prev_ch)) {
|
||
emph = active; cur_attr = active_attr;
|
||
p++; continue;
|
||
} else if (emph != EM_NONE && ws_or_eol_or_delim(next_ch)) {
|
||
emph = EM_NONE; cur_attr = base_attr;
|
||
p++; continue;
|
||
}
|
||
}
|
||
/* intraword / both-flanked — fall through, literal */
|
||
}
|
||
if (ch == '`') {
|
||
if (emph == EM_NONE) {
|
||
emph = EM_CODE; cur_attr = ATTR_TEXT_CODE;
|
||
p++; continue;
|
||
}
|
||
if (emph == EM_CODE) {
|
||
emph = EM_NONE; cur_attr = base_attr;
|
||
p++; continue;
|
||
}
|
||
}
|
||
}
|
||
|
||
p++;
|
||
if (ch == '\t') {
|
||
/* Tab expands in CONTENT-col space (cc), each generated
|
||
* space is then conditionally rendered through hpan. */
|
||
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;
|
||
}
|
||
}
|
||
|
||
/* Right-edge truncation indicator for nowrap lines: when the screen
|
||
* ran out (col == SCREEN_W) and this row is nowrap, peek forward
|
||
* past any zero-width inline markers — if there's still real visible
|
||
* content before the end of the line, overlay the last cell with '>'. */
|
||
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; }
|
||
/* literal `**` — there IS visible content */
|
||
} else if (c == '*' || c == '_') {
|
||
char pc = (pp > left_bound) ? fb(pp - 1) : ' ';
|
||
char nc = (pp + 1 < seg_end) ? fb(pp + 1) : '\n';
|
||
if (is_emph_flanked(pc, nc)) { pp++; continue; }
|
||
}
|
||
wrchar(SCREEN_W - 1, row, '>', ATTR_TRUNC);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
/* Pad rest of the row so previous render content is wiped. For code-
|
||
* body rows use the code attribute so the block extends to the right
|
||
* edge — visually distinguishes the block from surrounding text. */
|
||
{
|
||
uint8_t pad_attr = (parse_inline ? ATTR_TEXT : ATTR_TEXT_CODE);
|
||
while (col < SCREEN_W) wrchar(col++, row, ' ', pad_attr);
|
||
}
|
||
}
|
||
|
||
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));
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
static void render_full_status(void)
|
||
{
|
||
uint16_t last = top_line + VIEW_H;
|
||
if (last > n_lines) last = n_lines;
|
||
uint8_t pct = calc_pct();
|
||
|
||
fill_row(0, ATTR_BAR);
|
||
gotoxy(1, 0);
|
||
/* "MDVIEW", then col 7 = space, col 8 = spinner slot, col 9 = space,
|
||
* col 10+ = filename. Spinner_tick draws over col 8 in ATTR_BAR. */
|
||
printf("MDVIEW %s", filename);
|
||
gotoxy(45, 0);
|
||
printf("\xB3 L %5u-%u / %u", (top_line + 1), last, n_lines);
|
||
gotoxy(70, 0);
|
||
printf("\xB3 %3u%%", pct);
|
||
}
|
||
|
||
static void render_updated_status(void)
|
||
{
|
||
uint16_t last = top_line + VIEW_H;
|
||
if (last > n_lines) last = n_lines;
|
||
uint8_t pct = calc_pct();
|
||
|
||
gotoxy(45, 0);
|
||
printf("\xB3 L %5u-%u / %u", (top_line + 1), last, n_lines);
|
||
gotoxy(70, 0);
|
||
printf("\xB3 %3u%%", pct);
|
||
}
|
||
|
||
static void render_menu(void)
|
||
{
|
||
fill_row(MENU_ROW, ATTR_MENU_T);
|
||
put_str_attr(1, MENU_ROW, "F1", ATTR_MENU_K);
|
||
put_str_attr(4, MENU_ROW, "Help", ATTR_MENU_T);
|
||
/* F2 slot removed — no wrap/unwrap toggle any more. */
|
||
put_str_attr(SCREEN_W - 10, MENU_ROW, "F10", ATTR_MENU_K);
|
||
put_str_attr(SCREEN_W - 5, MENU_ROW, "Exit", ATTR_MENU_T);
|
||
}
|
||
|
||
/* ==================================================================
|
||
* scrolling
|
||
* ================================================================== */
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
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();
|
||
}
|
||
}
|
||
}
|
||
|
||
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();
|
||
}
|
||
}
|
||
}
|
||
|
||
/* Horizontal pan (only when viewport contains nowrap lines). */
|
||
static void scroll_h(int8_t delta)
|
||
{
|
||
uint8_t any_nowrap = 0;
|
||
for (uint8_t i = 0; i < VIEW_H; i++) {
|
||
if (is_nowrap((uint16_t)(top_line + i))) { any_nowrap = 1; break; }
|
||
}
|
||
if (!any_nowrap) return;
|
||
int16_t nx = (int16_t)viewport_x + delta;
|
||
if (nx < 0) nx = 0;
|
||
if (nx > 240) nx = 240;
|
||
if ((uint8_t)nx != viewport_x) {
|
||
viewport_x = (uint8_t)nx;
|
||
render_viewport();
|
||
}
|
||
}
|
||
|
||
/* Toggle wrap/truncate and rebuild the seg index. Tries to preserve
|
||
* the user's scroll position by remembering the byte offset of the
|
||
* current top seg and finding the seg that contains that offset after
|
||
* the rebuild. Also resets horizontal pan — wrap mode doesn't use it. */
|
||
static void toggle_wrap(void)
|
||
{
|
||
/* Removed — no longer needed. Kept as stub to avoid
|
||
* linker warnings if something still calls it. */
|
||
(void)0;
|
||
}
|
||
|
||
/* ==================================================================
|
||
* help screen
|
||
* ================================================================== */
|
||
|
||
static const char * const help_lines[] = {
|
||
"",
|
||
" MDVIEW v0.1 - Markdown text viewer for Sprinter",
|
||
" =================================================",
|
||
"",
|
||
" Navigation:",
|
||
" Up / Down - scroll one line",
|
||
" PgUp / PgDn - scroll one page",
|
||
" Home - go to top of document",
|
||
" End - go to bottom of document",
|
||
" Left / Right - pan horizontally (nowrap lines only)",
|
||
"",
|
||
" Other:",
|
||
" F1 - show this help",
|
||
" F2 - (reserved)",
|
||
" F10 / Esc - exit",
|
||
"",
|
||
" Press any key to return...",
|
||
(const char *)0
|
||
};
|
||
|
||
static void help_screen(void)
|
||
{
|
||
clrscr_attr(ATTR_TEXT);
|
||
for (uint8_t i = 0; help_lines[i]; i++) {
|
||
put_str_attr(0, (uint8_t)(i + 1), help_lines[i], ATTR_TEXT);
|
||
}
|
||
(void)getkey();
|
||
clrscr_attr(ATTR_TEXT);
|
||
render_menu();
|
||
render_full_status();
|
||
render_viewport();
|
||
}
|
||
|
||
/* ==================================================================
|
||
* error
|
||
* ================================================================== */
|
||
|
||
static void die(const char *msg)
|
||
{
|
||
clrscr_attr(ATTR_TEXT);
|
||
puts(msg);
|
||
puts("Press any key to exit...");
|
||
(void)getkey();
|
||
}
|
||
|
||
/* ==================================================================
|
||
* main
|
||
* ================================================================== */
|
||
|
||
int main(int argc, char **argv)
|
||
{
|
||
const char *path;
|
||
if (argc >= 2) path = argv[1];
|
||
else path = "SAMPLE.MD";
|
||
|
||
/* Copy path → static filename[] for the status bar. */
|
||
{
|
||
uint8_t i = 0;
|
||
while (i < (uint8_t)(sizeof(filename) - 1) && path[i]) {
|
||
filename[i] = path[i];
|
||
i++;
|
||
}
|
||
filename[i] = 0;
|
||
}
|
||
|
||
/* Bring the UI up FIRST so the user sees the menu + title bar (with
|
||
* a spinner) immediately, instead of a black screen while we read
|
||
* the file and index lines. */
|
||
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_TEXT);
|
||
puts("mdview: cannot load file");
|
||
puts(path);
|
||
switch (rc) {
|
||
case -1: puts("(open failed)"); break;
|
||
case -2: puts("(size > 128K or seek failed)"); break;
|
||
case -3: puts("(mem_alloc_pages failed)"); break;
|
||
case -4: puts("(short read)"); break;
|
||
case -5: puts("(index alloc failed)"); break;
|
||
}
|
||
puts("Press any key to exit...");
|
||
(void)getkey();
|
||
return 1;
|
||
}
|
||
index_lines();
|
||
spinner_show(0);
|
||
if (n_lines == 0) {
|
||
die("mdview: empty file");
|
||
unload_file();
|
||
return 1;
|
||
}
|
||
|
||
render_full_status();
|
||
render_viewport();
|
||
|
||
for (;;) {
|
||
uint16_t k = getkey();
|
||
uint8_t ascii = (uint8_t)(k & 0xFF);
|
||
uint8_t scan = (uint8_t)((k >> 8) & 0x7F);
|
||
|
||
if (ascii) {
|
||
if (ascii == 0x1B) break; /* Esc */
|
||
continue;
|
||
}
|
||
switch (scan) {
|
||
case KEY_F10: goto exit_loop;
|
||
case KEY_UP: scroll_up(1); break;
|
||
case KEY_DOWN: scroll_down(1); break;
|
||
case KEY_LEFT: scroll_h(-(int8_t)HPAN_STEP); break;
|
||
case KEY_RIGHT: scroll_h(+(int8_t)HPAN_STEP); break;
|
||
case KEY_PGUP: scroll_up(VIEW_H); break;
|
||
case KEY_PGDN: scroll_down(VIEW_H); break;
|
||
case KEY_HOME: top_line = 0;
|
||
viewport_x = 0;
|
||
render_viewport(); break;
|
||
case KEY_END: top_line = (n_lines > VIEW_H)
|
||
? (uint16_t)(n_lines - VIEW_H) : 0;
|
||
viewport_x = 0;
|
||
render_viewport(); break;
|
||
default: continue;
|
||
}
|
||
render_updated_status();
|
||
}
|
||
exit_loop:
|
||
unload_file();
|
||
pal_reset(PAL_CGA);
|
||
clrscr_attr(ATTR_TEXT);
|
||
return 0;
|
||
}
|