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

90 lines
9.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 = ' '`.
* Обычный `\n`**Soft 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`/`Down``viewport_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
План реализуется одним агентом (последовательно в одном файле), параллелизм не требуется. Дочерние агенты не используются.