# 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 План реализуется одним агентом (последовательно в одном файле), параллелизм не требуется. Дочерние агенты не используются.