fix(mdview): корректный multiline quote join в render_line

- исправлен rewind до первого non-cont сегмента для continuation
- для quote-потока newline обрабатывается как soft join с пропуском сырого ' > ' маркера
- восстановлен quote-префикс на continuation строках

Co-Authored-By: Oz <oz-agent@warp.dev>
This commit is contained in:
2026-06-07 20:26:19 +03:00
parent 463a058f56
commit 4bed9d3f3f
+73 -21
View File
@@ -988,10 +988,16 @@ static void index_lines(void)
prev_ch = ' '; continue;
}
if (ch == ' ') {
if (soft_break) last_space = q;
else last_space = q + 1;
seg_col++;
if (!soft_break) q++; else q++;
if (soft_break) {
/* Virtual separator space between joined lines:
* do NOT consume source byte at q here. */
last_space = q;
seg_col++;
} else {
last_space = q + 1;
seg_col++;
q++;
}
prev_ch = ' ';
} else {
seg_col++;
@@ -1095,6 +1101,12 @@ static void index_lines(void)
if (ws == ' ' || ws == '\t') next_content++;
else break;
}
/* Страховка: если в точке склейки остался маркер quote,
* поглощаем его (и один пробел после него). */
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 = ' ';
@@ -1166,10 +1178,16 @@ static void index_lines(void)
prev_ch = ' '; continue;
}
if (ch == ' ') {
if (soft_break) last_space = q;
else last_space = q + 1;
seg_col++;
if (!soft_break) q++; else q++;
if (soft_break) {
/* Виртуальный пробел между склеенными строками:
* исходный байт по q не потребляем. */
last_space = q;
seg_col++;
} else {
last_space = q + 1;
seg_col++;
q++;
}
prev_ch = ' ';
} else {
seg_col++;
@@ -1311,10 +1329,16 @@ static void index_lines(void)
prev_ch = ' '; continue;
}
if (ch == ' ') {
if (soft_break) last_space = q;
else last_space = q + 1;
seg_col++;
if (!soft_break) q++; else q++;
if (soft_break) {
/* Виртуальный пробел soft-break: символ по q
* обработаем на следующей итерации. */
last_space = q;
seg_col++;
} else {
last_space = q + 1;
seg_col++;
q++;
}
prev_ch = ' ';
} else {
seg_col++;
@@ -1372,17 +1396,23 @@ static void render_line(uint16_t line_idx, uint8_t row)
uint32_t seg_end = seg_off((uint16_t)(line_idx + 1));
if (seg_end > file_size) seg_end = file_size;
uint8_t kind;
uint8_t quote_stream = 0; /* 1: этот сегмент принадлежит quote-параграфу */
/* Continuation seg → repeat prefix/indent from the first non-CONT
* segment of this logical line, then render plain text. */
if (cont) {
uint16_t first = line_idx;
while (first > 0 && is_cont((uint16_t)(first - 1))) first--;
/* Для любого continuation-сегмента откатываемся к первому
* НЕ-cont сегменту этой логической строки. */
while (first > 0 && is_cont(first)) first--;
uint32_t p_start = seg_off(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 first_kind = classify_line(p_start, &content_off);
uint8_t indent = marker_visible_col(first_kind, p_start, content_off);
if (first_kind == LK_QUOTE) {
/* Для continuation quote нужно помнить тип потока,
* чтобы далее на newline корректно пропускать сырой `>`. */
quote_stream = 1;
uint8_t q = 0;
while (q + 2 < content_off && col < SCREEN_W && fb(p_start + q) == ' ') {
wrchar(col++, row, ' ', ATTR_TEXT);
@@ -1390,7 +1420,7 @@ static void render_line(uint16_t line_idx, uint8_t row)
}
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) {
} else if (first_kind == LK_ULIST || first_kind == LK_OLIST) {
uint8_t q = 0;
while (q < indent && col < SCREEN_W) {
wrchar(col++, row, ' ', ATTR_TEXT);
@@ -1414,6 +1444,7 @@ static void render_line(uint16_t line_idx, uint8_t row)
cur_attr = ATTR_TEXT_CODE;
parse_inline = 0;
kind = LK_PLAIN;
quote_stream = 0;
} else if (cont) {
/* Continuation row: the parent prefix/indent is already drawn
* above. Render the wrapped content as plain text — do NOT
@@ -1422,6 +1453,7 @@ static void render_line(uint16_t line_idx, uint8_t row)
kind = LK_PLAIN;
} else {
kind = classify_line(p, &content_off);
if (kind == LK_QUOTE) quote_stream = 1;
}
if (kind == LK_HR) {
@@ -1484,6 +1516,7 @@ static void render_line(uint16_t line_idx, uint8_t row)
uint8_t cc = 0;
char prev_ch = ' '; /* for `_` flanking check */
while (p < seg_end && col < SCREEN_W) {
uint8_t soft_join = 0; /* 1: вставлен виртуальный пробел при склейке quote-строк */
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
@@ -1492,9 +1525,28 @@ static void render_line(uint16_t line_idx, uint8_t row)
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 = ' ';
/* Soft break (newline inside a paragraph segment).
* Для quote-потока нужно не только отрисовать пробел, но и
* пропустить сырой quote-маркер следующей физической строки. */
if (parse_inline && quote_stream && (ch == '\n' || ch == '\r')) {
uint32_t next = p + 1;
if (next < seg_end) {
uint32_t next_content = next;
if (classify_line(next, &next_content) == LK_QUOTE) {
/* Мягкая склейка quote-строк: newline -> ' ',
* а `> ` следующей строки не попадает в вывод. */
p = next_content;
ch = ' ';
soft_join = 1;
} else {
ch = ' ';
}
} else {
ch = ' ';
}
} else if (ch == '\n' || ch == '\r') {
ch = ' ';
}
/* --- inline emphasis markers (consumed, not rendered) ---
* Asterisk / double-asterisk / underscore are markers only
@@ -1577,7 +1629,7 @@ static void render_line(uint16_t line_idx, uint8_t row)
}
}
p++;
if (!soft_join) p++;
if (ch == '\t') {
/* Tab expands in CONTENT-col space (cc), each generated
* space is then conditionally rendered through hpan. */