diff --git a/examples/mdview/docs/mdview-модель-документа-и-рендеринг.md b/examples/mdview/docs/mdview-модель-документа-и-рендеринг.md new file mode 100644 index 0000000..df7e910 --- /dev/null +++ b/examples/mdview/docs/mdview-модель-документа-и-рендеринг.md @@ -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` (строки 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 и прочие типы строк. diff --git a/examples/mdview/mdview — унифицированный рендеринг (Unified Wrap + Paragraph Model).md b/examples/mdview/mdview — унифицированный рендеринг (Unified Wrap + Paragraph Model).md new file mode 100644 index 0000000..cf30db4 --- /dev/null +++ b/examples/mdview/mdview — унифицированный рендеринг (Unified Wrap + Paragraph Model).md @@ -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 +План реализуется одним агентом (последовательно в одном файле), параллелизм не требуется. Дочерние агенты не используются. \ No newline at end of file diff --git a/examples/mdview/mdview.c b/examples/mdview/mdview.c index 0a0bbe5..b6d8219 100644 --- a/examples/mdview/mdview.c +++ b/examples/mdview/mdview.c @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -43,7 +44,7 @@ #define PAGE_MASK ((uint16_t)(PAGE_SIZE - 1u)) #define MAX_PAGES 8 /* 8 страниц × 16 КБ = 128 КБ */ #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-страница */ /* ---- Палитра атрибутов ------------------------------------------- */ @@ -172,21 +173,25 @@ static uint8_t spinner_active = 0; * ================================================================== */ /* Печатает строку с указанным атрибутом, начиная с позиции (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) { - while (*s && x < SCREEN_W) { - wrchar(x++, y, *s++, attr); - } + uint8_t maxlen = (x < SCREEN_W) ? (uint8_t)(SCREEN_W - x) : 0; + 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. */ static void fill_row(uint8_t y, uint8_t attr) { - for (uint8_t c = 0; c < SCREEN_W; c++) { - wrchar(c, y, ' ', attr); - } + bios_set_place(y, 0); + 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) { uint32_t last_space = 0xFFFFFFFFu; + uint8_t last_space_style = line_style; /* снимок line_style на момент last_space */ char prev_ch = ' '; 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 == ' ') { last_space = q + 1; + last_space_style = line_style; seg_col++; q++; prev_ch = ' '; } else { seg_col++; q++; prev_ch = ch; } if (seg_col >= SCREEN_W) { - uint32_t wrap_at = (last_space != 0xFFFFFFFFu && last_space > cur_seg_off) - ? last_space : q; + uint8_t use_ls = (uint8_t)(last_space != 0xFFFFFFFFu && last_space > cur_seg_off); + uint32_t wrap_at = use_ls ? last_space : q; 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; + /* Откатываем line_style к снимку на момент wrap_at: диапазон + * [wrap_at, q_old) будет пересканирован заново, и переключающие + * символы (обратная кавычка, звёздочка, подчёркивание, тильда) + * внутри него иначе применились бы дважды — именно так возникал + * баг: code-маркер на границе переноса ломает цвет следующего слова. */ + line_style = wrap_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) { uint32_t last_space = 0xFFFFFFFFu; + uint8_t last_space_style = *io_line_style; /* снимок line_style на момент last_space */ char prev_ch = ' '; uint8_t seg_col = col; 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 (soft_break) { last_space = q; + last_space_style = line_style; seg_col++; } else { last_space = q + 1; + last_space_style = line_style; seg_col++; q++; } @@ -981,14 +998,22 @@ static uint32_t scan_join_stream(uint32_t q, uint32_t para_start, uint8_t col, prev_ch = ch; } if (seg_col >= SCREEN_W) { - uint32_t wrap_at = (last_space != 0xFFFFFFFFu && last_space > cur_seg_off) - ? last_space : q; + uint8_t use_ls = (uint8_t)(last_space != 0xFFFFFFFFu && last_space > cur_seg_off); + uint32_t wrap_at = use_ls ? last_space : q; 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; last_space = 0xFFFFFFFFu; prev_ch = ' '; 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; } +/* Сбрасывает накопленный "прогон" из *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) { + /* Один 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 base_attr = ATTR_TEXT; - uint8_t cur_attr = is_code_body(line_idx) ? ATTR_TEXT_CODE : get_init_style(line_idx); - uint8_t emph = is_code_body(line_idx) ? EM_CODE : get_init_style_raw(line_idx); + uint8_t is_code = (uint8_t)(have_rec && (rec.flags & IF_CODE)); + 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: выводим код как есть */ - if (is_blank(line_idx)) { - while (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT); + if (have_rec && (rec.flags & IF_BLANK)) { + bios_set_place(row, col); + bios_fillcharattr(' ', ATTR_TEXT, (uint8_t)(SCREEN_W - col)); return; } - if (line_idx < n_lines) { - uint32_t p = seg_off(line_idx); + if (have_rec) { + uint32_t p = rec.off; 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; uint8_t kind; uint8_t quote_stream = 0; /* 1: текущий сегмент относится к потоку цитаты */ @@ -1265,11 +1333,16 @@ static void render_line(uint16_t line_idx, uint8_t row) /* Для сегмента-продолжения повторяем префикс/отступ * от первого не-CONT сегмента логической строки. */ if (cont) { - uint16_t first = line_idx; + uint16_t first = line_idx; + idx_rec_t rec_first = rec; /* rec.flags здесь уже содержит IF_CONT */ /* Для любого сегмента-продолжения откатываемся к первому - * НЕ-cont сегменту этой логической строки. */ - while (first > 0 && is_cont(first)) first--; - uint32_t p_start = seg_off(first); + * НЕ-cont сегменту этой логической строки — один idx_get() + * на шаг вместо двух (is_cont + повторный seg_off). */ + 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; uint8_t first_kind = classify_line(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 корректно пропускать сырой `>`. */ quote_stream = 1; uint8_t q = 0; - while (q + 2 < content_off && col < SCREEN_W && fb(p_start + q) == ' ') { - wrchar(col++, row, ' ', ATTR_TEXT); - q++; + while (q + 2 < content_off && q < SCREEN_W && fb(p_start + q) == ' ') q++; + if (q > (uint8_t)(SCREEN_W - col)) q = (uint8_t)(SCREEN_W - col); + 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, ' ', ATTR_TEXT); } else if (first_kind == LK_ULIST || first_kind == LK_OLIST) { - uint8_t q = 0; - while (q < indent && col < SCREEN_W) { - wrchar(col++, row, ' ', ATTR_TEXT); - q++; + uint8_t q = indent; + if (q > (uint8_t)(SCREEN_W - col)) q = (uint8_t)(SCREEN_W - col); + if (q > 0) { + bios_set_place(row, col); + bios_fillcharattr(' ', ATTR_TEXT, q); + col = (uint8_t)(col + q); } } - /* Дальше рендерим контент именно с текущего сегмента-продолжения. */ - p = seg_off(line_idx); - content_off = p; - /* Для сегмента-продолжения парсер работает как обычно. */ + /* p/content_off уже равны rec.off — внутри cont-блока они не + * менялись (там свои локальные тени с тем же именем), + * пересчитывать через seg_off() заново не нужно. */ } /* Кодовый блок: * - строка-разделитель показывается как пустая; - * - тело рисуется «как есть» с ATTR_TEXT_CODE без inline-парсинга. */ - if (is_fence_delim(line_idx)) { - while (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT); + * - тело рисуется «как есть» с ATTR_TEXT_CODE без inline-парсинга. + * is_fence_delim(line_idx) разворачено на месте: cont и p уже + * известны, повторный 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; } - if (is_code_body(line_idx)) { + if (is_code) { base_attr = ATTR_TEXT_CODE; cur_attr = ATTR_TEXT_CODE; parse_inline = 0; @@ -1321,7 +1402,8 @@ static void render_line(uint16_t line_idx, uint8_t row) } 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; } 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) { /* Базовая поддержка вложенности: ведущие пробелы сохраняем, * затем рисуем маркер списка и пробел перед содержимым. */ - uint32_t q = p; - while (q + 2 < content_off && col < SCREEN_W && fb(q) == ' ') { - wrchar(col++, row, ' ', ATTR_TEXT); - q++; + uint32_t q = p; + uint32_t qmax = p + SCREEN_W; + while (q + 2 < content_off && q < qmax && fb(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, ' ', ATTR_TEXT); @@ -1347,24 +1434,54 @@ static void render_line(uint16_t line_idx, uint8_t row) } else if (kind == LK_OLIST) { /* Отступ -> пробелы; цифры и '.' / ')' рисуем как маркер; * завершающий пробел — обычным атрибутом текста. */ - uint32_t q = p; - while (q < content_off && col < SCREEN_W && fb(q) == ' ') { - wrchar(col++, row, ' ', ATTR_TEXT); - q++; + uint32_t q = p; + uint32_t qmax = p + SCREEN_W; + while (q < content_off && q < qmax && fb(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) { - wrchar(col++, row, fb(q), ATTR_LIST_MARKER); - q++; + /* Маркер (цифры/'.'/')') читается из fb() — это окно 3, + * BIOS принимает указатели только из #4000..#BFFF, поэтому + * сначала копируем во временный буфер на стеке (окно 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); p = content_off; } else if (kind == LK_QUOTE) { /* Отступ -> пробелы; затем quote-префикс '│' (0xB3), * пробел и далее содержимое. */ - uint32_t q = p; - while (q + 1 < content_off && col < SCREEN_W && fb(q) == ' ') { - wrchar(col++, row, ' ', ATTR_TEXT); - q++; + uint32_t q = p; + uint32_t qmax = p + SCREEN_W; + while (q + 1 < content_off && q < qmax && fb(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, ' ', ATTR_TEXT); @@ -1372,11 +1489,25 @@ static void render_line(uint16_t line_idx, uint8_t row) } /* Эффективный горизонтальный сдвиг: применяется только к nowrap-строкам. */ - uint8_t effective_vx = is_nowrap(line_idx) ? viewport_x : 0; + uint8_t effective_vx = (rec.flags & IF_NOWRAP) ? viewport_x : 0; /* cc — видимая колонка внутри области контента * (после фиксированного префикса строки). */ uint8_t cc = 0; 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) { uint8_t soft_join = 0; /* 1: вставлен виртуальный пробел мягкой склейки, p уже сдвинут */ char ch = fb(p); @@ -1438,16 +1569,23 @@ static void render_line(uint16_t line_idx, uint8_t row) char lit_ch = 0; uint8_t mk = handle_inline_marker(ch, &p, seg_end, prev_ch, &emph, &lit_ch); 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); + run_attr = cur_attr; continue; } if (mk == 2) { - if (cc >= effective_vx && col < SCREEN_W) { - wrchar(col++, row, lit_ch, cur_attr); + if (cc >= effective_vx && col < SCREEN_W && run_len < sizeof(runbuf)) { + runbuf[run_len++] = lit_ch; + col++; } cc++; - if (cc >= effective_vx && col < SCREEN_W) { - wrchar(col++, row, lit_ch, cur_attr); + if (cc >= effective_vx && col < SCREEN_W && run_len < sizeof(runbuf)) { + runbuf[run_len++] = lit_ch; + col++; } cc++; prev_ch = lit_ch; @@ -1457,28 +1595,44 @@ static void render_line(uint16_t line_idx, uint8_t row) if (!soft_join) p++; if (ch == '\t') { - /* Таб расширяется в координатах контента (cc). */ - uint8_t tgt = (uint8_t)((cc & (uint8_t)~(TAB_STOP - 1)) + TAB_STOP); - while (cc < tgt && col < SCREEN_W) { - if (cc >= effective_vx) wrchar(col++, row, ' ', cur_attr); - cc++; + /* Таб расширяется в координатах контента (cc). + * Сначала сбросить накопленный текстовый прогон — таб + * выводится отдельным bios_fillcharattr(), не через runbuf. */ + flush_run(row, &col, runbuf, run_len, run_attr, &need_place); + 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 = ' '; } else { - if (cc >= effective_vx && col < SCREEN_W) { - wrchar(col++, row, ch, cur_attr); + if (cc >= effective_vx && col < SCREEN_W && run_len < sizeof(runbuf)) { + runbuf[run_len++] = ch; + col++; } cc++; prev_ch = ch; } } + flush_run(row, &col, runbuf, run_len, run_attr, &need_place); /* Индикатор обрезки справа для nowrap-строки: * если после текущей позиции есть ещё видимый контент, * поверх последней колонки рисуем '>'. */ - if (is_nowrap(line_idx) && col >= SCREEN_W) { + if ((rec.flags & IF_NOWRAP) && col >= SCREEN_W) { uint32_t pp = p; - uint32_t left_bound = seg_off(line_idx); + uint32_t left_bound = rec.off; while (pp < seg_end) { char c = fb(pp); 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); } } @@ -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); - 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(); 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); /* вертикальный разделитель */ gotoxy(47, 0); @@ -1684,16 +1842,16 @@ static void die(const char *msg) * Строка s в кодировке CP866; остаток до края дополняется пробелами. */ static void help_line(uint8_t r, const char *s, uint8_t attr) { - uint8_t x = HELP_X + 1u; - uint8_t y = HELP_Y + 1u + r; - uint8_t i = 0; - while (s[i] && i < HELP_W - 2u) { - wrchar((uint8_t)(x + i), y, s[i], attr); - i++; - } - while (i < HELP_W - 2u) { - wrchar((uint8_t)(x + i), y, ' ', ATTR_HELP_BG); - i++; + uint8_t x = HELP_X + 1u; + uint8_t y = HELP_Y + 1u + r; + uint8_t len = (uint8_t)strlen(s); + if (len > HELP_W - 2u) len = HELP_W - 2u; + + bios_set_place(y, x); + if (len > 0) bios_writeattr(s, len, attr); + /* place уже продвинут bios_writeattr на len колонок (verified). */ + if (len < HELP_W - 2u) { + 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 lft = (uint8_t)((HELP_W - 2u - tlen) / 2u); /* = 27 */ 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); /* ┌ */ - cx = HELP_X + 1u; - for (i = 0; i < lft; i++, cx++) wrchar(cx, HELP_Y, 0xC4, ATTR_HELP_BDR); - for (i = 0; i < tlen; i++, cx++) wrchar(cx, HELP_Y, title[i], ATTR_HELP_TIT); - for (i = 0; i < rgt; i++, cx++) wrchar(cx, HELP_Y, 0xC4, ATTR_HELP_BDR); + bios_set_place(HELP_Y, (uint8_t)(HELP_X + 1u)); + if (lft > 0) bios_fillcharattr(0xC4, ATTR_HELP_BDR, lft); + if (tlen > 0) bios_writeattr(title, tlen, ATTR_HELP_TIT); + if (rgt > 0) bios_fillcharattr(0xC4, ATTR_HELP_BDR, rgt); 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); /* └ */ - for (i = 1u; i < HELP_W - 1u; i++) - wrchar(HELP_X + i, HELP_Y + HELP_H - 1u, 0xC4, ATTR_HELP_BDR); + bios_set_place((uint8_t)(HELP_Y + HELP_H - 1u), (uint8_t)(HELP_X + 1u)); + 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); /* ┘ */ /* Содержимое (20 внутренних строк) */ @@ -1793,7 +1951,7 @@ int main(int argc, char **argv) /* Сначала поднимаем UI, чтобы пользователь видел шапку/меню/спиннер * уже во время загрузки файла и построения индекса. */ - set_videotextmode(TEXT_MODE_80x32); + settextmode(TEXT_MODE_80x32); clrscr_attr(ATTR_TEXT); set_pallete(); render_menu(); diff --git a/examples/mdview/todo2 b/examples/mdview/todo2 new file mode 100644 index 0000000..0244b94 --- /dev/null +++ b/examples/mdview/todo2 @@ -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 и прочих (тогда их размер может так же быть динамическим +и чем больше банков памяти будет использовано для чтения файла тем больше станут размеры этих +буферов. \ No newline at end of file