From 07c4004bd94d2595130943e5468183c999cd7431 Mon Sep 17 00:00:00 2001 From: Alexander Petrov Date: Mon, 8 Jun 2026 11:22:15 +0300 Subject: [PATCH] ChangeLog: - commit current version MDView.c --- examples/mdview/mdview.c | 971 ++++++++++++++------------------------- 1 file changed, 351 insertions(+), 620 deletions(-) diff --git a/examples/mdview/mdview.c b/examples/mdview/mdview.c index 667aa04..97306f9 100644 --- a/examples/mdview/mdview.c +++ b/examples/mdview/mdview.c @@ -67,6 +67,7 @@ static const uint8_t md_pallete[16 * 4] = { 0xFF, 0xFF, 0xFF, 0xFF }; +#define ATTR_RESET COLOR(COLOR_LIGHTGRAY, COLOR_BLACK) #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) @@ -165,46 +166,6 @@ static void put_str_attr(uint8_t x, uint8_t y, const char *s, uint8_t attr) } } -/* Упрощённый вывод строки через gotoxy/puts без явного атрибута. */ -static void put_str(uint8_t x, uint8_t y, const char *s) -{ - gotoxy(x, y); - puts(s); -} - -/* Преобразует uint16 в десятичную строку без printf. - * Возвращает число записанных цифр в out[] (1..5). */ -static uint8_t u16_to_dec(uint16_t v, char *out) -{ - char tmp[5]; - uint8_t n = 0; - if (v == 0) { - out[0] = '0'; - return 1; - } - while (v > 0 && n < 5) { - tmp[n++] = (char)('0' + (v % 10)); - v /= 10; - } - for (uint8_t i = 0; i < n; i++) out[i] = tmp[(uint8_t)(n - 1 - i)]; - return n; -} - -/* Рисует uint16 в десятичном виде, с выравниванием вправо по ширине width. */ -static void put_u16_right_attr(uint8_t x, uint8_t y, uint16_t v, uint8_t width, uint8_t attr) -{ - char buf[5]; - uint8_t len = u16_to_dec(v, buf); - uint8_t pad = (len < width) ? (uint8_t)(width - len) : 0; - while (pad-- && x < SCREEN_W) wrchar(x++, y, ' ', attr); - for (uint8_t i = 0; i < len && x < SCREEN_W; i++) wrchar(x++, y, buf[i], attr); -} - -/* Рисует uint8 в десятичном виде, с выравниванием вправо по ширине width. */ -static void put_u8_right_attr(uint8_t x, uint8_t y, uint8_t v, uint8_t width, uint8_t attr) -{ - put_u16_right_attr(x, y, (uint16_t)v, width, attr); -} /* Полностью очищает указанную строку экрана символами пробела с attr. */ static void fill_row(uint8_t y, uint8_t attr) @@ -482,36 +443,6 @@ static uint8_t get_init_style_raw(uint16_t idx) return (uint8_t)(r.style & 7u); } -/* Возвращает 1, если строку нельзя переносить: - * она должна рендериться одним сегментом даже если длиннее SCREEN_W. - * Сейчас учитываются границы блока кода и горизонтальные линии. */ -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; -} /* Тип строки, который возвращает classify_line(). */ #define LK_PLAIN 0 @@ -782,6 +713,232 @@ static uint8_t inline_scan(uint32_t q, uint32_t q_end, uint8_t col, return line_style; } +#define JOIN_MODE_LIST 1u +#define JOIN_MODE_QUOTE 2u +#define JOIN_MODE_PLAIN 3u + +/* Общий потоковый сканер для index_lines(). + * Убирает дублирование между ветками list/quote/plain: + * - mode задаёт правила обработки перевода строки, + * - остальная inline-логика (emphasis/таб/перенос) едина. + * Возвращает позицию q, на которой сканер остановился, и обновляет стиль. */ +static uint32_t scan_join_stream(uint32_t q, uint32_t para_start, uint8_t col, + uint8_t ckind, uint8_t *io_line_style, uint8_t mode) +{ + uint32_t last_space = 0xFFFFFFFFu; + char prev_ch = ' '; + uint8_t seg_col = col; + uint8_t line_style = *io_line_style; + + while (q < file_size && n_lines < max_lines) { + char ch = fb(q); + uint8_t soft_break = 0; + + if (ch == '\n') { + uint32_t next = q + 1; + if (next >= file_size) break; + + if (mode == JOIN_MODE_LIST) { + if (is_line_blank(next)) break; + if (is_fence_raw(next) || is_hr_raw(next) || is_table_raw(next)) break; + + { + uint32_t next_content = next; + uint8_t next_kind = classify_line(next, &next_content); + if (next_kind == LK_ULIST || next_kind == LK_OLIST || + next_kind == LK_QUOTE || + (next_kind >= LK_H1 && next_kind <= LK_H4) || + next_kind == LK_HR) { + break; + } + } + while (next < file_size) { + char ws = fb(next); + if (ws == ' ' || ws == '\t') next++; + else break; + } + q = next; + soft_break = 1; + ch = ' '; + } else if (mode == JOIN_MODE_QUOTE) { + if (is_line_blank(next)) break; + if (is_fence_raw(next) || is_hr_raw(next) || is_table_raw(next)) break; + + { + uint32_t next_content = next; + uint8_t next_kind = classify_line(next, &next_content); + if (next_kind != LK_QUOTE) break; + + { + uint32_t next_line_end = next_content; + while (next_line_end < file_size && fb(next_line_end) != '\n') next_line_end++; + + { + uint8_t empty_quote = 1; + for (uint32_t z = next_content; z < next_line_end; z++) { + char c = fb(z); + if (c != ' ' && c != '\t') { empty_quote = 0; break; } + } + if (empty_quote) break; + } + + { + uint32_t t = next_content; + while (t < next_line_end && (fb(t) == ' ' || fb(t) == '\t')) t++; + if (t < next_line_end && fb(t) == '>') break; + } + + while (next_content < next_line_end) { + char ws = fb(next_content); + if (ws == ' ' || ws == '\t') next_content++; + else break; + } + if (next_content < next_line_end && fb(next_content) == '>') { + next_content++; + if (next_content < next_line_end && fb(next_content) == ' ') next_content++; + } + q = next_content; + soft_break = 1; + ch = ' '; + } + } + } else { /* JOIN_MODE_PLAIN */ + if (fb(next) == '\n') break; + if (is_fence_raw(next) || is_hr_raw(next) || is_table_raw(next)) break; + { + uint32_t dummy; + if (classify_line(next, &dummy) != LK_PLAIN) break; + } + if (is_line_blank(next)) break; + + { + uint8_t is_hard = 0; + if (q >= para_start + 1) { + char b1 = fb(q - 1); + if (b1 == '\\') { + is_hard = 1; + } else if (q >= para_start + 2) { + char b2 = fb(q - 2); + if ((b1 == ' ' || b1 == '\t') && (b2 == ' ' || b2 == '\t')) + is_hard = 1; + } + } + if (is_hard) { + q++; + emit_seg(q, line_style, ckind, 0); + last_space = 0xFFFFFFFFu; + prev_ch = ' '; + seg_col = col; + continue; + } + } + + 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 == '~' && (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)) { + 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; + seg_col++; + } else { + last_space = q + 1; + seg_col++; + q++; + } + prev_ch = ' '; + } else { + seg_col++; + if (!soft_break) q++; else q++; + prev_ch = ch; + } + if (seg_col >= SCREEN_W) { + uint32_t wrap_at = (last_space != 0xFFFFFFFFu && last_space > cur_seg_off) + ? last_space : q; + if (wrap_at >= file_size || wrap_at == cur_seg_off) break; + emit_seg(wrap_at, line_style, ckind, 1); + seg_col = col; + last_space = 0xFFFFFFFFu; + prev_ch = ' '; + q = wrap_at; + } + } + + *io_line_style = line_style; + return q; +} + /* Строит индекс сегментов рендера по загруженному файлу. */ static void index_lines(void) { @@ -793,7 +950,9 @@ static void index_lines(void) uint32_t p = 0; while (p < file_size && n_lines < max_lines) { - if ((n_lines & 15) == 0) spinner_tick(); + + if ((n_lines & 15) == 0) + spinner_tick(); /* Пустая строка — разделитель параграфов. * В индекс добавляем не более одной пустой экранной строки подряд. */ @@ -863,139 +1022,10 @@ static void index_lines(void) if (kind == LK_ULIST || kind == LK_OLIST) { uint8_t col = marker_visible_col(kind, p, content_off); uint8_t line_style = INIT_STYLE_PLAIN; - uint32_t item_start = p; uint32_t q = content_off; - uint32_t last_space = 0xFFFFFFFFu; - char prev_ch = ' '; - uint8_t seg_col = col; emit_seg(p, line_style, CK_LIST, 0); - - while (q < file_size && n_lines < max_lines) { - char ch = fb(q); - uint8_t soft_break = 0; - - if (ch == '\n') { - uint32_t next = q + 1; - if (next >= file_size) break; - - /* Пустая строка завершает текущий пункт списка. */ - if (is_line_blank(next)) break; - /* Границы блоков тоже завершают пункт. */ - if (is_fence_raw(next) || is_hr_raw(next) || is_table_raw(next)) break; - - /* Новый маркер списка/цитаты/заголовка/разделителя начинает новый блок. */ - uint32_t next_content = next; - uint8_t next_kind = classify_line(next, &next_content); - if (next_kind == LK_ULIST || next_kind == LK_OLIST || - next_kind == LK_QUOTE || - (next_kind >= LK_H1 && next_kind <= LK_H4) || - next_kind == LK_HR) { - break; - } - - /* Ленивая склейка: соединяем с немаркерной строкой - * через один пробел, убирая её ведущий отступ. */ - while (next < file_size) { - char ws = fb(next); - if (ws == ' ' || ws == '\t') next++; - else break; - } - q = next; - 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 == '~' && (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)) { - 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) { - /* Виртуальный разделитель между склеенными строками: - * исходный байт по q здесь не потребляем. */ - last_space = q; - seg_col++; - } else { - last_space = q + 1; - seg_col++; - q++; - } - prev_ch = ' '; - } else { - seg_col++; - if (!soft_break) q++; else q++; - prev_ch = ch; - } - if (seg_col >= SCREEN_W) { - uint32_t wrap_at = (last_space != 0xFFFFFFFFu && last_space > cur_seg_off) - ? last_space : q; - if (wrap_at >= file_size || wrap_at == cur_seg_off) break; - emit_seg(wrap_at, line_style, CK_LIST, 1); - seg_col = col; last_space = 0xFFFFFFFFu; prev_ch = ' '; q = wrap_at; - } - } + q = scan_join_stream(q, p, col, CK_LIST, &line_style, JOIN_MODE_LIST); /* Переходим к началу следующей физической строки после пункта. */ if (q < file_size) { @@ -1025,7 +1055,6 @@ static void index_lines(void) } p = q; - if (p < item_start) p = item_start; continue; } @@ -1034,158 +1063,9 @@ static void index_lines(void) uint8_t col = marker_visible_col(kind, p, content_off); uint8_t line_style = INIT_STYLE_PLAIN; uint32_t q = content_off; - uint32_t last_space = 0xFFFFFFFFu; - char prev_ch = ' '; - uint8_t seg_col = col; emit_seg(p, line_style, CK_QUOTE, 0); - - while (q < file_size && n_lines < max_lines) { - char ch = fb(q); - uint8_t soft_break = 0; - - if (ch == '\n') { - uint32_t next = q + 1; - if (next >= file_size) break; - - /* Реальная пустая строка завершает блок цитаты. */ - if (is_line_blank(next)) break; - if (is_fence_raw(next) || is_hr_raw(next) || is_table_raw(next)) break; - - /* Продолжать параграф может только следующая quote-строка. */ - uint32_t next_content = next; - uint8_t next_kind = classify_line(next, &next_content); - if (next_kind != LK_QUOTE) break; - - /* Ищем физический конец следующей quote-строки. */ - uint32_t next_line_end = next_content; - while (next_line_end < file_size && fb(next_line_end) != '\n') next_line_end++; - - /* Пустая quote-строка (`>`/`> `) разделяет параграфы: - * останавливаем склейку, внешняя петля обработает её отдельно. */ - uint8_t empty_quote = 1; - for (uint32_t z = next_content; z < next_line_end; z++) { - char c = fb(z); - if (c != ' ' && c != '\t') { empty_quote = 0; break; } - } - if (empty_quote) break; - - /* Вложенная цитата (`> > ...`) начинает новый quote-параграф, - * поэтому не склеиваем её с текущим текстом. */ - { - uint32_t t = next_content; - while (t < next_line_end && (fb(t) == ' ' || fb(t) == '\t')) t++; - if (t < next_line_end && fb(t) == '>') break; - } - - /* Мягкая склейка через один пробел + удаление отступа - * у продолжения quote-текста. */ - while (next_content < next_line_end) { - char ws = fb(next_content); - if (ws == ' ' || ws == '\t') next_content++; - else break; - } - /* Страховка: если в точке склейки всё ещё есть `>`, - * поглощаем маркер и один пробел после него. */ - if (next_content < next_line_end && fb(next_content) == '>') { - next_content++; - if (next_content < next_line_end && fb(next_content) == ' ') next_content++; - } - q = next_content; - 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 == '~' && (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)) { - 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) { - /* Виртуальный пробел между склеенными строками: - * исходный байт по q не потребляем. */ - last_space = q; - seg_col++; - } else { - last_space = q + 1; - seg_col++; - q++; - } - prev_ch = ' '; - } else { - seg_col++; - if (!soft_break) q++; else q++; - prev_ch = ch; - } - if (seg_col >= SCREEN_W) { - uint32_t wrap_at = (last_space != 0xFFFFFFFFu && last_space > cur_seg_off) - ? last_space : q; - if (wrap_at >= file_size || wrap_at == cur_seg_off) break; - emit_seg(wrap_at, line_style, CK_QUOTE, 1); - seg_col = col; last_space = 0xFFFFFFFFu; prev_ch = ' '; q = wrap_at; - } - } + q = scan_join_stream(q, p, col, CK_QUOTE, &line_style, JOIN_MODE_QUOTE); while (q < file_size && fb(q) != '\n') q++; if (q < file_size) q++; @@ -1199,144 +1079,7 @@ static void index_lines(void) 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') { - /* Проверяем, не завершился ли параграф. */ - if (q + 1 >= file_size) break; - if (fb(q + 1) == '\n') break; /* пустая строка */ - uint32_t next = q + 1; - if (is_fence_raw(next)) break; - if (is_hr_raw(next)) break; - if (is_table_raw(next)) break; - uint32_t dummy; - if (classify_line(next, &dummy) != LK_PLAIN) break; - if (is_line_blank(next)) break; - - /* Жёсткий перенос: - * два пробела/таба в конце строки ИЛИ завершающий '\' - * перед newline. При этом inline-стиль сохраняется. */ - uint8_t is_hard = 0; - if (q >= p + 1) { - char b1 = fb(q - 1); - 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) { - q++; - emit_seg(q, line_style, CK_PLAIN, 0); - last_space = 0xFFFFFFFFu; prev_ch = ' '; seg_col = 0; - continue; - } - - /* Мягкий перенос: считаем его одиночным пробелом. */ - 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 == '~' && (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)) { - 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) { - /* Виртуальный пробел soft-break: символ по q - * обработаем на следующей итерации. */ - last_space = q; - seg_col++; - } else { - last_space = q + 1; - seg_col++; - q++; - } - prev_ch = ' '; - } else { - seg_col++; - if (!soft_break) q++; else q++; - prev_ch = ch; - } - if (seg_col >= SCREEN_W) { - uint32_t wrap_at = (last_space != 0xFFFFFFFFu && last_space > cur_seg_off) - ? last_space : q; - if (wrap_at >= file_size || wrap_at == cur_seg_off) break; - emit_seg(wrap_at, line_style, CK_PLAIN, 1); - seg_col = 0; last_space = 0xFFFFFFFFu; prev_ch = ' '; q = wrap_at; - } - } + q = scan_join_stream(q, p, 0, CK_PLAIN, &line_style, JOIN_MODE_PLAIN); if (q >= file_size) { p = file_size; @@ -1358,6 +1101,87 @@ static void index_lines(void) #define EM_CODE 4 #define EM_STRIKE 5 +/* Преобразует внутреннее состояние emphasis в экранный атрибут. */ +static uint8_t emph_to_attr(uint8_t emph, uint8_t base_attr) +{ + switch (emph) { + case EM_BOLD: return ATTR_TEXT_BOLD; + case EM_ITALIC: return ATTR_TEXT_ITALIC; + case EM_UNDER: return ATTR_TEXT_UNDERSORE; + case EM_CODE: return ATTR_TEXT_CODE; + case EM_STRIKE: return ATTR_TEXT_STRIKE; + default: return base_attr; + } +} + +/* Единая обработка inline-маркеров в render_line(). + * Возврат: + * 0 = маркер не поглощён (символ выводится как есть), + * 1 = маркер поглощён как управляющий (ничего не рисуем), + * 2 = литеральный двойной маркер (`**` или `~~`), нужно вывести две копии out_ch. */ +static uint8_t handle_inline_marker(char ch, uint32_t *io_p, uint32_t seg_end, + char prev_ch, uint8_t *io_emph, char *out_ch) +{ + uint32_t p = *io_p; + + *out_ch = 0; + + if (ch == '`') { + if (*io_emph == EM_NONE) { + *io_emph = EM_CODE; + *io_p = p + 1; + return 1; + } + if (*io_emph == EM_CODE) { + *io_emph = EM_NONE; + *io_p = p + 1; + return 1; + } + return 0; + } + + if ((ch == '*' || ch == '~') && (p + 1) < seg_end && fb(p + 1) == ch) { + uint8_t pair_emph = (ch == '*') ? EM_BOLD : EM_STRIKE; + char next_ch = ((p + 2) < seg_end) ? fb(p + 2) : '\n'; + + if (is_emph_flanked(prev_ch, next_ch)) { + if (*io_emph == EM_NONE && ws_or_eol(prev_ch)) { + *io_emph = pair_emph; + *io_p = p + 2; + return 1; + } else if (*io_emph == pair_emph && ws_or_eol_or_delim(next_ch)) { + *io_emph = EM_NONE; + *io_p = p + 2; + return 1; + } + } + + /* Невалидный по границам двойной маркер отображаем как литерал. */ + *io_p = p + 2; + *out_ch = ch; + return 2; + } + + 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; + if (*io_emph == EM_NONE && ws_or_eol(prev_ch)) { + *io_emph = active; + *io_p = p + 1; + return 1; + } else if (*io_emph != EM_NONE && ws_or_eol_or_delim(next_ch)) { + *io_emph = EM_NONE; + *io_p = p + 1; + return 1; + } + } + } + + return 0; +} + + static void render_line(uint16_t line_idx, uint8_t row) { uint8_t col = 0; @@ -1550,80 +1374,28 @@ static void render_line(uint16_t line_idx, uint8_t row) ch = ' '; } - /* --- маркеры inline-выделения (поглощаются, но не рисуются) --- - * Маркер активен только при XOR-границах (пробел с одной - * стороны). Это защищает выражения вроде "2 * 3" от ложного - * форматирования и синхронизирует ширину рендера с индексатором. */ + /* inline-маркеры (**, ~~, *, _, `): + * helper синхронизирует состояние emphasis и возвращает режим + * обработки текущего токена (поглотить/вывести литерал/обычный символ). */ if (parse_inline) { - /* `**` (жирный) */ - 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; - } - } - /* Литерал `**`: рисуем обе звезды, в ветку одиночной - * `*` не проваливаемся. */ - 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 = '*'; + char lit_ch = 0; + uint8_t mk = handle_inline_marker(ch, &p, seg_end, prev_ch, &emph, &lit_ch); + if (mk == 1) { + cur_attr = emph_to_attr(emph, base_attr); continue; } - /* `~~` (зачёркнутый) — те же правила границ, что у `**`. */ - 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; - } + if (mk == 2) { + if (cc >= effective_vx && col < SCREEN_W) { + wrchar(col++, row, lit_ch, cur_attr); } - /* Литерал `~~`: рисуем обе тильды. */ - 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); + if (cc >= effective_vx && col < SCREEN_W) { + wrchar(col++, row, lit_ch, cur_attr); + } cc++; - prev_ch = '~'; + prev_ch = lit_ch; continue; } - /* `*` (курсив) и `_` (подчёркивание) — то же XOR-правило. */ - 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; - } - } - /* Внутрисловной или двусторонний случай — оставляем литерал. */ - } - 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; - } - } } if (!soft_join) p++; @@ -1709,19 +1481,23 @@ static void render_status_numbers(void) if (last > n_lines) last = n_lines; uint8_t pct = calc_pct(); + textattr(ATTR_BAR); for (uint8_t c = 45; c < SCREEN_W; c++) wrchar(c, 0, ' ', ATTR_BAR); wrchar(45, 0, 0xB3, ATTR_BAR); /* вертикальный разделитель */ - wrchar(47, 0, 'L', ATTR_BAR); - put_u16_right_attr(49, 0, (uint16_t)(top_line + 1), 5, ATTR_BAR); - wrchar(54, 0, '-', ATTR_BAR); - put_u16_right_attr(55, 0, last, 5, ATTR_BAR); - put_str_attr(60, 0, " / ", ATTR_BAR); - put_u16_right_attr(63, 0, n_lines, 5, ATTR_BAR); + gotoxy(47, 0); + cputs("L "); + dec16(top_line + 1); + cputs("-"); + dec16(last); + cputs(" / "); + dec16(n_lines); + wrchar(70, 0, 0xB3, ATTR_BAR); /* вертикальный разделитель */ - put_u8_right_attr(73, 0, pct, 3, ATTR_BAR); - wrchar(76, 0, '%', ATTR_BAR); + gotoxy( 73, 0); + dec8(pct); + cputs("%"); } /* Полностью перерисовывает статус-бар (заголовок + числа). */ @@ -1829,52 +1605,6 @@ static void scroll_h(int8_t delta) } } -/* Историческая заглушка от режима wrap/unwrap. - * Функция оставлена только для совместимости по символу. */ -static void toggle_wrap(void) -{ - /* Функционально не используется. */ - (void)0; -} - -/* ================================================================== - * Справка - * ================================================================== */ - -static const char * const help_lines[] = { - "", - " MDVIEW v0.1 - Markdown text viewer for Sprinter", - " =================================================", - "", - " Navigation:", - " Up / Down - scroll one line", - " PgUp / PgDn - scroll one page", - " Home - go to top of document", - " End - go to bottom of document", - " Left / Right - pan horizontally (nowrap lines only)", - "", - " Other:", - " F1 - show this help", - " F2 - (reserved)", - " F10 / Esc - exit", - "", - " Press any key to return...", - (const char *)0 -}; - -/* Показывает экран справки и возвращает пользователя обратно в просмотрщик. */ -static void help_screen(void) -{ - clrscr_attr(ATTR_TEXT); - for (uint8_t i = 0; help_lines[i]; i++) { - put_str_attr(0, (uint8_t)(i + 1), help_lines[i], ATTR_TEXT); - } - (void)getkey(); - clrscr_attr(ATTR_TEXT); - render_menu(); - render_full_status(); - render_viewport(); -} /* ================================================================== * Вывод ошибки @@ -1921,18 +1651,19 @@ int main(int argc, char **argv) int rc = load_file(path); if (rc < 0) { spinner_show(0); - clrscr_attr(ATTR_TEXT); - puts("mdview: cannot load file"); - puts(path); + clrscr_attr(ATTR_RESET); + textattr(ATTR_RESET); + cputs("MDView: cannot load file '"); + cputs(path); + cputs("'\n\rError: "); switch (rc) { - case -1: puts("(open failed)"); break; - case -2: puts("(size > 128K or seek failed)"); break; - case -3: puts("(mem_alloc_pages failed)"); break; - case -4: puts("(short read)"); break; - case -5: puts("(index alloc failed)"); break; + case -1: cputs("open failed"); break; + case -2: cputs("size > 128K or seek failed"); break; + case -3: cputs("mem_alloc_pages failed"); break; + case -4: cputs("short read"); break; + case -5: cputs("index alloc failed"); break; } - puts("Press any key to exit..."); - (void)getkey(); + cputs("\n\r\n\r"); return 1; } index_lines();