From 47c9cd326a23f4b5a434f10d33a0fec7411383e8 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Sun, 7 Jun 2026 11:55:19 +0300 Subject: [PATCH] mdview: wide-break '\\', strikethrough, horizontal scroll bounds + '<' - A trailing backslash before a newline now forces an in-paragraph line break (like two trailing spaces); render consumes the marker (non-code). - Add ~~strikethrough~~ inline style (INIT_STYLE_STRIKE / EM_STRIKE), parsed in inline_scan, the paragraph merger and render, mirroring **. - Horizontal pan is bounded by the widest nowrap segment on screen, and a '<' indicator marks hidden content off the left edge. Co-Authored-By: Oz --- examples/mdview/mdview.c | 109 +++++++++++++++++++++++++++++++++++---- 1 file changed, 98 insertions(+), 11 deletions(-) diff --git a/examples/mdview/mdview.c b/examples/mdview/mdview.c index a01bed9..2af0b3d 100644 --- a/examples/mdview/mdview.c +++ b/examples/mdview/mdview.c @@ -93,6 +93,7 @@ static const uint8_t md_pallete[16 * 4] = { #define ATTR_TEXT_ITALIC COLOR(COLOR_LIGHTGREEN, COLOR_BLUE) #define ATTR_TEXT_UNDERSORE COLOR(COLOR_LIGHTMAGENTA, COLOR_BLUE) #define ATTR_TEXT_CODE COLOR(COLOR_WHITE, COLOR_BLUE) +#define ATTR_TEXT_STRIKE COLOR(COLOR_DARKGRAY, COLOR_BLUE) #define ATTR_LIST_MARKER COLOR(COLOR_LIGHTCYAN, COLOR_BLUE) #define ATTR_QUOTE_MARKER COLOR(COLOR_CYAN, COLOR_BLUE) #define ATTR_HR COLOR(COLOR_LIGHTGRAY, COLOR_BLUE) @@ -435,9 +436,11 @@ static uint8_t is_fence_delim(uint16_t idx) #define INIT_STYLE_ITALIC 0x2 #define INIT_STYLE_UNDER 0x3 #define INIT_STYLE_CODE 0x4 +#define INIT_STYLE_STRIKE 0x5 +/* Indexed by init_style & 7; order must match INIT_STYLE_* / EM_* below. */ static const uint8_t styles_map[] = { - ATTR_TEXT, ATTR_TEXT_BOLD, ATTR_TEXT_ITALIC, ATTR_TEXT_UNDERSORE, ATTR_TEXT_CODE + ATTR_TEXT, ATTR_TEXT_BOLD, ATTR_TEXT_ITALIC, ATTR_TEXT_UNDERSORE, ATTR_TEXT_CODE, ATTR_TEXT_STRIKE }; static uint8_t get_init_style(uint16_t idx) @@ -705,6 +708,17 @@ static uint8_t inline_scan(uint32_t q, uint32_t q_end, uint8_t col, } seg_col += 2; q += 2; prev_ch = '*'; continue; } + if (ch == '~' && (q + 1) < q_end && fb(q + 1) == '~') { + char next_ch = ((q + 2) < q_end) ? fb(q + 2) : '\n'; + if (is_emph_flanked(prev_ch, next_ch)) { + if (line_style == INIT_STYLE_PLAIN && ws_or_eol(prev_ch)) { + line_style = INIT_STYLE_STRIKE; q += 2; continue; + } else if (line_style == INIT_STYLE_STRIKE && ws_or_eol_or_delim(next_ch)) { + line_style = INIT_STYLE_PLAIN; q += 2; continue; + } + } + seg_col += 2; q += 2; prev_ch = '~'; continue; + } if (ch == '*' || ch == '_') { char next_ch = ((q + 1) < q_end) ? fb(q + 1) : '\n'; if (is_emph_flanked(prev_ch, next_ch)) { @@ -854,13 +868,19 @@ static void index_lines(void) if (classify_line(next, &dummy) != LK_PLAIN) break; if (is_line_blank(next)) break; - /* Hard break? */ + /* Wide break? Two trailing spaces/tabs OR a trailing + * backslash before the newline force a line break inside + * the paragraph (the inline style is preserved). */ uint8_t is_hard = 0; - if (q >= p + 2) { + if (q >= p + 1) { char b1 = fb(q - 1); - char b2 = fb(q - 2); - if ((b1 == ' ' || b1 == '\t') && (b2 == ' ' || b2 == '\t')) + if (b1 == '\\') { is_hard = 1; + } else if (q >= p + 2) { + char b2 = fb(q - 2); + if ((b1 == ' ' || b1 == '\t') && (b2 == ' ' || b2 == '\t')) + is_hard = 1; + } } if (is_hard) { @@ -899,6 +919,23 @@ static void index_lines(void) if (!soft_break) q += 2; else q++; prev_ch = '*'; continue; } + if (ch == '~' && (q + 1) < file_size && fb(q + 1) == '~') { + char next_ch = ((q + 2) < file_size) ? fb(q + 2) : '\n'; + if (is_emph_flanked(prev_ch, next_ch)) { + if (line_style == INIT_STYLE_PLAIN && ws_or_eol(prev_ch)) { + line_style = INIT_STYLE_STRIKE; + if (!soft_break) q += 2; else q++; + continue; + } else if (line_style == INIT_STYLE_STRIKE && ws_or_eol_or_delim(next_ch)) { + line_style = INIT_STYLE_PLAIN; + if (!soft_break) q += 2; else q++; + continue; + } + } + seg_col += 2; + if (!soft_break) q += 2; else q++; + prev_ch = '~'; continue; + } if (ch == '*' || ch == '_') { char next_ch = ((q + 1) < file_size) ? fb(q + 1) : '\n'; if (is_emph_flanked(prev_ch, next_ch)) { @@ -960,6 +997,7 @@ static void index_lines(void) #define EM_ITALIC 2 #define EM_UNDER 3 #define EM_CODE 4 +#define EM_STRIKE 5 static void render_line(uint16_t line_idx, uint8_t row) { @@ -1089,6 +1127,13 @@ static void render_line(uint16_t line_idx, uint8_t row) char prev_ch = ' '; /* for `_` flanking check */ while (p < seg_end && col < SCREEN_W) { char ch = fb(p); + /* A backslash immediately before this segment's end-of-line is a + * wide-break marker (index_lines split here) — consume it without + * drawing. Not applied to verbatim code bodies. */ + if (parse_inline && ch == '\\' && (p + 1) < seg_end) { + char nb = fb(p + 1); + if (nb == '\n' || nb == '\r') { p++; continue; } + } /* Soft break (newline inside a paragraph segment) — render as a * single space. Hard breaks are already separate segments. */ if (ch == '\n' || ch == '\r') ch = ' '; @@ -1125,6 +1170,27 @@ static void render_line(uint16_t line_idx, uint8_t row) prev_ch = '*'; continue; } + /* `~~` (strikethrough) — same flanking rule as `**` */ + 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_STRIKE; cur_attr = ATTR_TEXT_STRIKE; + p += 2; continue; + } else if (emph == EM_STRIKE && ws_or_eol_or_delim(next_ch)) { + emph = EM_NONE; cur_attr = base_attr; + p += 2; continue; + } + } + /* literal `~~` — render both tildes */ + p += 2; + if (cc >= effective_vx && col < SCREEN_W) wrchar(col++, row, '~', cur_attr); + cc++; + if (cc >= effective_vx && col < SCREEN_W) wrchar(col++, row, '~', cur_attr); + cc++; + prev_ch = '~'; + continue; + } /* `*` (italic) and `_` (underline) — same flanking rule */ if (ch == '*' || ch == '_') { char next_ch = ((p + 1) < seg_end) ? fb(p + 1) : '\n'; @@ -1197,6 +1263,12 @@ static void render_line(uint16_t line_idx, uint8_t row) break; } } + + /* Left-edge indicator: when panned right, show that nowrap content + * is hidden off the left side. Overlays column 0. */ + if (is_nowrap(line_idx) && viewport_x > 0) { + wrchar(0, row, '<', ATTR_TRUNC); + } } /* 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 @@ -1306,17 +1378,32 @@ static void scroll_down(uint16_t n) } } -/* Horizontal pan (only when viewport contains nowrap lines). */ +/* Horizontal pan (only when the viewport contains nowrap lines). The pan + * is bounded by the widest nowrap segment currently on screen, so the user + * cannot scroll past the end of the longest code/table line. */ static void scroll_h(int8_t delta) { - uint8_t any_nowrap = 0; + uint16_t maxw = 0; for (uint8_t i = 0; i < VIEW_H; i++) { - if (is_nowrap((uint16_t)(top_line + i))) { any_nowrap = 1; break; } + uint16_t li = (uint16_t)(top_line + i); + if (li >= n_lines) break; + if (!is_nowrap(li)) continue; + uint32_t a = seg_off(li); + uint32_t b = seg_off((uint16_t)(li + 1)); + if (b > file_size) b = file_size; + uint16_t w = (b > a) ? (uint16_t)(b - a) : 0; /* ~visible width */ + if (w > maxw) maxw = w; } - if (!any_nowrap) return; + if (maxw == 0) return; /* no nowrap content in view */ + + /* Max pan = width beyond the screen, capped to the uint8 pan range. */ + uint16_t over = (maxw > SCREEN_W) ? (uint16_t)(maxw - SCREEN_W) : 0; + if (over > 248u) over = 248u; + uint8_t max_vx = (uint8_t)over; + int16_t nx = (int16_t)viewport_x + delta; - if (nx < 0) nx = 0; - if (nx > 240) nx = 240; + if (nx < 0) nx = 0; + if (nx > (int16_t)max_vx) nx = max_vx; if ((uint8_t)nx != viewport_x) { viewport_x = (uint8_t)nx; render_viewport();