# mdview — модель документа и рендеринг (единый смешанный режим) Дизайн-документ переработки `mdview.c` под новое ТЗ форматирования (`examples/mdview/todo2`). Описывает решение по хранению данных, разбор документа при загрузке, упрощение рендера и снятие лимита на число строк. ## 1. Контекст и главный вопрос Мы отказываемся от двух режимов показа (Wrap/UnWrap-переключатель) и переходим к единому смешанному режиму: тип переноса задаётся типом блока (обычный текст/заголовки/списки/цитаты — Wrap; код/таблицы — UnWrap). Главный вопрос: нужно ли хранить оригинальный байтовый контент файла, или при загрузке сразу преобразовать его в «готовый к показу» текст (склеить строки параграфов, убрать маркеры, развернуть отступы и т.п.)? Ответ: **оригинал храним; отдельный «готовый» текстовый/ячеечный буфер не строим.** «Подготовка при загрузке» реализуется как построение компактного *индекса метаданных*, а не как преобразование *содержимого*. ## 2. Решение по архитектуре ### 2.1 Текущее состояние (база) * Файл целиком лежит в EMM-страницах (до 8×16 КБ). В окно W3 (`0xC000`) в каждый момент замаплена ровно одна страница; доступ к байту — через `fb()`/`map_page()` (`mdview.c:198-213`). * Индекс — это параллельные массивы по экранным сегментам: `line_offset[]` (смещение в файле) + битфлаги `cont/nowrap/blank/in_code`, 2-битный `line_kind[]`, `init_style[]` (`mdview.c:122-135`). * `index_lines()` за один проход уже делает склейку параграфов обычного текста, перенос по словам на 80 колонок и перенос emphasis через soft/hard break (`mdview.c:692-911`). * `render_line()` при каждой отрисовке заново читает байты из оригинала и заново парсит inline-разметку (`mdview.c:918-1156`). Вывод: «готовим при загрузке» мы уже делаем — но готовим **индекс**, а не текст. ### 2.2 Почему не материализуем содержимое * **Одно свободное окно W3.** Режим памяти `small`: код в W1, данные/стек/куча в W2, для банкуемых данных свободен только W3. Преобразование «оригинал → готовый буфер» требует одновременно держать замапленными исходную страницу (чтение) и страницу-приёмник (запись). При единственном окне это поток swap-ов на каждую границу. In-place преобразование тоже невозможно: склейка меняет длины, смещения «съезжают», параграф пересекает границу 16 КБ. * **Удвоение памяти и срыв гарантии 128 КБ.** Оригинал может занимать все 8 страниц; готовой копии нужны свои страницы — гарантировать, что влезут обе, нельзя. * **Готовая форма не обязательно меньше.** Снятие маркеров экономит байты, но добавляются отступы-продолжения у переносов списков/цитат. В лучшем случае ≈ размер оригинала, в худшем — больше. Ячеечная модель (символ+атрибут) — это ×2 (до 256 КБ), невозможно. * **Покадровая стоимость и так мала.** За кадр рисуются только 30 видимых строк (~30×80 чтений). Единственный дорогой проход — `index_lines()` — неизбежен в любой архитектуре (нужен полный скан для `n_lines` и процента прокрутки). ### 2.3 Что реально оптимизировать Не текст, а повторную работу `render_line()`: * вызов `classify_line()` на каждый кадр (`mdview.c:984`); * обратный проход к первому не-cont сегменту ради отступа продолжения (`mdview.c:944-970`). Это снимается переносом результата классификации (`kind`, ширина отступа/контент-колонка) в сам индекс на этапе `index_lines()`. ## 3. Модель данных индекса ### 3.1 Запись сегмента Единая запись на экранный сегмент полностью заменяет нынешние параллельные массивы (`line_offset`, `cont_flag`, `in_code`, `nowrap_flag`, `blank_flag`, `line_kind`, `init_style`); `index_lines()` переписывается с нуля под эту модель. Цель размера записи — 5–6 байт: * `offset` — 3 байта (24-битное смещение в файле, покрывает 128 КБ). * `flags` — 1 байт: биты `cont`, `nowrap`, `blank`, `in_code` + 2-битный `ckind` (тип продолжения: PLAIN/QUOTE/LIST/OTHER). * `style` — 1 байт: стартовый стиль сегмента (`init_style`, теперь включая STRIKE) + при необходимости глубина вложенности. * `indent` — 1 байт: предвычисленная контент-колонка/ширина префикса, чтобы рендер не вызывал `classify_line()` и не делал обратный проход. ### 3.2 Размещение и снятие лимита `MAX_LINES` Проблема: `line_offset` сейчас `uint32_t[2048]` = 8 КБ, `init_style` = 2 КБ; суммарно статический индекс ~11.6 КБ near-памяти (W2). Рост лимита в near невозможен — W2 переполнится. Решение: * **Индекс храним в отдельном EMM-блоке** (свои страницы, помимо файловых), доступ — через тот же W3. * **Near-кэш viewport**: перед отрисовкой кадра разово вычитываем записи для `VIEW_H+1` видимых сегментов в маленький near-массив (≈ `(VIEW_H+1)×6` ≈ 186 байт). Рендер работает по near-кэшу + читает только контент-страницы. Это устраняет per-byte thrashing между страницей индекса и страницей контента: переключений на кадр — единицы, а не тысячи. * **Динамический размер**: число страниц под индекс выделяем пропорционально размеру файла (число сегментов ∈ размеру). `MAX_LINES` становится функцией от выделенных страниц индекса. Это прямо ложится на заметку v2 («чем больше банков под файл, тем больше буферы»). * **Без регресса скорости при индексе в EMM**: чтобы вынос индекса в банки не замедлил сборку (W3 делится между чтением контента и записью индекса), записи копим в near-буфере батчами и сбрасываем в EMM-блок через `bank_write()` (он сам сохраняет/восстанавливает маппинг W3). Переключений окна на всю сборку — единицы, а не на каждый сегмент. * Если индекс-страницы выделить не удалось — деградируем до текущего near-лимита и показываем явную диагностику обрезки (а не молчаливый обрыв в `emit_seg()` — `mdview.c:574`). ## 4. `index_lines()` — разбор по новому ТЗ Единый проход по файлу строит сегменты. Деление на параграфы — по пустым строкам; несколько пустых строк подряд схлопываются в одну. ### 4.0 Скорость подготовки — главный приоритет Требование: подготовка максимально быстрая (сейчас ~25 КБ готовятся 6–10 с). Две структурные причины медленности и их устранение: * **Per-byte `fb(uint32_t)`.** Каждый доступ к байту пересчитывает страницу 32-битными `p >> 14` и `p & 0x3FFF` (`mdview.c:209-213`). На Z80 32-битная арифметика — это программные подпрограммы на каждый символ. Замена: **потоковый разбор** — мапим страницу один раз, идём по окну `char *`/16-битным индексом, страницу переключаем только на границе 16 КБ. 32-битным остаётся лишь сохраняемый в индекс `offset`. * **Многократные пере-сканы.** На каждом `\n` внутри параграфа вызываются `is_fence_raw()`, `is_hr_raw()`, `classify_line()`, `is_line_blank()` (`mdview.c:800-809`) — каждая заново сканирует следующую строку, а `classify_line()` ещё и повторяет цикл детекции HR. Для параграфа из N строк — O(N×длина) лишней работы. Замена: **один проход** — каждую строку классифицируем ровно один раз в момент её начала, lookahead — минимальный (несколько первых байт). * **32-битные сравнения.** Курсор скана — (страница:8 бит, смещение:16 бит); сравнение с концом — сначала по странице, потом 16-битно. Ожидаемый эффект: подготовка — по сути один линейный проход с 16-битными операциями, кратное ускорение относительно текущего multi-pass + 32-bit. (`render_line()` может остаться на `fb()` — там только ~30×80 байт за кадр.) ### 4.1 Обычный текст (Wrap) * **Soft break** (одиночный `\n`): склейка, следующая строка продолжается через пробел. * **Wide break**: 2+ пробелов перед `\n` **или** символ `\` перед `\n` → принудительный перенос внутри параграфа, стиль сохраняется. (Текущий код ловит только 2 пробела — `mdview.c:813-818`; добавить ветку для `\`.) * **Hard break** (пустая строка): новый параграф, отделяется ОДНОЙ пустой строкой независимо от числа пустых строк в оригинале. * **Модификаторы** bold/italic/strike/code действуют через soft/wide break внутри параграфа и сбрасываются на границе параграфа. (Добавить STRIKE `~~…~~` — сейчас его нет в `INIT_STYLE_*`/`ATTR_*`.) ### 4.2 Заголовки (Wrap) Один оригинальный абзац-строка; стартовый стиль по уровню. Во входе распознаём H1–H6, но H4/H5/H6 далее обрабатываются одинаково как H4 (сливаются в один стиль; `classify_line()` уже сворачивает `lvl>4` → `LK_H4`, `mdview.c:488`). Внутри допустимы bold/italic/code/strike. После заголовка всегда пустая строка. ### 4.3 Горизонтальный разделитель HR Всегда одна строка, после неё всегда пустая строка (новый абзац). ### 4.4 Списки (Wrap) — НОВОЕ: многострочная склейка * Пункт может занимать несколько оригинальных строк; soft break внутри пункта склеивается через пробел (как обычный текст). Сейчас списки эмитятся построчно (`mdview.c:762-772`) — переписать на paragraph-модель. * Новая строка с префиксом пункта → новый пункт. * Пустая строка завершает пункт. Следующая непустая НЕ-пункт строка не является продолжением. * **Группировка**: если после ОДНОЙ пустой строки идёт снова пункт — это тот же список, пустая строка в показе подавляется (пункты идут вплотную). Только ДВЕ+ пустые строки между пунктами разрывают на разные списки (в показе — одна пустая строка между ними). * Перенос продолжения пункта печатается с отступом до контент-колонки (для уровня 1 — 2 пробела). * Незакрытые модификаторы НЕ переносятся на следующий пункт (каждый пункт — свой параграф). * Вложенные списки поддерживаются (отступ растёт с ведущими пробелами). * Отдельные стили: префикс маркера и текст списка. Пример соответствует разделу «Списки» в `todo2` (строки 1–6). ### 4.5 Цитаты (Wrap) * Отдельный параграф; перед текстом — префикс цитаты, перенесённые строки тоже предваряются префиксом. * Многострочная склейка как у текста; пустая строка-цитата (`>`) показывается как пустая строка с префиксом. * Вложенность (`> >` → двойной префикс). Отдельные стили: префикс и текст цитаты. ### 4.6 Блок кода ``` ``` ``` (UnWrap) Весь блок одним стилем кода, без inline-модификаторов. После блока обязательна пустая строка. Строки не переносятся (truncate + горизонтальный скролл). ### 4.7 Таблицы (UnWrap) Пока as-is, без переноса. Выравнивание столбцов — v2. ## 5. `render_line()` — упрощение * Убрать ветку truncate-режима и `wrap_mode` (уже частично снято; `toggle_wrap()` — мёртвая заглушка `mdview.c:1286-1291`, удалить вместе с упоминаниями F2). * Не вызывать `classify_line()` и не делать обратный проход: использовать `kind`/`indent`/`style` из индекса. * Для cont-сегментов списков/цитат — печать отступа/префикса по `kind`+`indent` из записи сегмента. * Inline-парсинг emphasis выполняется только в пределах видимого сегмента (дёшево); для кода/таблиц — отключён. ## 6. Горизонтальный скроллинг (UnWrap) * Скроллится только UnWrap-текст (код/таблицы). Грануляция 8 символов (`HPAN_STEP`). * Правый край: индикатор `>` своим стилем, если есть скрытый контент справа (есть — `mdview.c:1135-1155`). * **Добавить** левый индикатор `<` в первой колонке, когда `viewport_x > 0`. * **Границы скролла по факту**: текущий кламп жёстко до 240 (`mdview.c:1275`). Заменить на вычисление максимального переполнения среди UnWrap-строк в текущем viewport, чтобы вправо нельзя было уйти за самую длинную строку, а влево — до колонки 0. ## 7. Чеклист расхождений с текущим кодом * [индекс] Перейти на запись-на-сегмент в EMM-банке + near-кэш viewport; снять `MAX_LINES=2048`. * [скорость] Потоковый разбор: один проход, 16-битный курсор в окне (без per-byte `fb()` с 32-битной арифметикой), классификация строки один раз, минимальный lookahead; батч-флеш индекса. * [текст] Wide break по символу `\`. * [текст] Модификатор strikethrough `~~…~~` (+ стиль). * [списки] Многострочная склейка пунктов и правило группировки по одной/двум пустым строкам. * [цитаты] Многострочная склейка и повтор префикса (в т.ч. вложенные) на переносах. * [заголовки] Читать H1–H6; H4/H5/H6 трактовать как H4 (частично уже есть — `mdview.c:488`). * [скролл] Индикатор `<` и корректные границы по фактическому переполнению. * [рендер] Снять per-кадровый `classify_line()` и обратный проход (данные — из индекса). * [очистка] Удалить `toggle_wrap()` и упоминания F2 (`mdview.c:1286-1291`, `1311`). ## 8. Память: бюджет * near (W2): текущий статический индекс ~11.6 КБ — у предела окна. После переноса `line_offset`/`init_style` в EMM в near остаётся near-кэш viewport (~0.2 КБ) + мелкие флаги → запас под стек/кучу растёт. * EMM: файл до 8 страниц + индекс ~1–2 страницы (при 5–6 байт/сегмент и нескольких тысячах сегментов). Перед выделением проверять `mem_info()` на доступность страниц. ## 9. Заметки для v2 * **Прогрессивный показ**: отрисовать первую страницу (первые ~30 сегментов) ДО завершения полной подготовки; остальное доиндексировать дальше или по мере прокрутки. Процент и `End` показывать как «вычисляется», пока не готов полный `n_lines`. (Синергия с потоковым разбором §4.0: первый экран готов после разбора лишь нескольких КБ.) * Разделители переноса Wrap: точка/запятая/`!`/`?`/дефис; правило «новая строка не начинается с разделителя», серия разделителей остаётся на первой строке. * Таблицы: вычисление ширины столбцов и выравнивание. * Подсветка синтаксиса внутри блоков кода. * URL/Images и прочие типы строк.