Files
Sprinter-SDCC/examples/mdview/mdview.c
T
snark13 035d93ab51 mdview: fix fenced code block emphasis leak and carry emphasis across wrap lines
- Add fenced code block tracking to the wrap-pass in index_lines().
  line_style is reset to PLAIN at every ``` delimiter, and all segs
  inside a fenced block get init_style=PLAIN. This prevents emphasis
  markers (e.g. _ in __var) inside code blocks from leaking into later
  normal text.
- Also carry init_style across wrap continuation segs so that a long
  bold/italic line that is wrapped continues with the correct style on
  the next segment.
- The fence bitmap pass now only updates in_code[], since init_style is
  already set correctly by the wrap pass.
2026-06-06 12:01:15 +03:00

1268 lines
47 KiB
C
Raw 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 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.
*
* line_offset[] semantics depend on wrap_mode:
* wrap_mode == 0 (truncate): one entry per logical line, value = byte
* offset inside the logical file.
* wrap_mode == 1 (wrap): one entry per VISIBLE viewport row. The
* continuation flag for wrap segments lives
* in the separate cont_flag[] bitmap.
*/
/* 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. */
uint32_t line_offset[MAX_LINES]; // = {0};
/* Bitmap: seg i is a continuation of the previous logical line — rendered
* as plain text without markdown classification. */
uint8_t cont_flag[MAX_LINES / 8]; // = {0};
/* Bitmap: seg i (non-CONT) is inside a fenced code block (between two
* ``` lines). The fence delimiter lines themselves are NOT marked here —
* they are detected dynamically via a 3-byte prefix check. */
uint8_t in_code[MAX_LINES / 8]; // = {0};
uint8_t init_style[MAX_LINES / 4]; // = {0};
static uint16_t n_lines = 0;
static uint16_t top_line = 0;
static uint32_t file_size = 0;
static uint8_t file_blk = 0;
static uint8_t cur_page = 0xFF; /* 0xFF = no page mapped yet */
static uint8_t wrap_mode = 1; /* default: wrap on */
static uint8_t viewport_x = 0; /* horizontal pan (truncate mode only) */
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 void map_page(uint8_t page)
{
if (page != cur_page) {
sprinter_page_w3(mem_get_page(file_blk, 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. */
static char fb(uint32_t p)
{
map_page((uint8_t)(p >> PAGE_BITS));
return FILE_BUF[(uint16_t)(p & PAGE_MASK)];
}
/* 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-flag bitmap helpers (see cont_flag[] above). */
static uint8_t is_cont(uint16_t idx)
{
return (uint8_t)((cont_flag[idx >> 3] >> (idx & 7)) & 1u);
}
static void set_cont(uint16_t idx)
{
cont_flag[idx >> 3] |= (uint8_t)(1u << (idx & 7));
}
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). */
uint8_t pages_needed = (uint8_t)((file_size + PAGE_SIZE - 1u) / PAGE_SIZE);
if (pages_needed == 0) pages_needed = 1;
file_blk = mem_alloc_pages(pages_needed);
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 < pages_needed; page++) {
spinner_tick();
uint8_t phys = mem_get_page(file_blk, page);
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);
return 0;
}
static void unload_file(void)
{
if (file_blk) {
mem_free_block(file_blk);
file_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 = line_offset[idx];
if (off + 2 >= file_size) return 0;
return (uint8_t)(fb(off) == '`' &&
fb(off + 1) == '`' &&
fb(off + 2) == '`');
}
static uint8_t is_code_body(uint16_t idx)
{
if (idx >= n_lines) return 0;
return (uint8_t)((in_code[idx >> 3] >> (idx & 7)) & 1u);
}
#define INIT_STYLE_PLAIN 0x0
#define INIT_STYLE_BOLD 0x1
#define INIT_STYLE_ITALIC 0x2
#define INIT_STYLE_UNDER 0x3
static const uint8_t styles_map[] = {
ATTR_TEXT, ATTR_TEXT_BOLD, ATTR_TEXT_ITALIC, ATTR_TEXT_UNDERSORE
};
static uint8_t get_init_style(uint16_t idx)
{
if (idx >= n_lines) return styles_map[INIT_STYLE_PLAIN];
return styles_map[(uint8_t)((init_style[idx >> 2] >> ((idx & 3) << 1)) & 3u)];
}
static uint8_t get_init_style_raw(uint16_t idx)
{
if (idx >= n_lines) return INIT_STYLE_PLAIN;
return (uint8_t)((init_style[idx >> 2] >> ((idx & 3) << 1)) & 3u);
}
static void set_init_style_raw(uint16_t idx, uint8_t style)
{
init_style[idx >> 2] = (init_style[idx >> 2] & ~(3u << ((idx & 3) << 1))) | ((style & 3u) << ((idx & 3) << 1));
}
/* 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) */
}
}
static void index_lines(void)
{
uint8_t line_style = INIT_STYLE_PLAIN;
n_lines = 0;
memset(in_code, 0, sizeof(in_code));
memset(cont_flag, 0, sizeof(cont_flag));
memset(init_style, 0, sizeof(init_style));
if (file_size == 0) return;
if (!wrap_mode) {
/* Truncate: one entry per logical line. */
line_offset[n_lines++] = 0;
for (uint32_t p = 0; p < file_size; p++) {
if (fb(p) == '\n' &&
p + 1 < file_size && n_lines < MAX_LINES) {
line_offset[n_lines++] = p + 1;
if ((n_lines & 15) == 0) spinner_tick();
}
}
} else {
/* Wrap: walk logical lines, split each into one or more segs. */
uint32_t p = 0;
uint8_t in_block = 0; /* inside fenced code block? */
line_style = INIT_STYLE_PLAIN;
while (p < file_size && n_lines < MAX_LINES) {
if ((n_lines & 15) == 0) spinner_tick();
/* Find end of logical line (\n or EOF). */
uint32_t line_end = p;
while (line_end < file_size && fb(line_end) != '\n') line_end++;
/* Detect fenced-code delimiter (``` at col 0). Toggle in_block
* and reset emphasis tracking — code blocks never carry inline
* style across their boundaries. */
if (p + 2 < file_size &&
fb(p) == '`' && fb(p + 1) == '`' && fb(p + 2) == '`') {
in_block = (uint8_t)!in_block;
line_style = INIT_STYLE_PLAIN;
}
/* Emit the first seg of this logical line (no CONT). */
set_init_style_raw(n_lines, in_block ? INIT_STYLE_PLAIN : line_style);
line_offset[n_lines++] = p;
if (!is_nowrap_line(p)) {
/* Determine marker prefix and starting visible column. */
uint32_t content_off = p;
uint8_t kind = classify_line(p, &content_off);
uint8_t col = marker_visible_col(kind, p, content_off);
uint32_t q;
if (kind == LK_H1 || kind == LK_H2 || kind == LK_H3 || kind == LK_H4 ||
kind == LK_ULIST || kind == LK_OLIST || kind == LK_QUOTE) {
q = content_off;
} else {
q = p;
}
uint32_t last_space = 0xFFFFFFFFu; /* byte pos AFTER last space */
char prev_ch = ' '; /* for emphasis flanking */
while (q < line_end && n_lines < MAX_LINES) {
char ch = fb(q);
/* Inside fenced code block: every byte is literal, no
* emphasis markers, but wrap still applies. */
if (in_block) {
if (ch == '\t') {
uint8_t tgt = (uint8_t)((col & (uint8_t)~(TAB_STOP - 1)) + TAB_STOP);
if (tgt > SCREEN_W) tgt = SCREEN_W;
col = tgt;
q++;
prev_ch = ' ';
} else {
if (ch == ' ') last_space = q + 1;
col++;
q++;
prev_ch = ch;
}
if (col >= SCREEN_W && q < line_end) {
uint32_t wrap_at = (last_space != 0xFFFFFFFFu &&
last_space > line_offset[n_lines - 1])
? last_space : q;
if (wrap_at >= line_end) break;
line_offset[n_lines] = wrap_at;
set_init_style_raw(n_lines, INIT_STYLE_PLAIN);
set_cont(n_lines);
n_lines++;
col = 0;
last_space = 0xFFFFFFFFu;
prev_ch = ' ';
q = wrap_at;
}
continue;
}
/* Backtick — always zero-width (inline code delimiter). */
if (ch == '`') {
q++;
continue;
}
/* `**` — zero-width only when XOR-flanked; otherwise
* both stars render as two literal columns. */
if (ch == '*' && (q + 1) < line_end && fb(q + 1) == '*') {
char next_ch = ((q + 2) < line_end) ? fb(q + 2) : '\n';
prev_ch = fb(q - 1);
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;
}
}
col += 2;
q += 2;
prev_ch = '*';
}
/* Single `*` / `_` — zero-width only when XOR-flanked. */
else if (ch == '*' || ch == '_') {
prev_ch = fb(q - 1);
char next_ch = ((q + 1) < line_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;
}
}
col++;
q++;
prev_ch = ch;
}
else if (ch == '\t') {
uint8_t tgt = (uint8_t)((col & (uint8_t)~(TAB_STOP - 1)) + TAB_STOP);
if (tgt > SCREEN_W) tgt = SCREEN_W;
col = tgt;
q++;
prev_ch = ' ';
} else {
if (ch == ' ') last_space = q + 1;
col++;
q++;
prev_ch = ch;
}
if (col >= SCREEN_W && q < line_end) {
uint32_t wrap_at = (last_space != 0xFFFFFFFFu &&
last_space > line_offset[n_lines - 1])
? last_space : q;
if (wrap_at >= line_end) break;
line_offset[n_lines] = wrap_at;
set_init_style_raw(n_lines, line_style); /* carry style across wrap */
set_cont(n_lines);
n_lines++;
col = 0;
last_space = 0xFFFFFFFFu;
prev_ch = ' ';
q = wrap_at;
}
}
}
p = (line_end < file_size) ? (line_end + 1) : line_end;
}
}
/* Fence/code-body bitmap pass — only non-CONT segs participate.
* The wrap-pass above already tracked fenced blocks and set init_style
* to PLAIN for every seg inside; this pass only fixes the in_code[]
* bitmap (used by render_line to disable inline parsing). */
{
uint8_t in_block = 0;
for (uint16_t i = 0; i < n_lines; i++) {
if (is_cont(i)) {
if (is_code_body(i - 1)) {
in_code[i >> 3] |= (uint8_t)(1u << (i & 7));
set_init_style_raw(i, INIT_STYLE_PLAIN);
}
continue;
}
if (is_fence_delim(i)) {
in_block = (uint8_t)!in_block;
set_init_style_raw(i, INIT_STYLE_PLAIN);
} else if (in_block) {
in_code[i >> 3] |= (uint8_t)(1u << (i & 7));
set_init_style_raw(i, INIT_STYLE_PLAIN);
}
}
}
}
/* ==================================================================
* rendering
* ================================================================== */
/* Inline emphasis tracking (one style active at a time, no nesting). */
#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 */
uint8_t x = wherex();
uint8_t y = wherey();
uint16_t from = 342;
if(line_idx >= from && line_idx <= from + 3) {
gotoxy(18 + (line_idx - from) * 9, 31); dec16(line_idx);
gotoxy(21 + (line_idx - from) * 9, 31); hex8(get_init_style_raw(line_idx));
gotoxy(23 + (line_idx - from) * 9, 31); hex8(is_code_body(line_idx));
}
gotoxy(61, 31); dec16(line_idx);
gotoxy(64, 31); hex8(get_init_style_raw(line_idx));
gotoxy(66, 31); hex8(is_code_body(line_idx));
gotoxy(x, y);
if (line_idx < n_lines) {
uint32_t p = line_offset[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 = (line_idx + 1 < n_lines)
? line_offset[line_idx + 1]
: file_size;
if (seg_end > file_size) seg_end = file_size;
uint8_t kind;
/* Continuation seg → render plain text from the seg's offset up to
* either the next seg's offset or end-of-line. No markdown
* classification, no marker prefixes, inline markers stay literal
* (we drop them silently so the visible output matches the wrap
* algorithm's column count). */
if (cont) {
// TODO -
// Для cont можно сделать такое -
// 1) если это продолжение quote - то отрисовать префикc QUOTE и дальше продолжить вывод
// 2) если это продолжение пункта list - то посмотреть какой отступ был, повторить его же и продолжить вывод
// 3) для обычного текста - продолжить вывод
}
/* 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;
}
/* `cc` is the VISIBLE column inside the content area (the part
* after any fixed marker). When viewport_x > 0 (only possible in
* truncate mode), chars at cc < viewport_x are skipped silently
* so the user can pan past the marker. The marker itself was
* already rendered above at fixed screen cols 0..(col-1). */
uint8_t cc = 0;
char prev_ch = ' '; /* for `_` flanking check */
while (p < seg_end && col < SCREEN_W) {
char ch = fb(p);
if (ch == '\n' || ch == '\r') break;
/* --- 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 >= viewport_x && col < SCREEN_W) wrchar(col++, row, '*', cur_attr);
cc++;
if (cc >= viewport_x && 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 >= viewport_x) wrchar(col++, row, ' ', cur_attr);
cc++;
}
prev_ch = ' ';
} else {
if (cc >= viewport_x && col < SCREEN_W) {
wrchar(col++, row, ch, cur_attr);
}
cc++;
prev_ch = ch;
}
}
/* Right-edge truncation indicator: when in truncate mode and the
* main loop stopped because the screen ran out (col == SCREEN_W),
* peek forward past any zero-width inline markers — if there's
* still real visible content before the end of the line, overlay
* the last screen cell with a bright '>'. */
if (!wrap_mode && col >= SCREEN_W) {
uint32_t pp = p;
uint32_t left_bound = line_offset[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);
put_str_attr(10, MENU_ROW, "F2", ATTR_MENU_K);
/* Label reflects what F2 will DO: when wrap is on, F2 turns it off. */
put_str_attr(13, MENU_ROW, wrap_mode ? "Unwrap" : "Wrap ", ATTR_MENU_T);
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 (truncate mode only). Each step is HPAN_STEP visible
* columns; clamped to [0, 240] — enough for any real-world long line
* within a 16 KB file. */
static void scroll_h(int8_t delta)
{
if (wrap_mode) 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)
{
uint32_t top_off = (top_line < n_lines) ? line_offset[top_line] : 0;
wrap_mode = wrap_mode ? 0u : 1u;
viewport_x = 0;
spinner_show(1);
spinner_tick();
index_lines();
spinner_show(0);
/* Largest i such that the i-th seg's offset is ≤ top_off. */
uint16_t i = 0;
while ((uint16_t)(i + 1) < n_lines &&
line_offset[i + 1] <= top_off) {
i++;
}
top_line = i;
clamp_top();
render_menu();
render_full_status();
render_viewport();
}
/* ==================================================================
* 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 (Unwrap mode only)",
"",
" Other:",
" F1 - show this help",
" F2 - toggle wrap / unwrap of long lines",
" 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)
{
memset(line_offset, 0, sizeof(line_offset));
memset(cont_flag, 0, sizeof(cont_flag));
memset(in_code, 0, sizeof(in_code));
memset(init_style, 0, sizeof(init_style));
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;
}
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_F1: help_screen(); break;
case KEY_F2: toggle_wrap(); break;
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;
}