diff --git a/examples/mdview2/mdview2.c b/examples/mdview2/mdview2.c index 558ec49..e15f537 100644 --- a/examples/mdview2/mdview2.c +++ b/examples/mdview2/mdview2.c @@ -489,6 +489,14 @@ 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)); } +/* Символ можно экранировать обратным слешем (\X): любая печатная ASCII- + * пунктуация (как в CommonMark). \* \_ \` \[ и т.п. → литерал X. */ +static uint8_t is_escapable(char c) +{ + return (uint8_t)(c >= '!' && c <= '~' && + !((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'))); +} + /* Тип сегмента-продолжения, передаваемый в emit_seg(). * Явно в индексе не хранится: рендер при необходимости восстанавливает * тип через classify_line(), но параметр оставлен для читаемости вызовов. */ @@ -865,6 +873,15 @@ static uint8_t is_line_blank(uint32_t p) return 1; } +/* Число ведущих пробелов строки (отступ). Используется для отделения + * dedent-пунктов списка (#3) и для отступа обычного абзаца (#4). */ +static uint8_t leading_spaces(uint32_t p) +{ + uint8_t n = 0; + while (p < file_size && fb(p) == ' ' && n < 250) { n++; p++; } + return n; +} + /* Быстрая проверка границы fenced-блока по маркеру ``` . * Ведущие пробелы/табы пропускаются — fence может быть с отступом * (например, ``` ```c ``` внутри элемента списка с отступом 2+ пробела). @@ -936,6 +953,21 @@ static uint8_t inline_scan(uint32_t q, uint32_t q_end, uint8_t col, continue; } + /* Экранированная пунктуация: \* \_ \` … → литерал (не внутри кода). */ + if (line_style != INIT_STYLE_CODE && ch == '\\' && (q + 1) < q_end && is_escapable(fb(q + 1))) { + char nb = fb(q + 1); + gc_put(nb, emph_to_attr(line_style, base)); + seg_col++; q += 2; prev_ch = nb; continue; + } + /* [x] — ровно один символ в скобках → болд (task-list checkbox; не внутри кода). */ + if (line_style != INIT_STYLE_CODE && ch == '[' && (q + 2) < q_end && fb(q + 2) == ']') { + char mid = fb(q + 1); + gc_put('[', ATTR_TEXT_BOLD); + gc_put(mid, ATTR_TEXT_BOLD); + gc_put(']', ATTR_TEXT_BOLD); + seg_col += 3; q += 3; prev_ch = ']'; continue; + } + if (ch == '`') { if (line_style == INIT_STYLE_PLAIN) { line_style = INIT_STYLE_CODE; q++; continue; } if (line_style == INIT_STYLE_CODE) { line_style = INIT_STYLE_PLAIN; q++; continue; } @@ -1143,6 +1175,21 @@ static uint32_t scan_join_stream(uint32_t q, uint32_t para_start, uint8_t col, } } + /* Экранированная пунктуация: \* \_ \` … → литерал (не внутри кода). */ + if (line_style != INIT_STYLE_CODE && ch == '\\' && (q + 1) < file_size && is_escapable(fb(q + 1))) { + char nb = fb(q + 1); + gc_put(nb, styles_map[line_style]); + seg_col++; q += 2; prev_ch = nb; continue; + } + /* [x] — ровно один символ в скобках → болд (task-list checkbox; не внутри кода). */ + if (line_style != INIT_STYLE_CODE && ch == '[' && (q + 2) < file_size && fb(q + 2) == ']') { + char mid = fb(q + 1); + gc_put('[', ATTR_TEXT_BOLD); + gc_put(mid, ATTR_TEXT_BOLD); + gc_put(']', ATTR_TEXT_BOLD); + seg_col += 3; q += 3; prev_ch = ']'; continue; + } + if (ch == '`') { /* Как handle_inline_marker в render: backtick переключает code-режим * ТОЛЬКО из PLAIN или CODE; если активен эмфазис (bold/italic/...), @@ -1261,9 +1308,10 @@ static uint32_t scan_join_stream(uint32_t q, uint32_t para_start, uint8_t col, uint8_t wrap_style = use_ls ? last_space_style : line_style; if (use_ls) g_ncells = g_ncells_at_space; /* усечь до пробела */ emit_seg(wrap_at, wrap_style, ckind, 1); - /* Префикс continuation-сегмента (как render в cont-блоке): список — - * col пробелов; цитата — отступ + маркер 0xB3 + пробел; plain — без префикса. */ - if (ckind == CK_LIST) { + /* Префикс continuation-сегмента: список и отступленный абзац (#4) — + * col пробелов выравнивания; цитата — отступ + маркер 0xB3 + пробел. + * Для обычного абзаца без отступа col==0 → префикса нет. */ + if (ckind == CK_LIST || ckind == CK_PLAIN) { gc_fill(' ', ATTR_TEXT, col); } else if (ckind == CK_QUOTE) { uint32_t content_off2 = para_start + col; @@ -1449,8 +1497,13 @@ static void index_lines(void) uint32_t d = t; uint8_t nk = classify_line(t, &d); if (nk == LK_ULIST || nk == LK_OLIST) { - p = t; /* подавляем единственную пустую строку */ - continue; + /* #3: подавляем blank только если следующий пункт того же + * уровня или глубже; при МЕНЬШЕМ отступе (dedent) пустую + * строку оставляем — пункты должны визуально разделиться. */ + if (leading_spaces(t) >= leading_spaces(p)) { + p = t; /* подавляем единственную пустую строку */ + continue; + } } } } @@ -1485,10 +1538,18 @@ static void index_lines(void) /* ---- Обычный текстовый параграф: склейка через soft-break ---- */ { uint8_t line_style = INIT_STYLE_PLAIN; - emit_seg(p, line_style, CK_PLAIN, 0); + /* #4: если абзац начинается с отступа — все его перенесённые строки + * получают такой же отступ (как continuation-префикс списка, но без + * маркера). Контент сканируем после отступа. Патологически большой + * отступ игнорируем. */ + uint8_t indent = leading_spaces(p); + if (indent >= SCREEN_W / 2) indent = 0; - uint32_t q = p; - q = scan_join_stream(q, p, 0, CK_PLAIN, &line_style, JOIN_MODE_PLAIN); + emit_seg(p, line_style, CK_PLAIN, 0); + if (indent) gc_fill(' ', ATTR_TEXT, indent); /* префикс первого сегмента */ + + uint32_t q = p + indent; + q = scan_join_stream(q, p, indent, CK_PLAIN, &line_style, JOIN_MODE_PLAIN); if (q >= file_size) { p = file_size;