Files
Sprinter-SDCC/examples/mdview/mdview — унифицированный рендеринг (Unified Wrap + Paragraph Model).md
T
snark13 016fedd94c mdview: speed up rendering via bios/text.h, fix wrap-boundary style bug
Replace per-character wrchar() loops in render_line/fill_row/help screens
with batched BIOS calls (bios_writeattr/bios_fillcharattr), and cache the
per-line index record (idx_get) instead of refetching it ~13 times per
rendered line — each refetch cost two W3 bank switches via bank_read().

Also fixes a pre-existing indexer bug: when word-wrap pushes a token that
starts with an emphasis/code marker (e.g. `_text_`) onto a new line, the
marker got re-scanned a second time during the wrap continuation, flipping
line_style back off and corrupting the rendered attribute of the next
word. Fixed by snapshotting line_style at the last seen space and rolling
back to that snapshot (both the saved continuation style and the live
scan state) when a wrap is taken.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 17:06:35 +03:00

9.9 KiB
Raw Blame History

mdview — унифицированный рендеринг (Unified Wrap + Paragraph Model)

Цель

Отказаться от двух режимов Wrap/Unwrap. Ввести единую модель отображения Markdown-документов, соответствующую стандарту CommonMark:

  • Обычный текст: параграфы склеиваются по soft breaks (\n → пробел), перенос по словам на 80 колонок.
  • Hard break ( \n): принудительный перенос внутри параграфа.
  • Paragraph break (\n\n): новый абзац (пустая строка между блоками).
  • Fenced code block и таблицы (будущее): не переносятся, работают как truncate + горизонтальный скроллинг + индикатор >.
  • Горизонтальный скроллинг активен только когда в текущем viewport есть nowrap-строки.

Текущее состояние

  • mdview.c использует wrap_mode (0 = truncate, 1 = wrap) и toggle_wrap() (F2).
  • index_lines() делает два разных прохода: truncate (1 строка файла = 1 строка экрана) и wrap (сегментация по SCREEN_W).
  • render_line() имеет ветвление по wrap_mode и cont.
  • line_offset[], cont_flag[], in_code[], init_style[] — существующие структуры.

Предлагаемые изменения

1. Новые структуры данных

  • Добавить nowrap_flag[] (битмап, аналогично cont_flag[]): строка не должна переноситься (code block, HR, таблица).
  • Добавить line_kind[] (2 бита на строку, 512 байт для 2048 строк): хранит классификацию (PLAIN, H1-H4, HR, ULIST, OLIST, QUOTE, CODE, TABLE). Нужен для continuation-сегментов, чтобы render_line знал, какой префикс/отступ повторять.
  • Убрать wrap_mode и toggle_wrap().
  • Убрать truncate-ветку из index_lines() и render_line().
  • viewport_x остаётся глобальным, но применяется только к nowrap строкам.

2. Новый index_lines() — paragraph scanner

Единый проход по файлу, без двух режимов.

2.1 Сканирование «сущностей»

Walk по файлу от p = 0 до file_size:

  1. Если fb(p) == '' × 3 → Fenced code block. Все строки до закрывающего ```nowrap_flag = 1, in_code = 1 (для body), line_kind = CODE. Длинные строки не wrap'аются.
  2. Если строка начинается с |...| (будущее) → Table. nowrap_flag = 1, line_kind = TABLE.
  3. Если строка — HR (---/***/___) → nowrap_flag = 1, line_kind = HR. 1 строка экрана.
  4. Если строка — Header (#...), List (- / * / + / N. / N) ), Quote (> ) → начало Normal paragraph / block. Обрабатывается как единый параграф до \n\n или EOF или code block.
  5. Иначе — Normal paragraph (plain text).

2.2 Normal paragraph — обработка

Внутри параграфа символы читаются как единый поток (не прерываясь на \n):

  • \n\n → конец параграфа. Emit текущую сегментную строку (если есть). Добавить пустую строку (1 строка экрана с нулевой длиной, или offset указывающий на второй \n).
  • \n (два пробела или таб + пробел перед \n) → Hard break. Emit текущую сегментную строку. Начать новую сегментную строку с того же абзаца, сохранив line_style. prev_ch = ' '.
  • Обычный \nSoft break. Считаем как 1 пробел: visible col++, prev_ch = ' '. Продолжаем текущую сегментную строку.
  • Wrap at SCREEN_W — так же как сейчас, по last_space. При создании continuation-сегмента: set_cont(), set_init_style_raw() (carry emphasis), set_line_kind() (carry list/quote kind для отступа/префикса).
  • Emphasis tracking (line_style) работает через весь параграф, через soft/hard breaks.

2.3 Fenced code block — обработка

  • Delimiter строка ( ): line_kind = CODE, nowrap_flag = 1, рендерится как пустая строка (или строка с — но сейчас пустая).
  • Body строки: каждая физическая строка = 1 строка экрана. nowrap_flag = 1, in_code = 1, line_kind = CODE. Нет inline parsing, нет wrap.
  • Если строка длиннее SCREEN_W — truncation (не wrap). render_line() рисует > в последней колонке, если есть content за пределами SCREEN_W + viewport_x.

2.4 List continuation

  • Первая строка list item: line_kind = ULIST/OLIST, marker_visible_col определяет отступ. Текст начинается после маркера.
  • Wrap внутри list item: set_line_kind() = тот же ULIST/OLIST. render_line() для continuation-сегмента не рисует маркер, но добавляет отступ = marker_visible_col (т.е. количество пробелов до текста первой строки).
  • Для вложенных списков отступ растёт, потому что marker_visible_col учитывает ведущие пробелы.

2.5 Quote continuation

  • Первая строка quote: line_kind = QUOTE, marker_visible_col = отступ + 2 (для ).
  • Wrap внутри quote: set_line_kind() = QUOTE. render_line() для continuation рисует префикс (или пробелы, если символ не нужен на всех кроме первой строки).

3. Изменения в render_line()

  • Убрать ветку if (!wrap_mode) (truncate mode больше не существует).
  • Добавить if (is_nowrap(line_idx)): применяется viewport_x, отображается > в колонке 79 если контент выходит за пределы.
  • Для cont сегментов (wrap continuation):
    • Если line_kind == QUOTE: повторить префикс отступа + (или только отступ, если решено показывать только на первой строке).
    • Если line_kind == ULIST/OLIST: повторить отступ = marker_visible_col (без маркера/цифр). Текст начинается с той же колонки, что и на первой строке item.
    • Для plain text: просто продолжить текст без префиксов.
  • Для nowrap строк: применять viewport_x как смещение. Текст сдвигается влево, скрытые символы не рисуются. Если за пределами видимой зоны есть ещё контент — > в колонке 79.

4. Горизонтальный скроллинг

  • scroll_h() проверяет: есть ли в текущем viewport (top_line .. top_line+VIEW_H-1) хотя бы одна строка с nowrap_flag == 1. Если нет — return (no-op).
  • Для nowrap строк render_line() использует viewport_x при отображении: символы с cc < viewport_x пропускаются, cc >= viewport_x рисуются.
  • Для wrap-строк viewport_x игнорируется (эффективно = 0).
  • При Home/End/PgUp/PgDn/Up/Downviewport_x не сбрасывается (пользователь может скроллить по вертикали, оставаясь на горизонтальном смещении для code block).

5. Очистка и навигация

  • Убрать wrap_mode, toggle_wrap().
  • Убрать F2 из меню и help.
  • scroll_up/scroll_down/scroll_h/clamp_top — обновить без ссылок на wrap_mode.
  • calc_pct и статусная строка — обновить, убрать ссылку на wrap/unwrap.

6. Порядок реализации (по файлу mdview.c)

  1. Структуры данных: добавить nowrap_flag[], line_kind[], убрать wrap_mode.
  2. index_lines(): полностью переписать в paragraph scanner. Это самый сложный и объёмный блок.
  3. render_line(): убрать truncate, добавить nowrap/quote/list continuation, добавить viewport_x для nowrap.
  4. scroll_h(): добавить проверку наличия nowrap в viewport.
  5. Очистка: убрать toggle_wrap, обновить меню/help, обновить main() (убрать F2).
  6. Тестирование: soft breaks, hard breaks, paragraph breaks, emphasis через soft breaks, fenced code blocks, list wrap с отступом, quote wrap с префиксом, горизонтальный скролл только для code.

Orchestration

План реализуется одним агентом (последовательно в одном файле), параллелизм не требуется. Дочерние агенты не используются.