diff --git a/examples/mdview/mdview.c b/examples/mdview/mdview.c index 57f5231..08b3950 100644 --- a/examples/mdview/mdview.c +++ b/examples/mdview/mdview.c @@ -109,12 +109,10 @@ static const uint8_t md_pallete[16 * 4] = { * 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. + * line_offset[]: one entry per VISIBLE viewport row. Segments created by + * word-wrap store the byte offset of the wrapped segment; continuation + * flag is in cont_flag[]. nowrap_flag[] marks code block / HR / table + * rows that do NOT wrap and support horizontal scrolling. */ /* NOTE: explicit `= 0` initialisers are MANDATORY on SDCC z80. @@ -122,23 +120,28 @@ static const uint8_t md_pallete[16 * 4] = { * 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. */ +/* Bitmap: seg i is a continuation of the previous logical line. */ 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. */ +/* Bitmap: seg i (non-CONT) is inside a fenced code block. */ uint8_t in_code[MAX_LINES / 8]; // = {0}; +/* Bitmap: seg i should NOT be wrapped (code block, HR, table). */ +uint8_t nowrap_flag[MAX_LINES / 8]; // = {0}; +/* Bitmap: seg i is a blank (empty visual line). */ +uint8_t blank_flag[MAX_LINES / 8]; // = {0}; +/* 2-bit continuation kind per seg (4 lines/byte): 0=PLAIN, 1=QUOTE, 2=LIST, 3=OTHER. + * Used by render_line() to repeat prefix/indent on wrapped continuation rows. */ +uint8_t line_kind[MAX_LINES / 4]; // = {0}; -uint8_t init_style[MAX_LINES / 4]; // = {0}; +uint8_t init_style[MAX_LINES]; // = {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 file_pages = 0; +static uint8_t file_phys[MAX_PAGES] = {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 uint8_t viewport_x = 0; /* horizontal pan (for nowrap lines) */ static char filename[64] = {0}; #define HPAN_STEP 8u @@ -197,18 +200,26 @@ static void spinner_show(uint8_t on) static void map_page(uint8_t page) { if (page != cur_page) { - sprinter_page_w3(mem_get_page(file_blk, 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. */ + * 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 char fb(uint32_t p) { - map_page((uint8_t)(p >> PAGE_BITS)); - return FILE_BUF[(uint16_t)(p & PAGE_MASK)]; + 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. */ @@ -244,6 +255,43 @@ static void set_cont(uint16_t idx) cont_flag[idx >> 3] |= (uint8_t)(1u << (idx & 7)); } +static uint8_t is_nowrap(uint16_t idx) +{ + return (uint8_t)((nowrap_flag[idx >> 3] >> (idx & 7)) & 1u); +} + +static void set_nowrap(uint16_t idx) +{ + nowrap_flag[idx >> 3] |= (uint8_t)(1u << (idx & 7)); +} + +static uint8_t is_blank(uint16_t idx) +{ + return (uint8_t)((blank_flag[idx >> 3] >> (idx & 7)) & 1u); +} + +static void set_blank(uint16_t idx) +{ + blank_flag[idx >> 3] |= (uint8_t)(1u << (idx & 7)); +} + +/* Continuation kind: 0=PLAIN, 1=QUOTE, 2=LIST (ULIST/OLIST), 3=OTHER (HR/CODE/TABLE). */ +#define CK_PLAIN 0 +#define CK_QUOTE 1 +#define CK_LIST 2 +#define CK_OTHER 3 + +static uint8_t get_ckind(uint16_t idx) +{ + return (uint8_t)((line_kind[idx >> 2] >> ((idx & 3) << 1)) & 3u); +} + +static void set_ckind(uint16_t idx, uint8_t kind) +{ + uint8_t shift = (uint8_t)((idx & 3) << 1); + line_kind[idx >> 2] = (uint8_t)((line_kind[idx >> 2] & ~(3u << shift)) | ((kind & 3u) << shift)); +} + static void set_pallete(void) { uint8_t buff[64]; for(int i = 0; i < 16; i++) { @@ -284,9 +332,9 @@ static int load_file(const char *path) 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); + 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; @@ -296,9 +344,10 @@ static int load_file(const char *path) * `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++) { + 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) @@ -351,26 +400,27 @@ static uint8_t is_code_body(uint16_t idx) #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, ATTR_TEXT_BOLD, ATTR_TEXT_ITALIC, ATTR_TEXT_UNDERSORE, ATTR_TEXT_CODE }; 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)]; + return styles_map[init_style[idx] & 7u]; } 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); + return init_style[idx] & 7u; } 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)); + if (idx < MAX_LINES) init_style[idx] = style & 7u; } /* Returns 1 iff the logical line at file offset `p_start` should NOT be @@ -529,206 +579,347 @@ static uint8_t marker_visible_col(uint8_t kind, uint32_t p_start, uint32_t conte } } +/* Emit a new segment entry. */ +static void emit_seg(uint32_t off, uint8_t style, uint8_t ckind, uint8_t cont) +{ + if (n_lines >= MAX_LINES) return; + line_offset[n_lines] = off; + set_init_style_raw(n_lines, style); + set_ckind(n_lines, ckind); + if (cont) set_cont(n_lines); + 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 > line_offset[n_lines - 1]) + ? last_space : q; + if (wrap_at >= q_end || wrap_at == line_offset[n_lines - 1]) 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 line_style = INIT_STYLE_PLAIN; + uint8_t in_block = 0; n_lines = 0; memset(in_code, 0, sizeof(in_code)); memset(cont_flag, 0, sizeof(cont_flag)); + memset(nowrap_flag, 0, sizeof(nowrap_flag)); + memset(blank_flag, 0, sizeof(blank_flag)); + memset(line_kind, 0, sizeof(line_kind)); 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(); + 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 || line_offset[n_lines - 1] != p) { + emit_seg(p, INIT_STYLE_PLAIN, CK_PLAIN, 0); + set_blank(n_lines - 1); } + continue; } - } 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; + + /* ---- 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(n_lines - 1); + 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(n_lines - 1); + in_code[(n_lines - 1) >> 3] |= (uint8_t)(1u << ((n_lines - 1) & 7)); + 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(n_lines - 1); + 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; + } - /* 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; - } + /* ---- 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; + } - /* 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; + /* ---- 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; + } - 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; + /* ---- 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 { - q = p; + seg_col++; + if (!soft_break) q++; else q++; + prev_ch = ch; } - - 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; - } + if (seg_col >= SCREEN_W) { + uint32_t wrap_at = (last_space != 0xFFFFFFFFu && last_space > line_offset[n_lines - 1]) + ? last_space : q; + if (wrap_at >= file_size || wrap_at == line_offset[n_lines - 1]) break; + emit_seg(wrap_at, line_style, CK_PLAIN, 1); + seg_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); + 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; } } } - } - -/* ================================================================== - * rendering - * ================================================================== */ - -/* Inline emphasis tracking (one style active at a time, no nesting). */ #define EM_NONE 0 #define EM_BOLD 1 #define EM_ITALIC 2 @@ -743,18 +934,10 @@ static void render_line(uint16_t line_idx, uint8_t row) 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)); + if (is_blank(line_idx)) { + while (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT); + return; } - 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]; @@ -767,17 +950,34 @@ static void render_line(uint16_t line_idx, uint8_t row) 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). */ + /* Continuation seg → repeat prefix/indent from the first non-CONT + * segment of this logical line, then render plain text. */ if (cont) { - // TODO - - // Для cont можно сделать такое - - // 1) если это продолжение quote - то отрисовать префикc QUOTE и дальше продолжить вывод - // 2) если это продолжение пункта list - то посмотреть какой отступ был, повторить его же и продолжить вывод - // 3) для обычного текста - продолжить вывод + uint16_t first = line_idx; + while (first > 0 && is_cont((uint16_t)(first - 1))) first--; + uint32_t p_start = line_offset[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 = line_offset[line_idx]; + content_off = p; + /* For cont segs, inline parsing continues normally. */ } /* Fenced code block: delimiter line → blank row; body lines render @@ -847,16 +1047,18 @@ static void render_line(uint16_t line_idx, uint8_t row) 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). 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). */ + * 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); - if (ch == '\n' || ch == '\r') break; + /* 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 @@ -883,9 +1085,9 @@ static void render_line(uint16_t line_idx, uint8_t row) /* 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); + if (cc >= effective_vx && col < SCREEN_W) wrchar(col++, row, '*', cur_attr); cc++; - if (cc >= viewport_x && col < SCREEN_W) wrchar(col++, row, '*', cur_attr); + if (cc >= effective_vx && col < SCREEN_W) wrchar(col++, row, '*', cur_attr); cc++; prev_ch = '*'; continue; @@ -924,12 +1126,12 @@ static void render_line(uint16_t line_idx, uint8_t row) * 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); + if (cc >= effective_vx) wrchar(col++, row, ' ', cur_attr); cc++; } prev_ch = ' '; } else { - if (cc >= viewport_x && col < SCREEN_W) { + if (cc >= effective_vx && col < SCREEN_W) { wrchar(col++, row, ch, cur_attr); } cc++; @@ -937,12 +1139,11 @@ static void render_line(uint16_t line_idx, uint8_t row) } } - /* 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) { + /* 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 = line_offset[line_idx]; while (pp < seg_end) { @@ -1024,9 +1225,7 @@ 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); + /* 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); } @@ -1074,12 +1273,14 @@ static void scroll_down(uint16_t n) } } -/* 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. */ +/* Horizontal pan (only when viewport contains nowrap lines). */ static void scroll_h(int8_t delta) { - if (wrap_mode) return; + 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; @@ -1095,26 +1296,9 @@ static void scroll_h(int8_t delta) * 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(); + /* Removed — no longer needed. Kept as stub to avoid + * linker warnings if something still calls it. */ + (void)0; } /* ================================================================== @@ -1131,11 +1315,11 @@ static const char * const help_lines[] = { " PgUp / PgDn - scroll one page", " Home - go to top of document", " End - go to bottom of document", - " Left / Right - pan horizontally (Unwrap mode only)", + " Left / Right - pan horizontally (nowrap lines only)", "", " Other:", " F1 - show this help", - " F2 - toggle wrap / unwrap of long lines", + " F2 - (reserved)", " F10 / Esc - exit", "", " Press any key to return...", @@ -1176,6 +1360,9 @@ 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(nowrap_flag, 0, sizeof(nowrap_flag)); + memset(blank_flag, 0, sizeof(blank_flag)); + memset(line_kind, 0, sizeof(line_kind)); memset(init_style, 0, sizeof(init_style)); const char *path; @@ -1240,8 +1427,6 @@ int main(int argc, char **argv) } 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;