/* * 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 #include #include #include #include #include #include #include #include /* ---- 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; }