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>
This commit is contained in:
2026-06-23 17:06:35 +03:00
parent c9d5ea35a4
commit 016fedd94c
4 changed files with 625 additions and 94 deletions
@@ -0,0 +1,103 @@
# 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` (строки 16).
### 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 и прочие типы строк.
@@ -0,0 +1,90 @@
# 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
План реализуется одним агентом (последовательно в одном файле), параллелизм не требуется. Дочерние агенты не используются.
+252 -94
View File
@@ -21,6 +21,7 @@
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
#include <conio.h> #include <conio.h>
#include <bios/text.h>
#include <fcntl.h> #include <fcntl.h>
#include <unistd.h> #include <unistd.h>
#include <sprinter.h> #include <sprinter.h>
@@ -43,7 +44,7 @@
#define PAGE_MASK ((uint16_t)(PAGE_SIZE - 1u)) #define PAGE_MASK ((uint16_t)(PAGE_SIZE - 1u))
#define MAX_PAGES 8 /* 8 страниц × 16 КБ = 128 КБ */ #define MAX_PAGES 8 /* 8 страниц × 16 КБ = 128 КБ */
#define MAX_FILE ((uint32_t)MAX_PAGES * PAGE_SIZE) /* максимальный размер файла: 131072 байт */ #define MAX_FILE ((uint32_t)MAX_PAGES * PAGE_SIZE) /* максимальный размер файла: 131072 байт */
#define MAX_LINES 2048 // #define MAX_LINES 2048
#define FILE_BUF ((char *)0xC000) /* окно W3, куда мапится текущая EMM-страница */ #define FILE_BUF ((char *)0xC000) /* окно W3, куда мапится текущая EMM-страница */
/* ---- Палитра атрибутов ------------------------------------------- */ /* ---- Палитра атрибутов ------------------------------------------- */
@@ -172,21 +173,25 @@ static uint8_t spinner_active = 0;
* ================================================================== */ * ================================================================== */
/* Печатает строку с указанным атрибутом, начиная с позиции (x,y), /* Печатает строку с указанным атрибутом, начиная с позиции (x,y),
* и останавливается на конце строки или правой границе экрана. */ * и останавливается на конце строки или правой границе экрана.
* `s` — литерал/статический буфер (CODE/DATA, окна 1/2), поэтому его
* можно передать в BIOS LP_PRINT_LN напрямую, без промежуточной копии. */
static void put_str_attr(uint8_t x, uint8_t y, const char *s, uint8_t attr) static void put_str_attr(uint8_t x, uint8_t y, const char *s, uint8_t attr)
{ {
while (*s && x < SCREEN_W) { uint8_t maxlen = (x < SCREEN_W) ? (uint8_t)(SCREEN_W - x) : 0;
wrchar(x++, y, *s++, attr); uint8_t len = (uint8_t)strlen(s);
} if (len > maxlen) len = maxlen;
if (len == 0) return;
bios_set_place(y, x);
bios_writeattr(s, len, attr);
} }
/* Полностью очищает указанную строку экрана символами пробела с attr. */ /* Полностью очищает указанную строку экрана символами пробела с attr. */
static void fill_row(uint8_t y, uint8_t attr) static void fill_row(uint8_t y, uint8_t attr)
{ {
for (uint8_t c = 0; c < SCREEN_W; c++) { bios_set_place(y, 0);
wrchar(c, y, ' ', attr); bios_fillcharattr(' ', attr, SCREEN_W);
}
} }
/* Продвигает спиннер на один кадр. /* Продвигает спиннер на один кадр.
@@ -695,6 +700,7 @@ static uint8_t inline_scan(uint32_t q, uint32_t q_end, uint8_t col,
uint8_t ckind, uint8_t line_style) uint8_t ckind, uint8_t line_style)
{ {
uint32_t last_space = 0xFFFFFFFFu; uint32_t last_space = 0xFFFFFFFFu;
uint8_t last_space_style = line_style; /* снимок line_style на момент last_space */
char prev_ch = ' '; char prev_ch = ' ';
uint8_t seg_col = col; uint8_t seg_col = col;
@@ -755,16 +761,24 @@ static uint8_t inline_scan(uint32_t q, uint32_t q_end, uint8_t col,
} }
if (ch == ' ') { if (ch == ' ') {
last_space = q + 1; last_space = q + 1;
last_space_style = line_style;
seg_col++; q++; prev_ch = ' '; seg_col++; q++; prev_ch = ' ';
} else { } else {
seg_col++; q++; prev_ch = ch; seg_col++; q++; prev_ch = ch;
} }
if (seg_col >= SCREEN_W) { if (seg_col >= SCREEN_W) {
uint32_t wrap_at = (last_space != 0xFFFFFFFFu && last_space > cur_seg_off) uint8_t use_ls = (uint8_t)(last_space != 0xFFFFFFFFu && last_space > cur_seg_off);
? last_space : q; uint32_t wrap_at = use_ls ? last_space : q;
if (wrap_at >= q_end || wrap_at == cur_seg_off) break; if (wrap_at >= q_end || wrap_at == cur_seg_off) break;
emit_seg(wrap_at, line_style, ckind, 1); uint8_t wrap_style = use_ls ? last_space_style : line_style;
emit_seg(wrap_at, wrap_style, ckind, 1);
seg_col = col; last_space = 0xFFFFFFFFu; prev_ch = ' '; q = wrap_at; seg_col = col; last_space = 0xFFFFFFFFu; prev_ch = ' '; q = wrap_at;
/* Откатываем line_style к снимку на момент wrap_at: диапазон
* [wrap_at, q_old) будет пересканирован заново, и переключающие
* символы (обратная кавычка, звёздочка, подчёркивание, тильда)
* внутри него иначе применились бы дважды — именно так возникал
* баг: code-маркер на границе переноса ломает цвет следующего слова. */
line_style = wrap_style;
} }
} }
return line_style; return line_style;
@@ -783,6 +797,7 @@ static uint32_t scan_join_stream(uint32_t q, uint32_t para_start, uint8_t col,
uint8_t ckind, uint8_t *io_line_style, uint8_t mode) uint8_t ckind, uint8_t *io_line_style, uint8_t mode)
{ {
uint32_t last_space = 0xFFFFFFFFu; uint32_t last_space = 0xFFFFFFFFu;
uint8_t last_space_style = *io_line_style; /* снимок line_style на момент last_space */
char prev_ch = ' '; char prev_ch = ' ';
uint8_t seg_col = col; uint8_t seg_col = col;
uint8_t line_style = *io_line_style; uint8_t line_style = *io_line_style;
@@ -968,9 +983,11 @@ static uint32_t scan_join_stream(uint32_t q, uint32_t para_start, uint8_t col,
if (ch == ' ') { if (ch == ' ') {
if (soft_break) { if (soft_break) {
last_space = q; last_space = q;
last_space_style = line_style;
seg_col++; seg_col++;
} else { } else {
last_space = q + 1; last_space = q + 1;
last_space_style = line_style;
seg_col++; seg_col++;
q++; q++;
} }
@@ -981,14 +998,22 @@ static uint32_t scan_join_stream(uint32_t q, uint32_t para_start, uint8_t col,
prev_ch = ch; prev_ch = ch;
} }
if (seg_col >= SCREEN_W) { if (seg_col >= SCREEN_W) {
uint32_t wrap_at = (last_space != 0xFFFFFFFFu && last_space > cur_seg_off) uint8_t use_ls = (uint8_t)(last_space != 0xFFFFFFFFu && last_space > cur_seg_off);
? last_space : q; uint32_t wrap_at = use_ls ? last_space : q;
if (wrap_at >= file_size || wrap_at == cur_seg_off) break; if (wrap_at >= file_size || wrap_at == cur_seg_off) break;
emit_seg(wrap_at, line_style, ckind, 1); uint8_t wrap_style = use_ls ? last_space_style : line_style;
emit_seg(wrap_at, wrap_style, ckind, 1);
seg_col = col; seg_col = col;
last_space = 0xFFFFFFFFu; last_space = 0xFFFFFFFFu;
prev_ch = ' '; prev_ch = ' ';
q = wrap_at; q = wrap_at;
/* Откатываем line_style к снимку на момент wrap_at: диапазон
* [wrap_at, q_old) будет пересканирован заново, и переключающие
* символы (обратная кавычка, звёздочка, подчёркивание, тильда)
* внутри него иначе применились бы дважды — именно так возникал
* баг: code-маркер на границе переноса ломает цвет следующего слова (mdview.c, отчёт пользователя
* 2026-06-23). */
line_style = wrap_style;
} }
} }
@@ -1238,26 +1263,69 @@ static uint8_t handle_inline_marker(char ch, uint32_t *io_p, uint32_t seg_end,
return 0; return 0;
} }
/* Сбрасывает накопленный "прогон" из *col символов одного атрибута
* одним bios_writeattr() вместо посимвольных wrchar(). *need_place
* лениво ставит BIOS-курсор перед первым выводом в текущей строке —
* далее place продвигается сам (verified), повторный bios_set_place
* не нужен пока *col растёт монотонно. */
static void flush_run(uint8_t row, uint8_t *col, const char *buf,
uint8_t len, uint8_t attr, uint8_t *need_place)
{
if (len == 0) return;
/* *col уже указывает на конец прогона (вызывающий цикл инкрементит
* col на каждый символ ещё во время накопления в буфер — это нужно
* для верных проверок col < SCREEN_W до сброса). Поэтому начало
* прогона на экране = *col - len, а не *col; и *col второй раз
* наращивать НЕ нужно — он уже корректен. */
if (*need_place) {
bios_set_place(row, (uint8_t)(*col - len));
*need_place = 0;
}
bios_writeattr(buf, len, attr);
}
static void render_line(uint16_t line_idx, uint8_t row) static void render_line(uint16_t line_idx, uint8_t row)
{ {
/* Один idx_get() на line_idx вместо ~13 отдельных is_blank/is_cont/
* is_nowrap/is_code_body/seg_off/get_init_style* вызовов. Каждый из
* них шёл через seg_flags()/seg_off() -> idx_get() -> bank_read(),
* а bank_read() ДВАЖДЫ переключает банк окна 3 (на страницу индекса
* и обратно) плюс memcpy — это оказалось главным узким местом
* рендера (≈700+ переключений банка на кадр), а не сам вывод на
* экран. Все нужные поля (off/flags/style) лежат в одной 8-байтовой
* записи idx_rec_t — читаем её один раз и используем дальше. */
uint8_t have_rec = (line_idx < n_lines);
idx_rec_t rec;
if (have_rec) idx_get(line_idx, &rec);
uint8_t col = 0; uint8_t col = 0;
uint8_t base_attr = ATTR_TEXT; uint8_t base_attr = ATTR_TEXT;
uint8_t cur_attr = is_code_body(line_idx) ? ATTR_TEXT_CODE : get_init_style(line_idx); uint8_t is_code = (uint8_t)(have_rec && (rec.flags & IF_CODE));
uint8_t emph = is_code_body(line_idx) ? EM_CODE : get_init_style_raw(line_idx); uint8_t cur_attr = is_code ? ATTR_TEXT_CODE
: styles_map[(have_rec ? rec.style : INIT_STYLE_PLAIN) & 7u];
uint8_t emph = is_code ? EM_CODE
: (uint8_t)((have_rec ? rec.style : INIT_STYLE_PLAIN) & 7u);
uint8_t parse_inline = 1; /* 1: включён парсер выделений, 0: выводим код как есть */ uint8_t parse_inline = 1; /* 1: включён парсер выделений, 0: выводим код как есть */
if (is_blank(line_idx)) { if (have_rec && (rec.flags & IF_BLANK)) {
while (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT); bios_set_place(row, col);
bios_fillcharattr(' ', ATTR_TEXT, (uint8_t)(SCREEN_W - col));
return; return;
} }
if (line_idx < n_lines) { if (have_rec) {
uint32_t p = seg_off(line_idx); uint32_t p = rec.off;
uint32_t content_off = p; uint32_t content_off = p;
uint8_t cont = is_cont(line_idx); uint8_t cont = (uint8_t)((rec.flags & IF_CONT) != 0);
/* Правая граница сегмента: начало следующего сегмента или конец файла. */ /* Правая граница сегмента: начало следующего сегмента или конец файла. */
uint32_t seg_end = seg_off((uint16_t)(line_idx + 1)); uint32_t seg_end;
if ((uint16_t)(line_idx + 1) < n_lines) {
idx_rec_t rec_next;
idx_get((uint16_t)(line_idx + 1), &rec_next);
seg_end = rec_next.off;
} else {
seg_end = file_size;
}
if (seg_end > file_size) seg_end = file_size; if (seg_end > file_size) seg_end = file_size;
uint8_t kind; uint8_t kind;
uint8_t quote_stream = 0; /* 1: текущий сегмент относится к потоку цитаты */ uint8_t quote_stream = 0; /* 1: текущий сегмент относится к потоку цитаты */
@@ -1265,11 +1333,16 @@ static void render_line(uint16_t line_idx, uint8_t row)
/* Для сегмента-продолжения повторяем префикс/отступ /* Для сегмента-продолжения повторяем префикс/отступ
* от первого не-CONT сегмента логической строки. */ * от первого не-CONT сегмента логической строки. */
if (cont) { if (cont) {
uint16_t first = line_idx; uint16_t first = line_idx;
idx_rec_t rec_first = rec; /* rec.flags здесь уже содержит IF_CONT */
/* Для любого сегмента-продолжения откатываемся к первому /* Для любого сегмента-продолжения откатываемся к первому
* НЕ-cont сегменту этой логической строки. */ * НЕ-cont сегменту этой логической строки — один idx_get()
while (first > 0 && is_cont(first)) first--; * на шаг вместо двух (is_cont + повторный seg_off). */
uint32_t p_start = seg_off(first); while (first > 0 && (rec_first.flags & IF_CONT)) {
first--;
idx_get(first, &rec_first);
}
uint32_t p_start = rec_first.off;
uint32_t content_off = p_start; uint32_t content_off = p_start;
uint8_t first_kind = classify_line(p_start, &content_off); uint8_t first_kind = classify_line(p_start, &content_off);
uint8_t indent = marker_visible_col(first_kind, p_start, content_off); uint8_t indent = marker_visible_col(first_kind, p_start, content_off);
@@ -1278,33 +1351,41 @@ static void render_line(uint16_t line_idx, uint8_t row)
* чтобы далее на newline корректно пропускать сырой `>`. */ * чтобы далее на newline корректно пропускать сырой `>`. */
quote_stream = 1; quote_stream = 1;
uint8_t q = 0; uint8_t q = 0;
while (q + 2 < content_off && col < SCREEN_W && fb(p_start + q) == ' ') { while (q + 2 < content_off && q < SCREEN_W && fb(p_start + q) == ' ') q++;
wrchar(col++, row, ' ', ATTR_TEXT); if (q > (uint8_t)(SCREEN_W - col)) q = (uint8_t)(SCREEN_W - col);
q++; if (q > 0) {
bios_set_place(row, col);
bios_fillcharattr(' ', ATTR_TEXT, q);
col = (uint8_t)(col + q);
} }
if (col < SCREEN_W) wrchar(col++, row, 0xB3, ATTR_QUOTE_MARKER); if (col < SCREEN_W) wrchar(col++, row, 0xB3, ATTR_QUOTE_MARKER);
if (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT); if (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT);
} else if (first_kind == LK_ULIST || first_kind == LK_OLIST) { } else if (first_kind == LK_ULIST || first_kind == LK_OLIST) {
uint8_t q = 0; uint8_t q = indent;
while (q < indent && col < SCREEN_W) { if (q > (uint8_t)(SCREEN_W - col)) q = (uint8_t)(SCREEN_W - col);
wrchar(col++, row, ' ', ATTR_TEXT); if (q > 0) {
q++; bios_set_place(row, col);
bios_fillcharattr(' ', ATTR_TEXT, q);
col = (uint8_t)(col + q);
} }
} }
/* Дальше рендерим контент именно с текущего сегмента-продолжения. */ /* p/content_off уже равны rec.off — внутри cont-блока они не
p = seg_off(line_idx); * менялись (там свои локальные тени с тем же именем),
content_off = p; * пересчитывать через seg_off() заново не нужно. */
/* Для сегмента-продолжения парсер работает как обычно. */
} }
/* Кодовый блок: /* Кодовый блок:
* - строка-разделитель показывается как пустая; * - строка-разделитель показывается как пустая;
* - тело рисуется «как есть» с ATTR_TEXT_CODE без inline-парсинга. */ * - тело рисуется «как есть» с ATTR_TEXT_CODE без inline-парсинга.
if (is_fence_delim(line_idx)) { * is_fence_delim(line_idx) разворачено на месте: cont и p уже
while (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT); * известны, повторный idx_get()/is_cont() не нужен. */
if (!cont && p + 2 < file_size &&
fb(p) == '`' && fb(p + 1) == '`' && fb(p + 2) == '`') {
bios_set_place(row, col);
bios_fillcharattr(' ', ATTR_TEXT, (uint8_t)(SCREEN_W - col));
return; return;
} }
if (is_code_body(line_idx)) { if (is_code) {
base_attr = ATTR_TEXT_CODE; base_attr = ATTR_TEXT_CODE;
cur_attr = ATTR_TEXT_CODE; cur_attr = ATTR_TEXT_CODE;
parse_inline = 0; parse_inline = 0;
@@ -1321,7 +1402,8 @@ static void render_line(uint16_t line_idx, uint8_t row)
} }
if (kind == LK_HR) { if (kind == LK_HR) {
while (col < SCREEN_W) wrchar(col++, row, 0xC4, ATTR_HR); bios_set_place(row, col);
bios_fillcharattr(0xC4, ATTR_HR, (uint8_t)(SCREEN_W - col));
return; return;
} }
if (kind >= LK_H1 && kind <= LK_H4) { if (kind >= LK_H1 && kind <= LK_H4) {
@@ -1336,10 +1418,15 @@ static void render_line(uint16_t line_idx, uint8_t row)
} else if (kind == LK_ULIST) { } else if (kind == LK_ULIST) {
/* Базовая поддержка вложенности: ведущие пробелы сохраняем, /* Базовая поддержка вложенности: ведущие пробелы сохраняем,
* затем рисуем маркер списка и пробел перед содержимым. */ * затем рисуем маркер списка и пробел перед содержимым. */
uint32_t q = p; uint32_t q = p;
while (q + 2 < content_off && col < SCREEN_W && fb(q) == ' ') { uint32_t qmax = p + SCREEN_W;
wrchar(col++, row, ' ', ATTR_TEXT); while (q + 2 < content_off && q < qmax && fb(q) == ' ') q++;
q++; uint8_t nsp = (uint8_t)(q - p);
if (nsp > (uint8_t)(SCREEN_W - col)) nsp = (uint8_t)(SCREEN_W - col);
if (nsp > 0) {
bios_set_place(row, col);
bios_fillcharattr(' ', ATTR_TEXT, nsp);
col = (uint8_t)(col + nsp);
} }
if (col < SCREEN_W) wrchar(col++, row, 0x07, ATTR_LIST_MARKER); /* символ маркера списка */ if (col < SCREEN_W) wrchar(col++, row, 0x07, ATTR_LIST_MARKER); /* символ маркера списка */
if (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT); if (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT);
@@ -1347,24 +1434,54 @@ static void render_line(uint16_t line_idx, uint8_t row)
} else if (kind == LK_OLIST) { } else if (kind == LK_OLIST) {
/* Отступ -> пробелы; цифры и '.' / ')' рисуем как маркер; /* Отступ -> пробелы; цифры и '.' / ')' рисуем как маркер;
* завершающий пробел — обычным атрибутом текста. */ * завершающий пробел — обычным атрибутом текста. */
uint32_t q = p; uint32_t q = p;
while (q < content_off && col < SCREEN_W && fb(q) == ' ') { uint32_t qmax = p + SCREEN_W;
wrchar(col++, row, ' ', ATTR_TEXT); while (q < content_off && q < qmax && fb(q) == ' ') q++;
q++; uint8_t nsp = (uint8_t)(q - p);
if (nsp > (uint8_t)(SCREEN_W - col)) nsp = (uint8_t)(SCREEN_W - col);
if (nsp > 0) {
bios_set_place(row, col);
bios_fillcharattr(' ', ATTR_TEXT, nsp);
col = (uint8_t)(col + nsp);
} }
while (q < content_off - 1 && col < SCREEN_W) { /* Маркер (цифры/'.'/')') читается из fb() — это окно 3,
wrchar(col++, row, fb(q), ATTR_LIST_MARKER); * BIOS принимает указатели только из #4000..#BFFF, поэтому
q++; * сначала копируем во временный буфер на стеке (окно 2),
* чанками по 8 байт (маркер почти всегда короче, но длинный
* корректно не обрезается, просто шлётся несколькими
* bios_writeattr вместо одного). */
{
char mkbuf[8];
uint8_t first_chunk = 1;
while (q < content_off - 1 && col < SCREEN_W) {
uint8_t n = 0;
while (q < content_off - 1 &&
(uint8_t)(col + n) < SCREEN_W &&
n < sizeof(mkbuf)) {
mkbuf[n] = fb(q);
q++;
n++;
}
if (n == 0) break;
if (first_chunk) { bios_set_place(row, col); first_chunk = 0; }
bios_writeattr(mkbuf, n, ATTR_LIST_MARKER);
col = (uint8_t)(col + n);
}
} }
if (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT); if (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT);
p = content_off; p = content_off;
} else if (kind == LK_QUOTE) { } else if (kind == LK_QUOTE) {
/* Отступ -> пробелы; затем quote-префикс '│' (0xB3), /* Отступ -> пробелы; затем quote-префикс '│' (0xB3),
* пробел и далее содержимое. */ * пробел и далее содержимое. */
uint32_t q = p; uint32_t q = p;
while (q + 1 < content_off && col < SCREEN_W && fb(q) == ' ') { uint32_t qmax = p + SCREEN_W;
wrchar(col++, row, ' ', ATTR_TEXT); while (q + 1 < content_off && q < qmax && fb(q) == ' ') q++;
q++; uint8_t nsp = (uint8_t)(q - p);
if (nsp > (uint8_t)(SCREEN_W - col)) nsp = (uint8_t)(SCREEN_W - col);
if (nsp > 0) {
bios_set_place(row, col);
bios_fillcharattr(' ', ATTR_TEXT, nsp);
col = (uint8_t)(col + nsp);
} }
if (col < SCREEN_W) wrchar(col++, row, 0xB3, ATTR_QUOTE_MARKER); if (col < SCREEN_W) wrchar(col++, row, 0xB3, ATTR_QUOTE_MARKER);
if (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT); if (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT);
@@ -1372,11 +1489,25 @@ static void render_line(uint16_t line_idx, uint8_t row)
} }
/* Эффективный горизонтальный сдвиг: применяется только к nowrap-строкам. */ /* Эффективный горизонтальный сдвиг: применяется только к nowrap-строкам. */
uint8_t effective_vx = is_nowrap(line_idx) ? viewport_x : 0; uint8_t effective_vx = (rec.flags & IF_NOWRAP) ? viewport_x : 0;
/* cc — видимая колонка внутри области контента /* cc — видимая колонка внутри области контента
* (после фиксированного префикса строки). */ * (после фиксированного префикса строки). */
uint8_t cc = 0; uint8_t cc = 0;
char prev_ch = ' '; /* используется в проверке границ emphasis */ char prev_ch = ' '; /* используется в проверке границ emphasis */
/* Буферизация "ровного" прогона одного атрибута: символ и атрибут
* внутри этого while переменные (emphasis/tab/soft-wrap), поэтому
* весь цикл одним BIOS-вызовом не свернуть — но между сменами
* атрибута почти всегда идёт протяжённый одноцветный текст, и
* именно его стоит копить в runbuf и сбрасывать одним
* bios_writeattr() вместо посимвольных wrchar().
* runbuf вмещает целую строку экрана — переполнение невозможно,
* т.к. весь цикл и так ограничен col < SCREEN_W. */
char runbuf[SCREEN_W];
uint8_t run_len = 0;
uint8_t run_attr = cur_attr;
uint8_t need_place = 1;
while (p < seg_end && col < SCREEN_W) { while (p < seg_end && col < SCREEN_W) {
uint8_t soft_join = 0; /* 1: вставлен виртуальный пробел мягкой склейки, p уже сдвинут */ uint8_t soft_join = 0; /* 1: вставлен виртуальный пробел мягкой склейки, p уже сдвинут */
char ch = fb(p); char ch = fb(p);
@@ -1438,16 +1569,23 @@ static void render_line(uint16_t line_idx, uint8_t row)
char lit_ch = 0; char lit_ch = 0;
uint8_t mk = handle_inline_marker(ch, &p, seg_end, prev_ch, &emph, &lit_ch); uint8_t mk = handle_inline_marker(ch, &p, seg_end, prev_ch, &emph, &lit_ch);
if (mk == 1) { if (mk == 1) {
/* Атрибут меняется -> сбросить накопленный прогон
* со СТАРЫМ атрибутом, прежде чем переключиться. */
flush_run(row, &col, runbuf, run_len, run_attr, &need_place);
run_len = 0;
cur_attr = emph_to_attr(emph, base_attr); cur_attr = emph_to_attr(emph, base_attr);
run_attr = cur_attr;
continue; continue;
} }
if (mk == 2) { if (mk == 2) {
if (cc >= effective_vx && col < SCREEN_W) { if (cc >= effective_vx && col < SCREEN_W && run_len < sizeof(runbuf)) {
wrchar(col++, row, lit_ch, cur_attr); runbuf[run_len++] = lit_ch;
col++;
} }
cc++; cc++;
if (cc >= effective_vx && col < SCREEN_W) { if (cc >= effective_vx && col < SCREEN_W && run_len < sizeof(runbuf)) {
wrchar(col++, row, lit_ch, cur_attr); runbuf[run_len++] = lit_ch;
col++;
} }
cc++; cc++;
prev_ch = lit_ch; prev_ch = lit_ch;
@@ -1457,28 +1595,44 @@ static void render_line(uint16_t line_idx, uint8_t row)
if (!soft_join) p++; if (!soft_join) p++;
if (ch == '\t') { if (ch == '\t') {
/* Таб расширяется в координатах контента (cc). */ /* Таб расширяется в координатах контента (cc).
uint8_t tgt = (uint8_t)((cc & (uint8_t)~(TAB_STOP - 1)) + TAB_STOP); * Сначала сбросить накопленный текстовый прогон — таб
while (cc < tgt && col < SCREEN_W) { * выводится отдельным bios_fillcharattr(), не через runbuf. */
if (cc >= effective_vx) wrchar(col++, row, ' ', cur_attr); flush_run(row, &col, runbuf, run_len, run_attr, &need_place);
cc++; run_len = 0;
uint8_t cc_start = cc;
uint8_t tgt = (uint8_t)((cc & (uint8_t)~(TAB_STOP - 1)) + TAB_STOP);
uint8_t n_total = (uint8_t)(tgt - cc_start);
uint8_t skip = (effective_vx > cc_start) ? (uint8_t)(effective_vx - cc_start) : 0;
if (skip > n_total) skip = n_total;
uint8_t want = (uint8_t)(n_total - skip);
if (want > (uint8_t)(SCREEN_W - col)) want = (uint8_t)(SCREEN_W - col);
if (want > 0) {
bios_set_place(row, col);
bios_fillcharattr(' ', cur_attr, want);
col = (uint8_t)(col + want);
need_place = 0;
} }
cc = (uint8_t)(cc_start + skip + want);
prev_ch = ' '; prev_ch = ' ';
} else { } else {
if (cc >= effective_vx && col < SCREEN_W) { if (cc >= effective_vx && col < SCREEN_W && run_len < sizeof(runbuf)) {
wrchar(col++, row, ch, cur_attr); runbuf[run_len++] = ch;
col++;
} }
cc++; cc++;
prev_ch = ch; prev_ch = ch;
} }
} }
flush_run(row, &col, runbuf, run_len, run_attr, &need_place);
/* Индикатор обрезки справа для nowrap-строки: /* Индикатор обрезки справа для nowrap-строки:
* если после текущей позиции есть ещё видимый контент, * если после текущей позиции есть ещё видимый контент,
* поверх последней колонки рисуем '>'. */ * поверх последней колонки рисуем '>'. */
if (is_nowrap(line_idx) && col >= SCREEN_W) { if ((rec.flags & IF_NOWRAP) && col >= SCREEN_W) {
uint32_t pp = p; uint32_t pp = p;
uint32_t left_bound = seg_off(line_idx); uint32_t left_bound = rec.off;
while (pp < seg_end) { while (pp < seg_end) {
char c = fb(pp); char c = fb(pp);
if (c == '\n' || c == '\r') break; if (c == '\n' || c == '\r') break;
@@ -1499,7 +1653,7 @@ static void render_line(uint16_t line_idx, uint8_t row)
} }
/* Индикатор скрытого контента слева при горизонтальном сдвиге. */ /* Индикатор скрытого контента слева при горизонтальном сдвиге. */
if (is_nowrap(line_idx) && viewport_x > 0) { if ((rec.flags & IF_NOWRAP) && viewport_x > 0) {
wrchar(0, row, '<', ATTR_TRUNC); wrchar(0, row, '<', ATTR_TRUNC);
} }
} }
@@ -1507,7 +1661,10 @@ static void render_line(uint16_t line_idx, uint8_t row)
* от предыдущего кадра рендера. */ * от предыдущего кадра рендера. */
{ {
uint8_t pad_attr = (parse_inline ? ATTR_TEXT : ATTR_TEXT_CODE); uint8_t pad_attr = (parse_inline ? ATTR_TEXT : ATTR_TEXT_CODE);
while (col < SCREEN_W) wrchar(col++, row, ' ', pad_attr); if (col < SCREEN_W) {
bios_set_place(row, col);
bios_fillcharattr(' ', pad_attr, (uint8_t)(SCREEN_W - col));
}
} }
} }
@@ -1539,7 +1696,8 @@ static void render_status_numbers(void)
uint8_t pct = calc_pct(); uint8_t pct = calc_pct();
textattr(ATTR_BAR); textattr(ATTR_BAR);
for (uint8_t c = 45; c < SCREEN_W; c++) wrchar(c, 0, ' ', ATTR_BAR); bios_set_place(0, 45);
bios_fillcharattr(' ', ATTR_BAR, (uint8_t)(SCREEN_W - 45));
wrchar(45, 0, 0xB3, ATTR_BAR); /* вертикальный разделитель */ wrchar(45, 0, 0xB3, ATTR_BAR); /* вертикальный разделитель */
gotoxy(47, 0); gotoxy(47, 0);
@@ -1684,16 +1842,16 @@ static void die(const char *msg)
* Строка s в кодировке CP866; остаток до края дополняется пробелами. */ * Строка s в кодировке CP866; остаток до края дополняется пробелами. */
static void help_line(uint8_t r, const char *s, uint8_t attr) static void help_line(uint8_t r, const char *s, uint8_t attr)
{ {
uint8_t x = HELP_X + 1u; uint8_t x = HELP_X + 1u;
uint8_t y = HELP_Y + 1u + r; uint8_t y = HELP_Y + 1u + r;
uint8_t i = 0; uint8_t len = (uint8_t)strlen(s);
while (s[i] && i < HELP_W - 2u) { if (len > HELP_W - 2u) len = HELP_W - 2u;
wrchar((uint8_t)(x + i), y, s[i], attr);
i++; bios_set_place(y, x);
} if (len > 0) bios_writeattr(s, len, attr);
while (i < HELP_W - 2u) { /* place уже продвинут bios_writeattr на len колонок (verified). */
wrchar((uint8_t)(x + i), y, ' ', ATTR_HELP_BG); if (len < HELP_W - 2u) {
i++; bios_fillcharattr(' ', ATTR_HELP_BG, (uint8_t)(HELP_W - 2u - len));
} }
} }
@@ -1704,14 +1862,14 @@ static void show_help(void)
uint8_t tlen = 8u; uint8_t tlen = 8u;
uint8_t lft = (uint8_t)((HELP_W - 2u - tlen) / 2u); /* = 27 */ uint8_t lft = (uint8_t)((HELP_W - 2u - tlen) / 2u); /* = 27 */
uint8_t rgt = (uint8_t)(HELP_W - 2u - tlen - lft); uint8_t rgt = (uint8_t)(HELP_W - 2u - tlen - lft);
uint8_t cx, i; /* Верхняя граница рамки с заголовком по центру.
* place продвигается каждым BIOS-вызовом (verified), поэтому
/* Верхняя граница рамки с заголовком по центру */ * достаточно одной установки места на всю строку рамки. */
wrchar(HELP_X, HELP_Y, 0xDA, ATTR_HELP_BDR); /* ┌ */ wrchar(HELP_X, HELP_Y, 0xDA, ATTR_HELP_BDR); /* ┌ */
cx = HELP_X + 1u; bios_set_place(HELP_Y, (uint8_t)(HELP_X + 1u));
for (i = 0; i < lft; i++, cx++) wrchar(cx, HELP_Y, 0xC4, ATTR_HELP_BDR); if (lft > 0) bios_fillcharattr(0xC4, ATTR_HELP_BDR, lft);
for (i = 0; i < tlen; i++, cx++) wrchar(cx, HELP_Y, title[i], ATTR_HELP_TIT); if (tlen > 0) bios_writeattr(title, tlen, ATTR_HELP_TIT);
for (i = 0; i < rgt; i++, cx++) wrchar(cx, HELP_Y, 0xC4, ATTR_HELP_BDR); if (rgt > 0) bios_fillcharattr(0xC4, ATTR_HELP_BDR, rgt);
wrchar(HELP_X + HELP_W - 1u, HELP_Y, 0xBF, ATTR_HELP_BDR); /* ┐ */ wrchar(HELP_X + HELP_W - 1u, HELP_Y, 0xBF, ATTR_HELP_BDR); /* ┐ */
/* Боковые границы (левый и правый │ для каждой строки тела) */ /* Боковые границы (левый и правый │ для каждой строки тела) */
@@ -1722,8 +1880,8 @@ static void show_help(void)
/* Нижняя граница рамки */ /* Нижняя граница рамки */
wrchar(HELP_X, HELP_Y + HELP_H - 1u, 0xC0, ATTR_HELP_BDR); /* └ */ wrchar(HELP_X, HELP_Y + HELP_H - 1u, 0xC0, ATTR_HELP_BDR); /* └ */
for (i = 1u; i < HELP_W - 1u; i++) bios_set_place((uint8_t)(HELP_Y + HELP_H - 1u), (uint8_t)(HELP_X + 1u));
wrchar(HELP_X + i, HELP_Y + HELP_H - 1u, 0xC4, ATTR_HELP_BDR); bios_fillcharattr(0xC4, ATTR_HELP_BDR, (uint8_t)(HELP_W - 2u));
wrchar(HELP_X + HELP_W - 1u, HELP_Y + HELP_H - 1u, 0xD9, ATTR_HELP_BDR); /* ┘ */ wrchar(HELP_X + HELP_W - 1u, HELP_Y + HELP_H - 1u, 0xD9, ATTR_HELP_BDR); /* ┘ */
/* Содержимое (20 внутренних строк) */ /* Содержимое (20 внутренних строк) */
@@ -1793,7 +1951,7 @@ int main(int argc, char **argv)
/* Сначала поднимаем UI, чтобы пользователь видел шапку/меню/спиннер /* Сначала поднимаем UI, чтобы пользователь видел шапку/меню/спиннер
* уже во время загрузки файла и построения индекса. */ * уже во время загрузки файла и построения индекса. */
set_videotextmode(TEXT_MODE_80x32); settextmode(TEXT_MODE_80x32);
clrscr_attr(ATTR_TEXT); clrscr_attr(ATTR_TEXT);
set_pallete(); set_pallete();
render_menu(); render_menu();
+180
View File
@@ -0,0 +1,180 @@
Новое требование к первоначальной подготовке документа.
Правила форматирования и показа -
1) Деление на параграфы. Параграфы разделяются пустыми строками.
2) Заголовки H1-H6 - отдельные параграфы (после них всегда пустые строки)
3) Разделители (---) - тоже - после них всегда пустые строки.
4) Таблица/код (через ```)/списки - это единый параграф.
Типы текста (строк) -
Обычный текст
Может в оригинале находиться на нескольких строках.
Строки могут разделяться
- обычный break - это когда на строке после текста идет перевод строки -
такой текст просто объединяется (в нашем случае если возможно то следующая строка продолжает
выводиться на той же строке что и предыдущая, только отделяется от нее уже не переводом строки
а через пробел.
- широкий break - это когда перед переводом строки есть два или более пробелов или перед переводом
строки находится символ обратный слэш '\' - в этом случае и когда мы выводим текст на экран следующая
строка начинается с новой строки.
- жесткий break - это когда две строки обычного текста разделяются двумя или более переводами
стрки (то есть между ними как минимум есть одна пустая строка) - в этом случае вторая строка
будет считаться новым параграфом и отделяться от предыдущей строки ОДНОЙ пустой строкой (вне
зависимости от того сколько пустых строк в оригинале.
Должно сохраняться действие модификатора bold/italic/strike начатое на одной строке на следующие
если они так же находятся в этом параграфе. Новый параграф (после пустой строки) теряет воздействие
незакрытого модификатора из предыдущего параграфа.
Тип переноса строк - Wrap.
Заголовки -
абзацы из одной оригинальной строки. Начальный стиль зависит от типа заголовка (в нем могут встречаться
модификаторы bold/italic/code/strike.
Тип переноса строк - Wrap.
Разделитель (horizontal rules) -
Всегда одна строка. После нее выводим пустую строку всегда (следующий текст - новый абзац)
Списки
как и в обычном тексте - один пункт списка может находиться в оригинале на нескольких строках.
если появляется новая строка с префиксом пункта списка - это означает что с этой строки начинается
новый пункт списка. если появляется пустая строка - это означает что следующая за пустой строкой
непустая строка уже не является продолжением пункта списка.
Если следующая за пустой строкой строка так же является пунктом списка то такая строка считается
продолжением текущего списка. Только если две строки списков разделены ДВУМЯ и более пустыми
строками то вторая строка с пунктом списка будет считаться началом нового списка -
Пример -
- строка 1
- строка 2
строка 3
- строка 4
- строка 5
- строка 6
Должно отображаться так -
- строка 1
- строка 2 строка 3
- строка 4
- строка 5
- строка 6
Тип переноса строк - Wrap.
Замечание - перенесенная строка продолжение пункта списка должна начинаться с отступа в
несколько пробелов (для списка первого уровня - два пробела) - то есть с той же позиции
что и начальный текст строки -
Пример -
- длинная строка которая не может поместиться и будет перенесена по слову 'будет'
Должно отображаться так -
- длинная строка которая не может поместиться и
будет перенесена по слову 'будет'
Незакрытые модификаторы типа (bold/etc.) не переносят свою модификацию не последующие пункты списка.
Действуют только в пределах одного пункта.
(то есть фактически - пункт списка - это отдельный параграф но следующий пункт списка (тоже отдельный
параграф) не отделяется от него пустой строкой.
Вложенные списки - поддерживаются.
Для отображения префиксов пунктов списка используется свой стиль. Так же для текста списков используется
отдельный (от обычного текста) стиль.
Блок Кода -
Весь блок кода отображается только одним стилем - стилем Кода. В нем не действуют модификаторы
bold/italic/etc (возможно в дальнейшей использование парсера языка для кода что бы отобразить
этот код с подсветкой синтаксиса этого языка, но сейчас весь блок рисуется только одним стилем)
После блока кода - обязательна пустая строка.
Тип переноса строк - UnWrap. - То есть строки НЕ ПЕРЕНОСЯТСЯ.
Quoted -
Отображается как отдельный параграф. Перед отображением текста отображается символ префикса Цитирования
Способы переноса текста аналогичны обычному тексту, за исключение того что перенесенные строки так же
предваряются префиксом Цитирования
Для отображения префиксов Цитирования используется свой стиль. Так же для текста цитирования используется
отдельный (от обычного текста) стиль.
Тип переноса строк - Wrap.
Пример форматирования -
> Первый параграф
>
> Второй параграф
> > Вложенный параграф
>
> Продолжение основной цитаты - длинная строка (переносится по слову 'строка')
Будет отображаться так -
| Первый параграф
|
| Второй параграф
| | Вложенный параграф
|
| Продолжение основной цитаты - длинная
| строка (переносится по слову 'строка')
Таблицы -
Пока отображаются as is.
Дальше возможно предусмотрим вариант вычисления ширины столбцов и форматирование
вывода что бы все ячейки столбца имели одинаковую ширину.
Тип переноса строк - UnWrap.
Типы переноса строк -
Wrap -
происходит перенос текста с одной строки на другую по разделителям пробелам (предусмотреть
во второй версии возможность использовать разделителем знаков точка, запятая, восклицательный
знак, вопросительный знак, дефис. Замечание - новая строка не может начинаться со знаков
разделителей - то есть если у нас идет многоточие (три точки) то нельзя что бы одна точка
была в конце первой строки а остальные две на другой - если идут несколько разделителей подряд
то они считаются как один и должны оставаться на первой строке).
UnWrap -
Текст НЕ ПЕРЕНОСИТСЯ.
Если строка не помещается на экране - то в конец строки на экране выводим символ-знак наличия
продолжения строки справа за краем экрана (свой стиль для этого символа).
Символ '>'.
Если есть хотя бы одна строка которая не помещается на экране - то разрешаем горизонтальный
скроллинг.
ЗАМЕЧАНИЕ - скроллируется только текст выводящийся в режиме UnWrap.
Если произведен скроллинг влево (строки UnWrap начинаются показываться не с первой позиции)
то на первой позиции отображем другой символ '<' сообщающий пользователю что есть текст
за левым краем экрана.
Скроллирование не бесконечно - если на экране нет строк UnWrap для которых есть скрытый текст
за правым краем экрана то скроллирование влево больше не возможно, и наоборот - если при
скроллировании вправо дошли до показа строк UnWrap с первой позиции то дальше скроллинг в этом
направлении так же невозможен. Скроллинг идет с грануляцией по 8 символов.
(сейчас UnWrap текст это только блоки кода и таблицы).
Проанализируй данную постановку задачи.
Что требуется -
Мы более не поддерживаем два режима показа - Wrap/UnWrap - только один (смешанный).
Потому вопрос - надо ли нам сохранять оригинальный контент считанного файла с диска ?
Или при подготовке данных считываемых с диска можно сразу преобразовывать его в формат
готовый для показа (объединять строки в одном параграфе и так далее) ?
Ограничения - не забывать о том что это все работает на компьютере с 8-ми битным процессором.
Потому очень экономно относимся к памяти и лишней работе процессора - все должно быть весьма
быстрым и компактным.
Требования для работы с файлами 128 Кб сохраняется.
Для версии 2 -
Подумай о возможности использования банков памяти не только для содержимого файла а например
для буферов типа line_offset/init_style и прочих (тогда их размер может так же быть динамическим
и чем больше банков памяти будет использовано для чтения файла тем больше станут размеры этих
буферов.