ChangeLog:

- big commit.
This commit is contained in:
2026-06-10 10:35:48 +03:00
parent f87b52bb7f
commit 858e5755ad
20 changed files with 411 additions and 1347 deletions
+31 -2
View File
@@ -1,10 +1,39 @@
# Build mdview.exe — Markdown viewer for Sprinter.
#
# small memory mode: code in W1, data/stack/heap in W2 (32 KB total).
# W3 stays free for the file buffer (EMM-mapped) and Phase 3+ render cache.
# W3 stays free for the file buffer (EMM-mapped).
PROJ_ROOT := $(abspath $(CURDIR)/../..)
EXAMPLE := mdview
MEMORY := small
EXTRA_DATA := SAMPLE.MD PLAN_866.md SAMPLEF.MD
include $(PROJ_ROOT)/app.mk
# ------------------------------------------------------------------
# Образ дискеты: только mdview.exe + README.MD (перекодированный
# из UTF-8 в CP866 — рабочую кодировку Sprinter).
#
# README.MD хранится в репозитории в UTF-8; iconv -c конвертирует
# его в CP866, отбрасывая символы без аналога в целевой кодировке.
# Результат кладётся в .disk_tmp/README.MD, чтобы make_disk.py
# использовал правильное имя файла на диске.
# ------------------------------------------------------------------
DISK_TMP := .disk_tmp
README_DISK := $(DISK_TMP)/README.MD
$(DISK_TMP):
mkdir -p $@
$(README_DISK): README.MD | $(DISK_TMP)
iconv -c -f UTF-8 -t CP866 README.MD > $@
floppy: $(EXAMPLE).exe $(README_DISK)
python3 $(MAKE_DISK) $(FLOPPY_IMG) $(EXAMPLE).exe $(README_DISK)
@echo
@echo "Floppy ready: $(FLOPPY_IMG)"
@echo "Run: cd $(MAME_DIR) && ./run_mame.sh"
clean:
rm -rf .sprinter-cc-* $(EXAMPLE).exe $(DISK_TMP)
.PHONY: all clean floppy run
-570
View File
@@ -1,570 +0,0 @@
# План: текстовый Markdown viewer для Sprinter (`examples/mdview`)
## Context
Тестовая крупная задача — проверить нашу libc на нетривиальном interactive-приложении (полноэкранный UI, файловый I/O, парсер). Параллельно даст хороший showcase платформы и поможет вытащить недоделки в conio/io. Конечная цель: viewer для `.md` файлов с подсветкой синтаксиса, навигацией по тексту и постраничным скроллингом.
**Ограничения v1 (зафиксированы пользователем):**
- POSIX file API (open/read/lseek/close); FILE pointer / fread / fgets — не использовать
- Подсветка через цвет: **размер заголовка** → цвет шрифта; **bold/italic** → цвет фона (моноширинный фонт без жирного/курсивного начертания)
**Расширения, реализованные после плана:**
- Файлы до 128 KB (18 EMM-страниц, lazy map в W3 через `fb()/map_page()`) — изначально было в v2
- Анимированный spinner и предварительная отрисовка UI при старте — UX
- Inline emphasis: `_`/`*`/`**` подчиняются XOR flanking-правилу (whitespace ровно с одной стороны), `COLOR_YELLOW` и `2 * 3` остаются литералами
---
## Текущий статус (2026-06-05)
| Phase | Статус | Комментарий |
|---|---|---|
| 1 Plain text + nav | ✓ | загрузка, индексация, status/menu, ↑↓/PgUp/PgDn/Home/End/F1/F10/Esc |
| 2 Headers + HR | ✓ | H1..H4, `---`/`***`/`___` (с ≥3 marker'ов) |
| 3 Inline emphasis | ✓ | `**` / `*` / `_` / `` ` ``; XOR flanking (см. выше) |
| 4 Lists / quote / fenced code | ✓ | `- / * / +`, `N. / N)`, `> `, ``` ``` ```; light nested lists |
| 4-tables | ✗ | таблицы отложены вместе с Phase 6 |
| 5 Wrap / Unwrap (F2) | ✓ | wrap-by-default; soft wrap; F2 переключает; hpan ←/→ в truncate-режиме |
| 6 Полный layout таблиц | ✗ | deferred |
| 7 Links + search | ✗ | deferred |
| 8 F8 Raw toggle | ✗ | deferred |
| Cache рендеренных строк | ✗ | не нужно по скорости |
**UX-поправки (отдельно от phase-плана, 2026-06-05):**
- UI (menu + title bar) отрисовывается ДО `load_file`/`index_lines` — пользователь сразу видит интерфейс, а не чёрный экран
- Title bar: `MDVIEW <spinner> <filename>` (3 пробела между MDVIEW и filename, slot спиннера — col 8)
- Spinner крутится во время `load_file` (по странице) и `index_lines` (раз в 32 логических строки), включая `toggle_wrap`
---
## Архитектура
### Раскладка экрана (80×32, текст mode 0x03)
```
Row 0: ┃ MDVIEW │ mdview.md │ L 1-30 / 142 │ 21% ┃ ← status bar (BG=blue, FG=white)
▲ ▲
│ └── filename @ col 10
└────────── spinner slot @ col 8 (anim. while busy)
Row 1: ┃ ┃
... ┃ document viewport (30 rows) ┃
Row 30: ┃ ┃
Row 31: ┃ F1 Help F2 Wrap F10 Exit ┃ ← menu bar (BG=blue, FG=cyan)
```
- Viewport = 30 строк × 80 столбцов.
- Status / menu рендерятся через `wrchar()` (без авто-скролла), viewport — через `LOCATE` + посимвольный `wrchar()` (тоже без авто-скролла, ставит и char и attr за один call).
### Память
Memory mode: **`small`** — DSS отводит под наш образ **два банка (W1 + W2)** = 32 KB суммарно (CODE в W1, DATA + STACK + HEAP в W2). Этого должно хватить чтобы НЕ заводить `__banked` функции. W3 остаётся полностью свободным для маппинга больших буферов:
```
W0 (0x0000-0x3FFF): ESTEX (system, untouchable)
W1 (0x4000-0x7FFF): CODE (small mode page 1)
W2 (0x8000-0xBFFF): DATA + STACK + HEAP (small mode page 2)
W3 (0xC000-0xFFFF): paged window — лениво переключается между EMM-страницами
файла (до 8 страниц = 128 KB). `cur_page` кэширует
текущую mapping, `fb(p)` маппит нужную страницу при
первом обращении.
```
Почему small + W3:
- 32 KB на код+данные с большим запасом → нет банкинга
- W3 — стандартный paged window, под него у нас уже есть `bank_io_w3` API
- Файлы до 128 KB поддерживаются нативно: `mem_alloc_pages(pages_needed)` под весь файл; `map_page()` через `sprinter_page_w3()`; `fb(p)` — единая точка доступа из индексатора и рендера.
Статики (в W2):
- `line_offset[MAX_LINES]` — uint16_t смещение каждой строки в файловой странице (4 KB на 2048 строк)
- `cache_tag[CACHE_N]` — uint16_t тег слота (200 байт на 100 слотов, появляется в Phase 3)
- `filename[64]`, `top_line`, `total_lines`, `file_size`, `file_blk`, `cache_blk` — единицы байт
**FILE_BUF**: `((char*)0xC000)` — фиксированная адресация в W3 после маппинга нужной страницы.
### Поток данных
```
main → open() → read() chunks 1KB → write to W1 (mapped EMM page) → close()
→ index_lines() (одно сканирование, заполняет line_offset[])
→ render_viewport() + main loop { getkey(); handle(); render_status(); render_viewport() }
```
**Тонкость с `read()`**: ESTEX READ записывает по dst-указателю в адресном пространстве вызывающего. Поскольку мы маппим EMM-страницу в W3 (0xC000) ДО вызова read(), указатель 0xC000+offset валиден. Если выяснится что BIOS трогает W3 во время read (графический видеобуфер по умолчанию в W3 при графических режимах, но в текстовом — должен быть свободен) — fallback: читать в 1 KB буфер в W2 и копировать в W3 через `bank_write_w3()`.
---
## Реализация — поэтапная
### Phase 1 — Plain text viewer (MVP) ✓
**Что работает:**
- Загрузка файла (`open/read/close`) в W1-страницу
- Индексация строк (LF / CRLF разделители)
- Status bar: имя файла, L N-M / Total, процент скроллинга
- Menu bar: `F1 Help F10 Exit`
- Навигация: ↑/↓ (1 строка), PgUp/PgDn (30 строк), Home/End (начало/конец), Esc/F10 (выход), F1 (help screen)
- Обрезка строк длиннее 80 символов (без word-wrap)
- Цвета: текст белый на чёрном; status/menu — белый на синем
**Критичные файлы:**
- `examples/mdview/mdview.c` — main, key loop, rendering, indexing (one-file MVP)
- `examples/mdview/Makefile`
- `examples/mdview/SAMPLE.MD` — тестовый markdown файл
### Phase 2 — Headers и горизонтальная линия ✓
**MD фичи:**
- `# H1` → ярко-жёлтый (COLOR_YELLOW = 14) на чёрном
- `## H2` → ярко-голубой (COLOR_LBLUE = 11)
- `### H3` → ярко-зелёный (COLOR_LGREEN = 10)
- `#### H4+` → серый (COLOR_GREY = 8)
- `---` / `***` на отдельной строке → линия 0xC4 (горизонтальная рамка ASCII) во всю ширину
### Phase 3 — Inline emphasis ✓
**Парсер inline (per-line, runs в одну строку):**
- `**bold**` → ATTR_TEXT_BOLD
- `*italic*` → ATTR_TEXT_ITALIC
- `_underscore_` → ATTR_TEXT_UNDERSORE
- `` `code` `` → ATTR_TEXT_CODE
- Маркеры `**`/`*`/`_`/`` ` `` НЕ рендерятся (съедаются)
State machine: один активный стиль одновременно (без вложенности); конфликтующий маркер при чужом активном стиле всё равно консьюмится (zero-width) для синхронизации ширины с index_lines. Состояние сбрасывается на каждой строке.
**Flanking-правило (CommonMark intraword, реализовано после изначального плана):**
- `*` / `**` / `_` считаются markdown-маркером только если whitespace/EOL
ровно с ОДНОЙ стороны (XOR).
- Случаи "оба whitespace" (`2 * 3`, `2 ** 3`) → литералы (арифметика).
- Случаи "ни одного whitespace" (`COLOR_YELLOW`, `FILE*/fread`, `foo*bar*baz`)
→ литералы (intraword).
- Backtick (`` ` ``) flanking НЕ требует — `` `code` `` работает без пробелов.
- Правило применено симметрично в 4 местах (`index_lines`, cont-render,
основной inline-парсер, truncation peek), иначе wrap-индексатор и
рендер разъедутся по ширине.
> **Кэш отформатированных строк** — отложен в самый конец, см. "Phase ∞: оптимизации".
> Скорости текущего наивного рендера хватает на 80×30 = 2400 wrchar / кадр; PgUp/PgDn визуально мгновенен.
### Phase 4 — Block elements ✓ (без таблиц)
- Маркированные списки: `- foo`, `* foo`, `+ foo` → префикс `` (0x07) + пробел; цвет маркера ярче основного
- Нумерованные списки: `1. foo`, `2. foo` → как есть (число оставляем)
- Blockquote: строки с `> ` → префикс `` (0xB3) серого цвета, остальной текст слегка приглушённый
- Fenced code blocks: `` ``` `` открывает/закрывает блок; все строки между — bg=серый, моноширинно (без inline-парсинга)
- Indented code blocks (4+ пробелов): аналогично fenced, но без явного маркера
**Light nested lists (v1 — реализовано):**
- `classify_line()` пропускает leading spaces перед ulist/olist/quote маркером,
возвращает `content_off` после маркера → `content_off - p_start` = indent + marker
бит в visible col.
- `render_line()` рисует leading-spaces в `ATTR_TEXT`, потом маркер на сдвинутой
позиции (col = indent). Marker всё ещё фиксирован при горизонтальном pan'е.
- HR / header / fence delim остаются строго col-0 (CommonMark разрешает до 3
ведущих пробелов для них — упростили).
- Tab-indent → не распознаётся как nesting (только spaces).
**Phase 4-full — полная поддержка вложенности (deferred):**
- **Tab-indent**: считать tab = 4 пробела для определения уровня.
- **Quote nesting** (`> > foo`): каждый `>` подряд = +1 уровень, каждый рисуется
отдельным `` в `ATTR_QUOTE_MARKER` (визуальная "лестница" слева).
- **Hanging indent в wrap-continuation**: когда `- some very long bullet text
that wraps...` — continuation seg должен начинаться от content-col (после
маркера), а не от col 0. Сейчас continuation идёт от col 0 (v1 simplification).
Требует хранить `marker_width` per логическая строка (8 бит) или re-classify
first seg при рендере continuation.
- **Lazy continuation**: строки без маркера, но с правильным indent под
предыдущим bullet'ом, должны считаться продолжением того bullet'а
(визуально — общий attr).
- **Strict CommonMark indent rules**: вложенный пункт должен быть на indent
≥ content_col родителя, иначе считается breakout. Нужен мини-stack
активных списков при индексации.
### Phase 5 — Wrap / Unwrap длинных строк ✓
Дефолт: **wrap on**. F2 переключает; в меню-баре подпись отражает действие
("Unwrap" когда wrap включён, "Wrap" когда выключен). Во время реиндексации
крутится спиннер на title bar.
**v1 — реализовано:**
- Один массив `line_offset[2048]` хранит ВИДИМЫЕ сегменты (а не логические
строки); биты 0..13 — байтовое смещение, бит 15 — CONT-флаг continuation.
- Wrap-режим: soft wrap на последнем пробеле ≤ 80; hard fallback если
пробела нет.
- Маркеры эмфазиса (`**`/`*`/`_`/`` ` ``) и header-префиксы (`#`/`##`/...) не
учитываются в visible-col при поиске точки переноса.
- "Специальные" логические строки не wrap'аются вообще (одна seg-запись на
логическую строку): fence delim, table row (header/separator/body), HR.
- F2 toggle сохраняет визуальную позицию через `top_offset` в FILE_BUF.
- Bitmaps (`in_code`/`in_table`/`is_tab_hdr`) перестраиваются вместе с
сегментами, индексируются seg-индексом, биты ставятся только на первом
seg'е логической строки.
- Continuation-сегменты рендерятся в стиле "v1: плоско" — plain text,
никаких markdown-классификаций; padding до конца строки `ATTR_TEXT`.
**v1.5 — отложено для полной картины wrap:**
- **Hanging indent**: continuation от ulist/olist/quote должен выравниваться
под content, а не от col 0. Требует хранить marker_width per логическая
строка ИЛИ re-classify первого seg'а при рендере continuation.
- **Наследование base_attr**: continuation от header'а должен сохранять
цвет; continuation от code body — фон ATTR_TEXT_CODE. Требует хранить
1 байт `base_attr` per seg ИЛИ lookup первого seg'а.
- **Inline emphasis через границу**: эмфазис, открытый в первом seg'е и не
закрытый, должен продолжаться во втором. Требует хранить emph state per
seg (3 бита).
- Compact way: добавить параллельный массив `seg_meta[MAX_SEGS]` по 1 байту
— пакует marker_width (4 бита) + emph_state (3 бита) + base_attr_idx
(4 бита из таблицы → нужен 2-байтовый seg_meta).
- **Hpan для длинных строк**: если wrap выключен, добавить ←/→ для
горизонтального скролла >80 cols. Общий механизм с tables (Phase 6).
**РЕАЛИЗОВАНО** (light) — `viewport_x` + полный re-render на каждое ←/→.
- **Ускорение hpan через ESTEX WINCOPY/WINREST** (deferred): сейчас pan
делает полный `render_viewport()` = 30 строк × 80 wrchar. Можно
скопировать существующее содержимое viewport'а на N cols влево/вправо
через win-copy, потом рендерить только узкую полосу справа/слева
(HPAN_STEP cols × 30 rows ≈ 240 wrchar вместо 2400). ESTEX SCROLL
горизонталь не поддерживает — нужна именно WINCOPY-операция или
rdchar/wrchar loop. Активировать когда ощутится тормоз; сейчас на
типовом markdown'е не заметно.
**v2 — отдельная фича, мимо wrap:**
- Toggle подсветки целиком (F3?)
- Search по тексту (Ctrl+F / F4)
- Links `[text](url)` → синий подчёркнутый text, url прячется
- Images `![alt](path)` → `[IMG: alt]`
### Phase 7 — Links и поиск (post-v1)
- `[text](url)` → отрисовать только `text` с ярко-синим FG (визуально подчёркнутое)
- `![alt](path)` → `[IMG: alt]` в скобках
- Search по тексту (F3 / Ctrl+F): инкрементальный, подсветка совпадений
### Phase 8 — F8 Raw / Render toggle
Переключатель режима отображения: при включённом Raw показывается исходный
текст файла как есть — все markdown-маркеры (`#`, `**`, `_`, `` ` ``, `|`,
`>`, `-`, etc.) рендерятся литералами с `ATTR_TEXT`, без классификации.
Полезно когда:
- нужно увидеть точную разметку (отлаживание .md, скриншоты, копирование)
- markdown-классификатор ошибся и хочется увидеть оригинал
- хочется быстро сравнить "до/после" рендера
**Поведение:**
- F8 переключает `render_mode` (1=render, 0=raw); меню показывает обратное
действие ("Raw" когда сейчас render, "Render" когда сейчас raw) — той же
логикой что F2/Wrap/Unwrap.
- В Raw режиме: `render_line()` идёт по короткому пути — никакого
`classify_line`, `is_fence_delim`, `is_code_body`, inline-эмфазиса; просто
байтовый дамп FILE_BUF от seg-offset до next-seg/EOL с tab-expansion и
ATTR_TEXT.
- Раздельно от F2: оба режима независимы (можно Raw+Wrap, Raw+Truncate,
Render+Wrap, Render+Truncate). Wrap-логика в `index_lines` работает в
обоих случаях одинаково (опирается на визуальные колонки независимо от
раскраски).
- Статус-бар: добавить индикатор `[R]` / `[V]` (Raw / View) или текстом
`RAW` рядом с именем файла.
**Минимальная реализация:**
- Один новый static `uint8_t render_mode = 1;`
- В `render_line()`: на самом верху `if (!render_mode) { … raw render … return; }`
- В `render_menu()`: добавить ярлык F8 рядом с F2.
- В главном цикле: `case KEY_F8: toggle_render(); break;`
- `toggle_render()` отличается от `toggle_wrap()` тем, что НЕ перестраивает
`line_offset[]` (wrap-сегментация не меняется), только перерендерит экран.
---
## API-новинки в libc (минимальные)
### `getkey()` — extended key reader (в libc)
Текущий `getch()` теряет scan code расширенных клавиш (возвращает только E=ASCII). Добавляем **сразу в `libc/conio/conio.c`** новую функцию рядом с `getch()`:
```c
// Returns scan in high byte, ASCII in low byte.
// Extended keys (arrows, F-keys, PgUp/PgDn, Home/End): ASCII=0, scan code в high byte.
// Plain keys: ASCII в low byte; high byte содержит positional scan (бит 7 = Ctrl/Alt/Shift modifier).
uint16_t getkey(void) __naked {
__asm
ld c, #0x30 ; ESTEX WAITKEY
rst #0x10 ; A=ASCII, D=scan, E=ASCII
ld e, a ; ensure E=ASCII even if E clobbered
ret ; SDCC __sdcccall(1): возврат uint16_t в DE (D=scan, E=ASCII)
__endasm;
}
```
Дополнительно — **в `libc/include/conio.h`** прописать прототип и константы scan-кодов:
```c
uint16_t getkey(void);
/* Scan codes for getkey() high byte when ASCII=0 (extended keys). */
#define KEY_F1 0x0E
#define KEY_F2 0x0F
#define KEY_F3 0x10
#define KEY_F4 0x11
#define KEY_F5 0x12
#define KEY_F6 0x13
#define KEY_F7 0x14
#define KEY_F8 0x15
#define KEY_F9 0x16
#define KEY_F10 0x17
#define KEY_F11 0x18
#define KEY_F12 0x19
#define KEY_END 0x24
#define KEY_DOWN 0x25
#define KEY_PGDN 0x26
#define KEY_LEFT 0x27
#define KEY_RIGHT 0x29
#define KEY_HOME 0x2A
#define KEY_UP 0x2B
#define KEY_PGUP 0x2C
#define KEY_INS 0x23
#define KEY_DEL 0x22
```
**Скан-коды (из docs/converted/ProgrammerManual.txt:2143-2323):**
| Клавиша | scan | Клавиша | scan |
|---|---|---|---|
| F1 | 0x0E | Up | 0x2B |
| F10 | 0x17 | Down | 0x25 |
| F11 | 0x18 | Left | 0x27 |
| F12 | 0x19 | Right | 0x29 |
| | | PgUp | 0x2C |
| | | PgDn | 0x26 |
| | | Home | 0x2A |
| | | End | 0x24 |
### Что **переиспользуем** из существующей libc
- `open/read/lseek/close` — `libc/io/{open,read,lseek}.c` (POSIX wrappers)
- `mem_alloc_pages/mem_free_block/mem_get_page` — `libc/mem/mem_alloc.c`
- `sprinter_page_w3()` — inline `__sfr` write в `libc/include/sprinter.h:113`
- `bank_read_w3/bank_write_w3` — `libc/mem/bank_io_w3.c` (для fallback или v2 multi-page)
- `wrchar(x, y, ch, attr)` — `libc/conio/conio.c:476` (без auto-scroll, идеально для viewport)
- `clrscr_attr(attr)` — `libc/conio/conio.c:395`
- `gotoxy/wherex/wherey` — `libc/conio/conio.c:412-462` (если нужно)
- `kbhit()` — `libc/conio/conio.c:22` (для non-blocking опроса, опционально)
- `dec16/dec8` — `libc/stdio/dec_print.c` (для status bar: текущая строка / total / %)
- `COLOR(fg, bg)` макрос — `libc/include/conio.h:152`
- Цветовые константы `COLOR_*` — `libc/include/conio.h:145`
- `strlen/memcpy/memset` — z80.lib (НЕ переписывать)
---
## Структура исходников
```
examples/mdview/
├── Makefile # стандартный pattern (см. examples/cat/Makefile)
├── mdview.c # Phase 1: всё в одном файле (main, keys, indexing, render)
├── SAMPLE.MD # тестовый markdown
└── README.md # описание и controls
```
После Phase 3 раскидать по модулям (если суммарный размер > ~6KB):
```
mdview.c — main loop, status/menu bars, key dispatch
mdrender.c — line rendering with MD inline parser
mdindex.c — file load + line indexing
```
### Сборка
```makefile
PROJ ?= ../..
SPRINTER_CC := $(PROJ)/bin/sprinter-cc
mdview.exe: mdview.c
$(SPRINTER_CC) --memory small -o $@ mdview.c
```
`--memory small`: код+данные в W2 (DSS даёт нужное число страниц); файл — отдельная EMM-страница в W3.
---
## Структура mdview.c (Phase 1, эскиз)
```c
#include <stdint.h>
#include <stdio.h>
#include <conio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sprinter.h>
#include <sprinter_mem.h>
#define VIEW_TOP 1
#define VIEW_BOT 30 // inclusive
#define VIEW_H 30
#define SCREEN_W 80
#define MAX_LINES 2048
#define FILE_BUF ((char*)0xC000) /* W3 — EMM page mapped here */
#define TAB_STOP 4
#define ATTR_TEXT COLOR(COLOR_WHITE, COLOR_BLACK)
#define ATTR_BAR COLOR(COLOR_WHITE, COLOR_BLUE)
#define ATTR_MENU_K COLOR(COLOR_YELLOW, COLOR_BLUE)
#define ATTR_MENU_T COLOR(COLOR_WHITE, COLOR_BLUE)
static uint16_t line_off[MAX_LINES];
static uint16_t n_lines;
static uint16_t top_line;
static uint16_t file_size;
static uint8_t file_blk;
static char filename[64];
static int load_file(const char *path); // open, alloc EMM page, map W3, read, close
static void index_lines(void); // scan FILE_BUF, fill line_off[]
static void render_status(void); // row 0
static void render_menu(void); // row 31
static void render_line(uint16_t idx, uint8_t row); // one line @ row
static void render_viewport(void); // VIEW_H lines starting from top_line
static void scroll_up(uint16_t n);
static void scroll_down(uint16_t n);
static void help_screen(void); // F1
int main(int argc, char **argv) {
if (argc < 2) { puts("Usage: mdview <file.md>"); return 1; }
if (load_file(argv[1]) < 0) { puts("load error"); return 1; }
index_lines();
clrscr_attr(ATTR_TEXT);
render_menu();
render_status();
render_viewport();
for (;;) {
uint16_t k = getkey();
uint8_t ascii = k & 0xFF;
uint8_t scan = (k >> 8) & 0x7F; // strip mod bit
if (ascii) {
if (ascii == 0x1B) break; // Esc → exit
continue;
}
switch (scan) {
case KEY_F10: goto exit;
case KEY_F1: help_screen(); break;
case KEY_UP: scroll_up(1); break;
case KEY_DOWN: scroll_down(1); break;
case KEY_PGUP: scroll_up(VIEW_H); break;
case KEY_PGDN: scroll_down(VIEW_H); break;
case KEY_HOME: top_line = 0;
render_viewport(); break;
case KEY_END: /* clamp to last viewport */ break;
}
render_status();
}
exit:
mem_free_block(file_blk);
clrscr_attr(ATTR_TEXT);
return 0;
}
```
---
## Verification
### Phase 1
1. Подготовить `SAMPLE.MD` ~5 KB (заголовки, абзацы, списки) — рендериться будет plain.
2. `cd examples/mdview && make`
3. `python make_disk.py mdview.exe SAMPLE.MD → mc.img && ./run_mame.sh`
4. Проверить:
- Status bar показывает `SAMPLE.MD L 1-30 / N X%`
- Menu bar внизу
- ↑/↓: 1 строка
- PgUp/PgDn: 30 строк, корректное clamp на границах
- Home: top_line=0
- End: top_line = total_lines - VIEW_H
- F1: показывает help, любая клавиша возвращает
- F10 / Esc: выход, экран очищен
5. Edge cases: пустой файл, файл из одной строки, файл с очень длинной строкой (>80), CRLF и LF mixed.
### Phase 2-4
Расширять `SAMPLE.MD` с фичами по мере добавления, визуально верифицировать в MAME. Скриншот-сравнение опционально.
### Регрессии
- Никаких изменений в libc на Phase 1 кроме (потенциально) добавления `getkey()` в `libc/conio/conio.c` — если так, прогнать `examples/conio2` и `examples/filetest` чтобы убедиться что ничего не сломалось.
---
## Решения по неоднозначностям
1. **Word-wrap vs truncate**: v1 = truncate (просто). v2 — F2 toggle wrap.
2. **Горизонтальный скроллинг**: v1 — нет; v2 — `←/→` сдвиг viewport по столбцам.
3. **Tab handling**: преобразование при рендере, **tabstop = 4** (стандарт MD). Оригинал в W3 не трогаем.
4. **UTF-8**: рендерим байты как есть. Если файл в CP866 — отрисуется кириллицей через системный фонт. UTF-8 — пока не поддерживаем (визуально будет каша на не-ASCII символах; детект и предупреждение — в v2).
---
### Phase 6 — Полный layout таблиц (deferred)
Сейчас (Phase 4-tables) таблицы рисуются "декоративно" — пайпы и тире
заменяются на box-drawing chars, но ширины колонок берутся как есть из
исходника. Цель Phase 6 — пересчитать таблицу в нормализованный вид:
- **Pre-scan таблицы**: пройти все строки одного table-блока, найти
максимальную ширину каждой колонки (с учётом съеденных inline-маркеров —
визуальный размер, не байтовый).
- **Re-emit в буфере**: при загрузке файла (или при первой встрече таблицы)
переписать строки в FILE_BUF так, чтобы все ячейки одной колонки имели
одинаковую ширину; добавить top/bottom рамки (`┌─┬─┐` / `└─┴─┘`) как
синтетические строки. Это позволит сохранить 1:1 соответствие "логическая
строка → одна viewport row" без специальной логики при рендере.
- **Память**: re-emit может УВЕЛИЧИТЬ файл за счёт padding и доп.рамок.
Если буфер близок к 16KB — отрезать таблицу и пометить её overflow'ом.
- **Горизонтальный скроллинг**: если итоговая ширина таблицы (или любой
строки) > SCREEN_W = 80 — добавить ←/→ для horizontal pan. Это будет
общий механизм для длинных строк (см. также wrap mode), не только таблиц.
- **Выравнивание из separator-row**: `:-` → left, `-:` → right, `:-:` →
center; учитывать при padding'е содержимого ячейки.
- **Шаги реализации**:
1. Walking pass по фенсам/таблицам прямо в `index_lines()` — собрать
extents всех таблиц.
2. Для каждой таблицы — определить ширины колонок.
3. Решение: rewrite-in-buffer (проще для рендера, но мутирует исходник)
vs render-time layout (cleaner, но требует отдельной структуры
описания layout'а на каждую таблицу).
4. Hpan: общий `viewport_x_offset` для всего экрана, или отдельный
"широкий режим" только внутри таблиц.
> Не блокирующая фича. Запускать когда станет понятен типовой источник
> markdown-файлов (узкие читалки → достаточно текущего декоратора;
> широкие README с большими таблицами → нужен полный layout).
---
### Phase ∞ — Кэш рендеренных строк (low priority)
Отложено: текущая скорость более чем достаточна. Активировать если появится
сценарий, где видна задержка PgUp/PgDn (например, при тяжёлом inline-парсере
v2 с UTF-8 / linkifier / таблицами).
**Кэш отформатированных строк** (W3, отдельная EMM-страница):
```
Cache layout (16 KB EMM page, всего 16000 байт используется):
slot 0: 80 chars + 80 attrs = 160 bytes @ offset 0
...
slot 99: 80 chars + 80 attrs = 160 bytes @ offset 15840
Cache tags (W2 static): uint16_t cache_tag[100] = 200 bytes
cache_tag[i] = line_id, или 0xFFFF = invalid
```
Стратегия — **direct map (no LRU)**: `slot = line_id % 100`. Коллизия → вытеснение.
**Batched viewport render**: 2 page-swap'а на ВЕСЬ viewport (cache → file → cache),
не 60 как при наивной реализации.
При сборке: `cache_blk = mem_alloc_pages(1)` после `file_blk`; `mem_free_block` на exit.
---
## Что отложено в v2
- ~~Файлы >16 KB~~ — **сделано в v1.5** (до 128 KB через 18 EMM-страниц + lazy map в W3).
- ~~Word wrap~~ — **сделано** (Phase 5, F2 toggle).
- Search (Find / Find next) — F3 / F4.
- F8 Raw / Render toggle — спецификация в Phase 8.
- Links `[text](url)` + image alt — Phase 7.
- Tables — Phase 6 (полный layout).
- Toggle highlight on/off — частный случай F8 Raw.
-570
View File
@@ -1,570 +0,0 @@
# « ­: ⥪áâ®¢ë© Markdown viewer ¤«ï Sprinter (`examples/mdview`)
## Context
’¥á⮢ ï ªà㯭 ï § ¤ ç  - ¯à®¢¥à¨âì ­ èã libc ­  ­¥âਢ¨ «ì­®¬ interactive-¯à¨«®¦¥­¨¨ (¯®«­®íªà ­­ë© UI, ä ©«®¢ë© I/O, ¯ àá¥à).  à ««¥«ì­® ¤ áâ å®à®è¨© showcase ¯« âä®à¬ë ¨ ¯®¬®¦¥â ¢ëâ é¨âì ­¥¤®¤¥«ª¨ ¢ conio/io. Š®­¥ç­ ï 楫ì: viewer ¤«ï `.md` ä ©«®¢ á ¯®¤á¢¥âª®© ᨭ⠪á¨á , ­ ¢¨£ æ¨¥© ¯® ⥪áâã ¨ ¯®áâà ­¨ç­ë¬ áªà®««¨­£®¬.
**Ž£à ­¨ç¥­¨ï v1 (§ ä¨ªá¨à®¢ ­ë ¯®«ì§®¢ â¥«¥¬):**
- POSIX file API (open/read/lseek/close); FILE pointer / fread / fgets - ­¥ ¨á¯®«ì§®¢ âì
- ®¤á¢¥âª  ç¥à¥§ 梥â: **à §¬¥à § £®«®¢ª ** -> 梥â èà¨äâ ; **bold/italic** -> 梥â ä®­  (¬®­®è¨à¨­­ë© ä®­â ¡¥§ ¦¨à­®£®/ªãàᨢ­®£® ­ ç¥àâ ­¨ï)
** áè¨à¥­¨ï, ॠ«¨§®¢ ­­ë¥ ¯®á«¥ ¯« ­ :**
- ” ©«ë ¤® 128 KB (1-8 EMM-áâà ­¨æ, lazy map ¢ W3 ç¥à¥§ `fb()/map_page()`) - ¨§­ ç «ì­® ¡ë«® ¢ v2
- €­¨¬¨à®¢ ­­ë© spinner ¨ ¯à¥¤¢ à¨â¥«ì­ ï ®âà¨á®¢ª  UI ¯à¨ áâ à⥠- UX
- Inline emphasis: `_`/`*`/`**` ¯®¤ç¨­ïîâáï XOR flanking-¯à ¢¨«ã (whitespace ஢­® á ®¤­®© áâ®à®­ë), `COLOR_YELLOW` ¨ `2 * 3` ®áâ îâáï «¨â¥à « ¬¨
---
## ’¥ªã騩 áâ âãá (2026-06-05)
| Phase | ‘â âãá | Š®¬¬¥­â à¨© |
|---|---|---|
| 1 Plain text + nav | | § £à㧪 , ¨­¤¥ªá æ¨ï, status/menu, ^V/PgUp/PgDn/Home/End/F1/F10/Esc |
| 2 Headers + HR | | H1..H4, `---`/`***`/`___` (á >=3 marker'®¢) |
| 3 Inline emphasis | | `**` / `*` / `_` / `` ` ``; XOR flanking (á¬. ¢ëè¥) |
| 4 Lists / quote / fenced code | | `- / * / +`, `N. / N)`, `> `, ``` ``` ```; light nested lists |
| 4-tables | | â ¡«¨æë ®â«®¦¥­ë ¢¬¥á⥠á Phase 6 |
| 5 Wrap / Unwrap (F2) | | wrap-by-default; soft wrap; F2 ¯¥à¥ª«îç ¥â; hpan <-/-> ¢ truncate-०¨¬¥ |
| 6 ®«­ë© layout â ¡«¨æ | | deferred |
| 7 Links + search | | deferred |
| 8 F8 Raw toggle | | deferred |
| Cache ७¤¥à¥­­ëå áâப | | ­¥ ­ã¦­® ¯® ᪮à®á⨠|
**UX-¯®¯à ¢ª¨ (®â¤¥«ì­® ®â phase-¯« ­ , 2026-06-05):**
- UI (menu + title bar) ®âà¨á®¢ë¢ ¥âáï „Ž `load_file`/`index_lines` - ¯®«ì§®¢ â¥«ì áà §ã ¢¨¤¨â ¨­â¥à䥩á,   ­¥ çñà­ë© íªà ­
- Title bar: `MDVIEW <spinner> <filename>` (3 ¯à®¡¥«  ¬¥¦¤ã MDVIEW ¨ filename, slot ᯨ­­¥à  - col 8)
- Spinner ªàãâ¨âáï ¢® ¢à¥¬ï `load_file` (¯® áâà ­¨æ¥) ¨ `index_lines` (à § ¢ 32 «®£¨ç¥áª¨å áâப¨), ¢ª«îç ï `toggle_wrap`
---
## €àå¨â¥ªâãà 
###  áª« ¤ª  íªà ­  (80x32, ⥪áâ mode 0x03)
```
Row 0: MDVIEW ³ mdview.md ³ L 1-30 / 142 ³ 21% <- status bar (BG=blue, FG=white)
³ ÀÄÄ filename @ col 10
ÀÄÄÄÄÄÄÄÄÄÄ spinner slot @ col 8 (anim. while busy)
Row 1:
... document viewport (30 rows)
Row 30:
Row 31: F1 Help F2 Wrap F10 Exit <- menu bar (BG=blue, FG=cyan)
```
- Viewport = 30 áâப x 80 á⮫¡æ®¢.
- Status / menu ७¤¥àïâáï ç¥à¥§ `wrchar()` (¡¥§  ¢â®-áªà®«« ), viewport - ç¥à¥§ `LOCATE` + ¯®á¨¬¢®«ì­ë© `wrchar()` (⮦¥ ¡¥§  ¢â®-áªà®«« , áâ ¢¨â ¨ char ¨ attr §  ®¤¨­ call).
###  ¬ïâì
Memory mode: **`small`** - DSS ®â¢®¤¨â ¯®¤ ­ è ®¡à § **¤¢  ¡ ­ª  (W1 + W2)** = 32 KB á㬬 à­® (CODE ¢ W1, DATA + STACK + HEAP ¢ W2). ⮣® ¤®«¦­® 墠â¨âì çâ®¡ë … § ¢®¤¨âì `__banked` ä㭪樨. W3 ®áâ ñâáï ¯®«­®áâìî ᢮¡®¤­ë¬ ¤«ï ¬ ¯¯¨­£  ¡®«ìè¨å ¡ãä¥à®¢:
```
W0 (0x0000-0x3FFF): ESTEX (system, untouchable)
W1 (0x4000-0x7FFF): CODE (small mode page 1)
W2 (0x8000-0xBFFF): DATA + STACK + HEAP (small mode page 2)
W3 (0xC000-0xFFFF): paged window - «¥­¨¢® ¯¥à¥ª«îç ¥âáï ¬¥¦¤ã EMM-áâà ­¨æ ¬¨
ä ©«  (¤® 8 áâà ­¨æ = 128 KB). `cur_page` ªíè¨àã¥â
⥪ãéãî mapping, `fb(p)` ¬ ¯¯¨â ­ã¦­ãî áâà ­¨æã ¯à¨
¯¥à¢®¬ ®¡à é¥­¨¨.
```
®ç¥¬ã small + W3:
- 32 KB ­  ª®¤+¤ ­­ë¥ á ¡®«ì訬 § ¯ á®¬ -> ­¥â ¡ ­ª¨­£ 
- W3 - áâ ­¤ àâ­ë© paged window, ¯®¤ ­¥£® ã ­ á 㦥 ¥áâì `bank_io_w3` API
- ” ©«ë ¤® 128 KB ¯®¤¤¥à¦¨¢ îâáï ­ â¨¢­®: `mem_alloc_pages(pages_needed)` ¯®¤ ¢¥áì ä ©«; `map_page()` ç¥à¥§ `sprinter_page_w3()`; `fb(p)` - ¥¤¨­ ï â®çª  ¤®áâ㯠 ¨§ ¨­¤¥ªá â®à  ¨ ७¤¥à .
‘â â¨ª¨ (¢ W2):
- `line_offset[MAX_LINES]` - uint16_t ᬥ饭¨¥ ª ¦¤®© áâப¨ ¢ ä ©«®¢®© áâà ­¨æ¥ (4 KB ­  2048 áâப)
- `cache_tag[CACHE_N]` - uint16_t ⥣ á«®â  (200 ¡ ©â ­  100 ᫮⮢, ¯®ï¢«ï¥âáï ¢ Phase 3)
- `filename[64]`, `top_line`, `total_lines`, `file_size`, `file_blk`, `cache_blk` - ¥¤¨­¨æë ¡ ©â
**FILE_BUF**: `((char*)0xC000)` - 䨪á¨à®¢ ­­ ï  ¤à¥á æ¨ï ¢ W3 ¯®á«¥ ¬ ¯¯¨­£  ­ã¦­®© áâà ­¨æë.
### ®â®ª ¤ ­­ëå
```
main -> open() -> read() chunks 1KB -> write to W1 (mapped EMM page) -> close()
-> index_lines() (®¤­® ᪠­¨à®¢ ­¨¥, § ¯®«­ï¥â line_offset[])
-> render_viewport() + main loop { getkey(); handle(); render_status(); render_viewport() }
```
**’®­ª®áâì á `read()`**: ESTEX READ § ¯¨á뢠¥â ¯® dst-㪠§ â¥«î ¢  ¤à¥á­®¬ ¯à®áâà ­á⢥ ¢ë§ë¢ î饣®. ®áª®«ìªã ¬ë ¬ ¯¯¨¬ EMM-áâà ­¨æã ¢ W3 (0xC000) „Ž ¢ë§®¢  read(), 㪠§ â¥«ì 0xC000+offset ¢ «¨¤¥­. …᫨ ¢ëïá­¨âáï çâ® BIOS âண ¥â W3 ¢® ¢à¥¬ï read (£à ä¨ç¥áª¨© ¢¨¤¥®¡ãä¥à ¯® 㬮«ç ­¨î ¢ W3 ¯à¨ £à ä¨ç¥áª¨å ०¨¬ å, ­® ¢ ⥪á⮢®¬ - ¤®«¦¥­ ¡ëâì ᢮¡®¤¥­) - fallback: ç¨â âì ¢ 1 KB ¡ãä¥à ¢ W2 ¨ ª®¯¨à®¢ âì ¢ W3 ç¥à¥§ `bank_write_w3()`.
---
## ¥ «¨§ æ¨ï - ¯®íâ ¯­ ï
### Phase 1 - Plain text viewer (MVP)
**—â® à ¡®â ¥â:**
- ‡ £à㧪  ä ©«  (`open/read/close`) ¢ W1-áâà ­¨æã
- ˆ­¤¥ªá æ¨ï áâப (LF / CRLF à §¤¥«¨â¥«¨)
- Status bar: ¨¬ï ä ©« , L N-M / Total, ¯à®æ¥­â áªà®««¨­£ 
- Menu bar: `F1 Help F10 Exit`
-  ¢¨£ æ¨ï: ^/V (1 áâப ), PgUp/PgDn (30 áâப), Home/End (­ ç «®/ª®­¥æ), Esc/F10 (¢ë室), F1 (help screen)
- Ž¡à¥§ª  áâப ¤«¨­­¥¥ 80 ᨬ¢®«®¢ (¡¥§ word-wrap)
- –¢¥â : ⥪áâ ¡¥«ë© ­  çñà­®¬; status/menu - ¡¥«ë© ­  ᨭ¥¬
**Šà¨â¨ç­ë¥ ä ©«ë:**
- `examples/mdview/mdview.c` - main, key loop, rendering, indexing (one-file MVP)
- `examples/mdview/Makefile`
- `examples/mdview/SAMPLE.MD` - â¥áâ®¢ë© markdown ä ©«
### Phase 2 - Headers ¨ £®à¨§®­â «ì­ ï «¨­¨ï
**MD ä¨ç¨:**
- `# H1` -> ïમ-¦ñ«âë© (COLOR_YELLOW = 14) ­  çñà­®¬
- `## H2` -> ïમ-£®«ã¡®© (COLOR_LBLUE = 11)
- `### H3` -> ïમ-§¥«ñ­ë© (COLOR_LGREEN = 10)
- `#### H4+` -> á¥àë© (COLOR_GREY = 8)
- `---` / `***` ­  ®â¤¥«ì­®© áâப¥ -> «¨­¨ï 0xC4 (£®à¨§®­â «ì­ ï à ¬ª  ASCII) ¢® ¢áî è¨à¨­ã
### Phase 3 - Inline emphasis
** àá¥à inline (per-line, runs ¢ ®¤­ã áâபã):**
- `**bold**` -> ATTR_TEXT_BOLD
- `*italic*` -> ATTR_TEXT_ITALIC
- `_underscore_` -> ATTR_TEXT_UNDERSORE
- `` `code` `` -> ATTR_TEXT_CODE
- Œ àª¥àë `**`/`*`/`_`/`` ` `` … ७¤¥àïâáï (áꥤ îâáï)
State machine: ®¤¨­  ªâ¨¢­ë© áâ¨«ì ®¤­®¢à¥¬¥­­® (¡¥§ ¢«®¦¥­­®áâ¨); ª®­ä«¨ªâãî騩 ¬ àª¥à ¯à¨ ç㦮¬  ªâ¨¢­®¬ á⨫¥ ¢áñ à ¢­® ª®­áìâáï (zero-width) ¤«ï ᨭåà®­¨§ æ¨¨ è¨à¨­ë á index_lines. ‘®áâ®ï­¨¥ á¡à á뢠¥âáï ­  ª ¦¤®© áâப¥.
**Flanking-¯à ¢¨«® (CommonMark intraword, ॠ«¨§®¢ ­® ¯®á«¥ ¨§­ ç «ì­®£® ¯« ­ ):**
- `*` / `**` / `_` áç¨â îâáï markdown-¬ àª¥à®¬ ⮫쪮 ¥á«¨ whitespace/EOL
஢­® á Ž„މ áâ®à®­ë (XOR).
- ‘«ãç ¨ "®¡  whitespace" (`2 * 3`, `2 ** 3`) -> «¨â¥à «ë ( à¨ä¬¥â¨ª ).
- ‘«ãç ¨ "­¨ ®¤­®£® whitespace" (`COLOR_YELLOW`, `FILE*/fread`, `foo*bar*baz`)
-> «¨â¥à «ë (intraword).
- Backtick (`` ` ``) flanking … âॡã¥â - `` `code` `` à ¡®â ¥â ¡¥§ ¯à®¡¥«®¢.
- à ¢¨«® ¯à¨¬¥­¥­® ᨬ¬¥âà¨ç­® ¢ 4 ¬¥áâ å (`index_lines`, cont-render,
®á­®¢­®© inline-¯ àá¥à, truncation peek), ¨­ ç¥ wrap-¨­¤¥ªá â®à ¨
७¤¥à ࠧꥤãâáï ¯® è¨à¨­¥.
> **Šíè ®âä®à¬ â¨à®¢ ­­ëå áâப** - ®â«®¦¥­ ¢ á ¬ë© ª®­¥æ, á¬. "Phase : ®¯â¨¬¨§ æ¨¨".
> ‘ª®à®á⨠⥪ã饣® ­ ¨¢­®£® ७¤¥à  墠⠥⠭  80x30 = 2400 wrchar / ª ¤à; PgUp/PgDn ¢¨§ã «ì­® ¬£­®¢¥­¥­.
### Phase 4 - Block elements (¡¥§ â ¡«¨æ)
- Œ àª¨à®¢ ­­ë¥ ᯨ᪨: `- foo`, `* foo`, `+ foo` -> ¯à¥ä¨ªá `o` (0x07) + ¯à®¡¥«; 梥⠬ àª¥à  ïàç¥ ®á­®¢­®£®
- 㬥஢ ­­ë¥ ᯨ᪨: `1. foo`, `2. foo` -> ª ª ¥áâì (ç¨á«® ®áâ ¢«ï¥¬)
- Blockquote: áâப¨ á `> ` -> ¯à¥ä¨ªá `³` (0xB3) á¥à®£® 梥â , ®áâ «ì­®© ⥪áâ á«¥£ª  ¯à¨£«ãèñ­­ë©
- Fenced code blocks: `` ``` `` ®âªà뢠¥â/§ ªà뢠¥â ¡«®ª; ¢á¥ áâப¨ ¬¥¦¤ã - bg=á¥àë©, ¬®­®è¨à¨­­® (¡¥§ inline-¯ àᨭ£ )
- Indented code blocks (4+ ¯à®¡¥«®¢):  ­ «®£¨ç­® fenced, ­® ¡¥§ ®£® ¬ àª¥à 
**Light nested lists (v1 - ॠ«¨§®¢ ­®):**
- `classify_line()` ¯à®¯ã᪠¥â leading spaces ¯¥à¥¤ ulist/olist/quote ¬ àª¥à®¬,
¢®§¢à é ¥â `content_off` ¯®á«¥ ¬ àª¥à  -> `content_off - p_start` = indent + marker
¡¨â ¢ visible col.
- `render_line()` à¨áã¥â leading-spaces ¢ `ATTR_TEXT`, ¯®â®¬ ¬ àª¥à ­  ᤢ¨­ã⮩
¯®§¨æ¨¨ (col = indent). Marker ¢áñ ¥éñ 䨪á¨à®¢ ­ ¯à¨ £®à¨§®­â «ì­®¬ pan'¥.
- HR / header / fence delim ®áâ îâáï áâண® col-0 (CommonMark à §à¥è ¥â ¤® 3
¢¥¤ãé¨å ¯à®¡¥«®¢ ¤«ï ­¨å - ã¯à®á⨫¨).
- Tab-indent -> ­¥ à á¯®§­ ñâáï ª ª nesting (⮫쪮 spaces).
**Phase 4-full - ¯®«­ ï ¯®¤¤¥à¦ª  ¢«®¦¥­­®á⨠(deferred):**
- **Tab-indent**: áç¨â âì tab = 4 ¯à®¡¥«  ¤«ï ®¯à¥¤¥«¥­¨ï ã஢­ï.
- **Quote nesting** (`> > foo`): ª ¦¤ë© `>` ¯®¤àï¤ = +1 ã஢¥­ì, ª ¦¤ë© à¨áã¥âáï
®â¤¥«ì­ë¬ `³` ¢ `ATTR_QUOTE_MARKER` (¢¨§ã «ì­ ï "«¥áâ­¨æ " á«¥¢ ).
- **Hanging indent ¢ wrap-continuation**: ª®£¤  `- some very long bullet text
that wraps...` - continuation seg ¤®«¦¥­ ­ ç¨­ âìáï ®â content-col (¯®á«¥
¬ àª¥à ),   ­¥ ®â col 0. ‘¥©ç á continuation ¨¤ñâ ®â col 0 (v1 simplification).
’ॡã¥â åà ­¨âì `marker_width` per «®£¨ç¥áª ï áâப  (8 ¡¨â) ¨«¨ re-classify
first seg ¯à¨ ७¤¥à¥ continuation.
- **Lazy continuation**: áâப¨ ¡¥§ ¬ àª¥à , ­® á ¯à ¢¨«ì­ë¬ indent ¯®¤
¯à¥¤ë¤ã騬 bullet'®¬, ¤®«¦­ë áç¨â âìáï ¯à®¤®«¦¥­¨¥¬ ⮣® bullet' 
(¢¨§ã «ì­® - ®¡é¨© attr).
- **Strict CommonMark indent rules**: ¢«®¦¥­­ë© ¯ã­ªâ ¤®«¦¥­ ¡ëâì ­ 
indent >= content_col த¨â¥«ï, ¨­ ç¥ áç¨â ¥âáï breakout. 㦥­ ¬¨­¨-stack
 ªâ¨¢­ëå ᯨ᪮¢ ¯à¨ ¨­¤¥ªá æ¨¨.
### Phase 5 - Wrap / Unwrap ¤«¨­­ëå áâப
„¥ä®«â: **wrap on**. F2 ¯¥à¥ª«îç ¥â; ¢ ¬¥­î-¡ à¥ ¯®¤¯¨áì ®âà ¦ ¥â ¤¥©á⢨¥
("Unwrap" ª®£¤  wrap ¢ª«îçñ­, "Wrap" ª®£¤  ¢ëª«î祭). ‚® ¢à¥¬ï २­¤¥ªá æ¨¨
ªàãâ¨âáï ᯨ­­¥à ­  title bar.
**v1 - ॠ«¨§®¢ ­®:**
- ޤ¨­ ¬ áᨢ `line_offset[2048]` åà ­¨â ‚ˆ„ˆŒ›… ᥣ¬¥­âë (  ­¥ «®£¨ç¥áª¨¥
áâப¨); ¡¨âë 0..13 - ¡ ©â®¢®¥ ᬥ饭¨¥, ¡¨â 15 - CONT-ä« £ continuation.
- Wrap-०¨¬: soft wrap ­  ¯®á«¥¤­¥¬ ¯à®¡¥«¥ <= 80; hard fallback ¥á«¨
¯à®¡¥«  ­¥â.
- Œ àª¥àë í¬ä §¨á  (`**`/`*`/`_`/`` ` ``) ¨ header-¯à¥ä¨ªáë (`#`/`##`/...) ­¥
ãç¨â뢠îâáï ¢ visible-col ¯à¨ ¯®¨áª¥ â®çª¨ ¯¥à¥­®á .
- "‘¯¥æ¨ «ì­ë¥" «®£¨ç¥áª¨¥ áâப¨ ­¥ wrap' îâáï ¢®®¡é¥ (®¤­  seg-§ ¯¨áì ­ 
«®£¨ç¥áªãî áâபã): fence delim, table row (header/separator/body), HR.
- F2 toggle á®åà ­ï¥â ¢¨§ã «ì­ãî ¯®§¨æ¨î ç¥à¥§ `top_offset` ¢ FILE_BUF.
- Bitmaps (`in_code`/`in_table`/`is_tab_hdr`) ¯¥à¥áâà ¨¢ îâáï ¢¬¥á⥠á
ᥣ¬¥­â ¬¨, ¨­¤¥ªá¨àãîâáï seg-¨­¤¥ªá®¬, ¡¨âë áâ ¢ïâáï ⮫쪮 ­  ¯¥à¢®¬
seg'¥ «®£¨ç¥áª®© áâப¨.
- Continuation-ᥣ¬¥­âë ७¤¥àïâáï ¢ á⨫¥ "v1: ¯«®áª®" - plain text,
­¨ª ª¨å markdown-ª« áá¨ä¨ª æ¨©; padding ¤® ª®­æ  áâப¨ `ATTR_TEXT`.
**v1.5 - ®â«®¦¥­® ¤«ï ¯®«­®© ª à⨭ë wrap:**
- **Hanging indent**: continuation ®â ulist/olist/quote ¤®«¦¥­ ¢ëà ¢­¨¢ âìáï
¯®¤ content,   ­¥ ®â col 0. ’ॡã¥â åà ­¨âì marker_width per «®£¨ç¥áª ï
áâப  ˆ‹ˆ re-classify ¯¥à¢®£® seg'  ¯à¨ ७¤¥à¥ continuation.
- ** á«¥¤®¢ ­¨¥ base_attr**: continuation ®â header'  ¤®«¦¥­ á®åà ­ïâì
梥â; continuation ®â code body - ä®­ ATTR_TEXT_CODE. ’ॡã¥â åà ­¨âì
1 ¡ ©â `base_attr` per seg ˆ‹ˆ lookup ¯¥à¢®£® seg' .
- **Inline emphasis ç¥à¥§ £à ­¨æã**: í¬ä §¨á, ®âªàëâë© ¢ ¯¥à¢®¬ seg'¥ ¨ ­¥
§ ªàëâë©, ¤®«¦¥­ ¯à®¤®«¦ âìáï ¢® ¢â®à®¬. ’ॡã¥â åà ­¨âì emph state per
seg (3 ¡¨â ).
- Compact way: ¤®¡ ¢¨âì ¯ à ««¥«ì­ë© ¬ áᨢ `seg_meta[MAX_SEGS]` ¯® 1 ¡ ©âã -
¯ ªã¥â marker_width (4 ¡¨â ) + emph_state (3 ¡¨â ) + base_attr_idx
(4 ¡¨â  ¨§ â ¡«¨æë -> ­ã¦¥­ 2-¡ ©â®¢ë© seg_meta).
- **Hpan ¤«ï ¤«¨­­ëå áâப**: ¥á«¨ wrap ¢ëª«î祭, ¤®¡ ¢¨âì <-/-> ¤«ï
£®à¨§®­â «ì­®£® áªà®««  >80 cols. ޡ鍩 ¬¥å ­¨§¬ á tables (Phase 6).
**…€‹ˆ‡Ž‚€Ž** (light) - `viewport_x` + ¯®«­ë© re-render ­  ª ¦¤®¥ <-/->.
- **“᪮७¨¥ hpan ç¥à¥§ ESTEX WINCOPY/WINREST** (deferred): ᥩç á pan
¤¥« ¥â ¯®«­ë© `render_viewport()` = 30 áâப x 80 wrchar. Œ®¦­®
᪮¯¨à®¢ âì áãé¥áâ¢ãî饥 ᮤ¥à¦¨¬®¥ viewport'  ­  N cols ¢«¥¢®/¢¯à ¢®
ç¥à¥§ win-copy, ¯®â®¬ ७¤¥à¨âì ⮫쪮 㧪ãî ¯®«®áã á¯à ¢ /á«¥¢ 
(HPAN_STEP cols x 30 rows 240 wrchar ¢¬¥áâ® 2400). ESTEX SCROLL
£®à¨§®­â «ì ­¥ ¯®¤¤¥à¦¨¢ ¥â - ­ã¦­  ¨¬¥­­® WINCOPY-®¯¥à æ¨ï ¨«¨
rdchar/wrchar loop. €ªâ¨¢¨à®¢ âì ª®£¤  ®éãâ¨âáï â®à¬®§; ᥩç á ­ 
⨯®¢®¬ markdown'¥ ­¥ § ¬¥â­®.
**v2 - ®â¤¥«ì­ ï ä¨ç , ¬¨¬® wrap:**
- Toggle ¯®¤á¢¥âª¨ 楫¨ª®¬ (F3?)
- Search ¯® ⥪áâã (Ctrl+F / F4)
- Links `[text](url)` -> ᨭ¨© ¯®¤çñભãâë© text, url ¯àïç¥âáï
- Images `![alt](path)` -> `[IMG: alt]`
### Phase 7 - Links ¨ ¯®¨áª (post-v1)
- `[text](url)` -> ®âà¨á®¢ âì ⮫쪮 `text` á ïમ-ᨭ¨¬ FG (¢¨§ã «ì­® ¯®¤çñભã⮥)
- `![alt](path)` -> `[IMG: alt]` ¢ ᪮¡ª å
- Search ¯® ⥪áâã (F3 / Ctrl+F): ¨­ªà¥¬¥­â «ì­ë©, ¯®¤á¢¥âª  ᮢ¯ ¤¥­¨©
### Phase 8 - F8 Raw / Render toggle
¥à¥ª«îç â¥«ì ०¨¬  ®â®¡à ¦¥­¨ï: ¯à¨ ¢ª«îçñ­­®¬ Raw ¯®ª §ë¢ ¥âáï ¨á室­ë©
⥪áâ ä ©«  ª ª ¥áâì - ¢á¥ markdown-¬ àª¥àë (`#`, `**`, `_`, `` ` ``, `|`,
`>`, `-`, etc.) ७¤¥àïâáï «¨â¥à « ¬¨ á `ATTR_TEXT`, ¡¥§ ª« áá¨ä¨ª æ¨¨.
®«¥§­® ª®£¤ :
- ­ã¦­® 㢨¤¥âì â®ç­ãî à §¬¥âªã (®â« ¦¨¢ ­¨¥ .md, áªà¨­è®âë, ª®¯¨à®¢ ­¨¥)
- markdown-ª« áá¨ä¨ª â®à ®è¨¡áï ¨ å®ç¥âáï 㢨¤¥âì ®à¨£¨­ «
- å®ç¥âáï ¡ëáâà® áà ¢­¨âì "¤®/¯®á«¥" ७¤¥à 
**®¢¥¤¥­¨¥:**
- F8 ¯¥à¥ª«îç ¥â `render_mode` (1=render, 0=raw); ¬¥­î ¯®ª §ë¢ ¥â ®¡à â­®¥
¤¥©á⢨¥ ("Raw" ª®£¤  ᥩç á render, "Render" ª®£¤  ᥩç á raw) - ⮩ ¦¥
«®£¨ª®© çâ® F2/Wrap/Unwrap.
- Raw ०¨¬¥: `render_line()` ¨¤ñâ ¯® ª®à®âª®¬ã ¯ã⨠- ­¨ª ª®£®
`classify_line`, `is_fence_delim`, `is_code_body`, inline-í¬ä §¨á ; ¯à®áâ®
¡ ©â®¢ë© ¤ ¬¯ FILE_BUF ®â seg-offset ¤® next-seg/EOL á tab-expansion ¨
ATTR_TEXT.
-  §¤¥«ì­® ®â F2: ®¡  ०¨¬  ­¥§ ¢¨á¨¬ë (¬®¦­® Raw+Wrap, Raw+Truncate,
Render+Wrap, Render+Truncate). Wrap-«®£¨ª  ¢ `index_lines` à ¡®â ¥â ¢
®¡®¨å á«ãç ïå ®¤¨­ ª®¢® (®¯¨à ¥âáï ­  ¢¨§ã «ì­ë¥ ª®«®­ª¨ ­¥§ ¢¨á¨¬® ®â
à áªà áª¨).
- ‘â âãá-¡ à: ¤®¡ ¢¨âì ¨­¤¨ª â®à `[R]` / `[V]` (Raw / View) ¨«¨ ⥪á⮬
`RAW` à冷¬ á ¨¬¥­¥¬ ä ©« .
**Œ¨­¨¬ «ì­ ï ॠ«¨§ æ¨ï:**
- ޤ¨­ ­®¢ë© static `uint8_t render_mode = 1;`
- `render_line()`: ­  á ¬®¬ ¢¥àåã `if (!render_mode) { ... raw render ... return; }`
- `render_menu()`: ¤®¡ ¢¨âì ïà«ëª F8 à冷¬ á F2.
- ‚ £« ¢­®¬ 横«¥: `case KEY_F8: toggle_render(); break;`
- `toggle_render()` ®â«¨ç ¥âáï ®â `toggle_wrap()` ⥬, çâ® … ¯¥à¥áâà ¨¢ ¥â
`line_offset[]` (wrap-ᥣ¬¥­â æ¨ï ­¥ ¬¥­ï¥âáï), ⮫쪮 ¯¥à¥à¥­¤¥à¨â íªà ­.
---
## API-­®¢¨­ª¨ ¢ libc (¬¨­¨¬ «ì­ë¥)
### `getkey()` - extended key reader (¢ libc)
’¥ªã騩 `getch()` â¥àï¥â scan code à áè¨à¥­­ëå ª« ¢¨è (¢®§¢à é ¥â ⮫쪮 E=ASCII). „®¡ ¢«ï¥¬ **áà §ã ¢ `libc/conio/conio.c`** ­®¢ãî äã­ªæ¨î à冷¬ á `getch()`:
```c
// Returns scan in high byte, ASCII in low byte.
// Extended keys (arrows, F-keys, PgUp/PgDn, Home/End): ASCII=0, scan code ¢ high byte.
// Plain keys: ASCII ¢ low byte; high byte ᮤ¥à¦¨â positional scan (¡¨â 7 = Ctrl/Alt/Shift modifier).
uint16_t getkey(void) __naked {
__asm
ld c, #0x30 ; ESTEX WAITKEY
rst #0x10 ; A=ASCII, D=scan, E=ASCII
ld e, a ; ensure E=ASCII even if E clobbered
ret ; SDCC __sdcccall(1): ¢®§¢à â uint16_t ¢ DE (D=scan, E=ASCII)
__endasm;
}
```
„®¯®«­¨â¥«ì­® - **¢ `libc/include/conio.h`** ¯à®¯¨á âì ¯à®â®â¨¯ ¨ ª®­áâ ­âë scan-ª®¤®¢:
```c
uint16_t getkey(void);
/* Scan codes for getkey() high byte when ASCII=0 (extended keys). */
#define KEY_F1 0x0E
#define KEY_F2 0x0F
#define KEY_F3 0x10
#define KEY_F4 0x11
#define KEY_F5 0x12
#define KEY_F6 0x13
#define KEY_F7 0x14
#define KEY_F8 0x15
#define KEY_F9 0x16
#define KEY_F10 0x17
#define KEY_F11 0x18
#define KEY_F12 0x19
#define KEY_END 0x24
#define KEY_DOWN 0x25
#define KEY_PGDN 0x26
#define KEY_LEFT 0x27
#define KEY_RIGHT 0x29
#define KEY_HOME 0x2A
#define KEY_UP 0x2B
#define KEY_PGUP 0x2C
#define KEY_INS 0x23
#define KEY_DEL 0x22
```
**‘ª ­-ª®¤ë (¨§ docs/converted/ProgrammerManual.txt:2143-2323):**
| Š« ¢¨è  | scan | Š« ¢¨è  | scan |
|---|---|---|---|
| F1 | 0x0E | Up | 0x2B |
| F10 | 0x17 | Down | 0x25 |
| F11 | 0x18 | Left | 0x27 |
| F12 | 0x19 | Right | 0x29 |
| | | PgUp | 0x2C |
| | | PgDn | 0x26 |
| | | Home | 0x2A |
| | | End | 0x24 |
### —â® **¯¥à¥¨á¯®«ì§ã¥¬** ¨§ áãé¥áâ¢ãî饩 libc
- `open/read/lseek/close` - `libc/io/{open,read,lseek}.c` (POSIX wrappers)
- `mem_alloc_pages/mem_free_block/mem_get_page` - `libc/mem/mem_alloc.c`
- `sprinter_page_w3()` - inline `__sfr` write ¢ `libc/include/sprinter.h:113`
- `bank_read_w3/bank_write_w3` - `libc/mem/bank_io_w3.c` (¤«ï fallback ¨«¨ v2 multi-page)
- `wrchar(x, y, ch, attr)` - `libc/conio/conio.c:476` (¡¥§ auto-scroll, ¨¤¥ «ì­® ¤«ï viewport)
- `clrscr_attr(attr)` - `libc/conio/conio.c:395`
- `gotoxy/wherex/wherey` - `libc/conio/conio.c:412-462` (¥á«¨ ­ã¦­®)
- `kbhit()` - `libc/conio/conio.c:22` (¤«ï non-blocking ®¯à®á , ®¯æ¨®­ «ì­®)
- `dec16/dec8` - `libc/stdio/dec_print.c` (¤«ï status bar: ⥪ãé ï áâப  / total / %)
- `COLOR(fg, bg)` ¬ ªà®á - `libc/include/conio.h:152`
- –¢¥â®¢ë¥ ª®­áâ ­âë `COLOR_*` - `libc/include/conio.h:145`
- `strlen/memcpy/memset` - z80.lib (… ¯¥à¥¯¨á뢠âì)
---
## ‘âàãªâãà  ¨á室­¨ª®¢
```
examples/mdview/
ÃÄÄ Makefile # áâ ­¤ àâ­ë© pattern (á¬. examples/cat/Makefile)
ÃÄÄ mdview.c # Phase 1: ¢áñ ¢ ®¤­®¬ ä ©«¥ (main, keys, indexing, render)
ÃÄÄ SAMPLE.MD # â¥áâ®¢ë© markdown
ÀÄÄ README.md # ®¯¨á ­¨¥ ¨ controls
```
®á«¥ Phase 3 à áª¨¤ âì ¯® ¬®¤ã«ï¬ (¥á«¨ á㬬 à­ë© à §¬¥à > ~6KB):
```
mdview.c - main loop, status/menu bars, key dispatch
mdrender.c - line rendering with MD inline parser
mdindex.c - file load + line indexing
```
### ‘¡®àª 
```makefile
PROJ ?= ../..
SPRINTER_CC := $(PROJ)/bin/sprinter-cc
mdview.exe: mdview.c
$(SPRINTER_CC) --memory small -o $@ mdview.c
```
`--memory small`: ª®¤+¤ ­­ë¥ ¢ W2 (DSS ¤ ñ⠭㦭®¥ ç¨á«® áâà ­¨æ); ä ©« - ®â¤¥«ì­ ï EMM-áâà ­¨æ  ¢ W3.
---
## ‘âàãªâãà  mdview.c (Phase 1, í᪨§)
```c
#include <stdint.h>
#include <stdio.h>
#include <conio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sprinter.h>
#include <sprinter_mem.h>
#define VIEW_TOP 1
#define VIEW_BOT 30 // inclusive
#define VIEW_H 30
#define SCREEN_W 80
#define MAX_LINES 2048
#define FILE_BUF ((char*)0xC000) /* W3 - EMM page mapped here */
#define TAB_STOP 4
#define ATTR_TEXT COLOR(COLOR_WHITE, COLOR_BLACK)
#define ATTR_BAR COLOR(COLOR_WHITE, COLOR_BLUE)
#define ATTR_MENU_K COLOR(COLOR_YELLOW, COLOR_BLUE)
#define ATTR_MENU_T COLOR(COLOR_WHITE, COLOR_BLUE)
static uint16_t line_off[MAX_LINES];
static uint16_t n_lines;
static uint16_t top_line;
static uint16_t file_size;
static uint8_t file_blk;
static char filename[64];
static int load_file(const char *path); // open, alloc EMM page, map W3, read, close
static void index_lines(void); // scan FILE_BUF, fill line_off[]
static void render_status(void); // row 0
static void render_menu(void); // row 31
static void render_line(uint16_t idx, uint8_t row); // one line @ row
static void render_viewport(void); // VIEW_H lines starting from top_line
static void scroll_up(uint16_t n);
static void scroll_down(uint16_t n);
static void help_screen(void); // F1
int main(int argc, char **argv) {
if (argc < 2) { puts("Usage: mdview <file.md>"); return 1; }
if (load_file(argv[1]) < 0) { puts("load error"); return 1; }
index_lines();
clrscr_attr(ATTR_TEXT);
render_menu();
render_status();
render_viewport();
for (;;) {
uint16_t k = getkey();
uint8_t ascii = k & 0xFF;
uint8_t scan = (k >> 8) & 0x7F; // strip mod bit
if (ascii) {
if (ascii == 0x1B) break; // Esc -> exit
continue;
}
switch (scan) {
case KEY_F10: goto exit;
case KEY_F1: help_screen(); break;
case KEY_UP: scroll_up(1); break;
case KEY_DOWN: scroll_down(1); break;
case KEY_PGUP: scroll_up(VIEW_H); break;
case KEY_PGDN: scroll_down(VIEW_H); break;
case KEY_HOME: top_line = 0;
render_viewport(); break;
case KEY_END: /* clamp to last viewport */ break;
}
render_status();
}
exit:
mem_free_block(file_blk);
clrscr_attr(ATTR_TEXT);
return 0;
}
```
---
## Verification
### Phase 1
1. ®¤£®â®¢¨âì `SAMPLE.MD` ~5 KB (§ £®«®¢ª¨,  ¡§ æë, ᯨ᪨) - ७¤¥à¨âìáï ¡ã¤¥â plain.
2. `cd examples/mdview && make`
3. `python make_disk.py mdview.exe SAMPLE.MD -> mc.img && ./run_mame.sh`
4. ஢¥à¨âì:
- Status bar ¯®ª §ë¢ ¥â `SAMPLE.MD L 1-30 / N X%`
- Menu bar ¢­¨§ã
- ^/V: 1 áâப 
- PgUp/PgDn: 30 áâப, ª®à४⭮¥ clamp ­  £à ­¨æ å
- Home: top_line=0
- End: top_line = total_lines - VIEW_H
- F1: ¯®ª §ë¢ ¥â help, «î¡ ï ª« ¢¨è  ¢®§¢à é ¥â
- F10 / Esc: ¢ë室, íªà ­ ®ç¨é¥­
5. Edge cases: ¯ãá⮩ ä ©«, ä ©« ¨§ ®¤­®© áâப¨, ä ©« á ®ç¥­ì ¤«¨­­®© áâப®© (>80), CRLF ¨ LF mixed.
### Phase 2-4
 áè¨àïâì `SAMPLE.MD` á ä¨ç ¬¨ ¯® ¬¥à¥ ¤®¡ ¢«¥­¨ï, ¢¨§ã «ì­® ¢¥à¨ä¨æ¨à®¢ âì ¢ MAME. ‘ªà¨­è®â-áà ¢­¥­¨¥ ®¯æ¨®­ «ì­®.
### ¥£à¥áᨨ
- ¨ª ª¨å ¨§¬¥­¥­¨© ¢ libc ­  Phase 1 ªà®¬¥ (¯®â¥­æ¨ «ì­®) ¤®¡ ¢«¥­¨ï `getkey()` ¢ `libc/conio/conio.c` - ¥á«¨ â ª, ¯à®£­ âì `examples/conio2` ¨ `examples/filetest` ç⮡ë ã¡¥¤¨âìáï çâ® ­¨ç¥£® ­¥ á«®¬ «®áì.
---
## ¥è¥­¨ï ¯® ­¥®¤­®§­ ç­®áâï¬
1. **Word-wrap vs truncate**: v1 = truncate (¯à®áâ®). v2 - F2 toggle wrap.
2. **ƒ®à¨§®­â «ì­ë© áªà®««¨­£**: v1 - ­¥â; v2 - `<-/->` ᤢ¨£ viewport ¯® á⮫¡æ ¬.
3. **Tab handling**: ¯à¥®¡à §®¢ ­¨¥ ¯à¨ ७¤¥à¥, **tabstop = 4** (áâ ­¤ àâ MD). Žà¨£¨­ « ¢ W3 ­¥ âண ¥¬.
4. **UTF-8**: ७¤¥à¨¬ ¡ ©âë ª ª ¥áâì. …᫨ ä ©« ¢ CP866 - ®âà¨áã¥âáï ª¨à¨««¨æ¥© ç¥à¥§ á¨á⥬­ë© ä®­â. UTF-8 - ¯®ª  ­¥ ¯®¤¤¥à¦¨¢ ¥¬ (¢¨§ã «ì­® ¡ã¤¥â ª è  ­  ­¥-ASCII ᨬ¢®« å; ¤¥â¥ªâ ¨ ¯à¥¤ã¯à¥¦¤¥­¨¥ - ¢ v2).
---
### Phase 6 - ®«­ë© layout â ¡«¨æ (deferred)
‘¥©ç á (Phase 4-tables) â ¡«¨æë à¨áãîâáï "¤¥ª®à â¨¢­®" - ¯ ©¯ë ¨ â¨à¥
§ ¬¥­ïîâáï ­  box-drawing chars, ­® è¨à¨­ë ª®«®­®ª ¡¥àãâáï ª ª ¥áâì ¨§
¨á室­¨ª . –¥«ì Phase 6 - ¯¥à¥áç¨â âì â ¡«¨æã ¢ ­®à¬ «¨§®¢ ­­ë© ¢¨¤:
- **Pre-scan â ¡«¨æë**: ¯à®©â¨ ¢á¥ áâப¨ ®¤­®£® table-¡«®ª , ­ ©â¨
¬ ªá¨¬ «ì­ãî è¨à¨­ã ª ¦¤®© ª®«®­ª¨ (á ãçñ⮬ áꥤ¥­­ëå inline-¬ àª¥à®¢ -
¢¨§ã «ì­ë© à §¬¥à, ­¥ ¡ ©â®¢ë©).
- **Re-emit ¢ ¡ãä¥à¥**: ¯à¨ § £à㧪¥ ä ©«  (¨«¨ ¯à¨ ¯¥à¢®© ¢áâà¥ç¥ â ¡«¨æë)
¯¥à¥¯¨á âì áâப¨ ¢ FILE_BUF â ª, çâ®¡ë ¢á¥ ï祩ª¨ ®¤­®© ª®«®­ª¨ ¨¬¥«¨
®¤¨­ ª®¢ãî è¨à¨­ã; ¤®¡ ¢¨âì top/bottom à ¬ª¨ (`ÚÄÂÄ¿` / `ÀÄÁÄÙ`) ª ª
ᨭâ¥â¨ç¥áª¨¥ áâப¨. â® ¯®§¢®«¨â á®åà ­¨âì 1:1 ᮮ⢥âá⢨¥ "«®£¨ç¥áª ï
áâப  -> ®¤­  viewport row" ¡¥§ ᯥ樠«ì­®© «®£¨ª¨ ¯à¨ ७¤¥à¥.
- ** ¬ïâì**: re-emit ¬®¦¥â “‚…‹ˆ—ˆ’œ ä ©« §  áçñâ padding ¨ ¤®¯.à ¬®ª.
…᫨ ¡ãä¥à ¡«¨§®ª ª 16KB - ®â१ âì â ¡«¨æã ¨ ¯®¬¥â¨âì ¥ñ overflow'®¬.
- **ƒ®à¨§®­â «ì­ë© áªà®««¨­£**: ¥á«¨ ¨â®£®¢ ï è¨à¨­  â ¡«¨æë (¨«¨ «î¡®©
áâப¨) > SCREEN_W = 80 - ¤®¡ ¢¨âì <-/-> ¤«ï horizontal pan. â® ¡ã¤¥â
®¡é¨© ¬¥å ­¨§¬ ¤«ï ¤«¨­­ëå áâப (á¬. â ª¦¥ wrap mode), ­¥ ⮫쪮 â ¡«¨æ.
- **‚ëà ¢­¨¢ ­¨¥ ¨§ separator-row**: `:-` -> left, `-:` -> right, `:-:` ->
center; ãç¨â뢠âì ¯à¨ padding'¥ ᮤ¥à¦¨¬®£® ï祩ª¨.
- **˜ £¨ ॠ«¨§ æ¨¨**:
1. Walking pass ¯® 䥭ᠬ/â ¡«¨æ ¬ ¯àאַ ¢ `index_lines()` - ᮡà âì
extents ¢á¥å â ¡«¨æ.
2. „«ï ª ¦¤®© â ¡«¨æë - ®¯à¥¤¥«¨âì è¨à¨­ë ª®«®­®ª.
3. ¥è¥­¨¥: rewrite-in-buffer (¯à®é¥ ¤«ï ७¤¥à , ­® ¬ãâ¨àã¥â ¨á室­¨ª)
vs render-time layout (cleaner, ­® âॡã¥â ®â¤¥«ì­®© áâàãªâãàë
®¯¨á ­¨ï layout'  ­  ª ¦¤ãî â ¡«¨æã).
4. Hpan: ®¡é¨© `viewport_x_offset` ¤«ï ¢á¥£® íªà ­ , ¨«¨ ®â¤¥«ì­ë©
"è¨à®ª¨© ०¨¬" ⮫쪮 ¢­ãâਠ⠡«¨æ.
> ¥ ¡«®ª¨àãîé ï ä¨ç . ‡ ¯ã᪠âì ª®£¤  áâ ­¥â ¯®­ï⥭ ⨯®¢®© ¨áâ®ç­¨ª
> markdown-ä ©«®¢ (㧪¨¥ ç¨â «ª¨ -> ¤®áâ â®ç­® ⥪ã饣® ¤¥ª®à â®à ;
> è¨à®ª¨¥ README á ¡®«ì訬¨ â ¡«¨æ ¬¨ -> ­ã¦¥­ ¯®«­ë© layout).
---
### Phase - Šíè ७¤¥à¥­­ëå áâப (low priority)
Žâ«®¦¥­®: ⥪ãé ï ᪮à®áâì ¡®«¥¥ 祬 ¤®áâ â®ç­ . €ªâ¨¢¨à®¢ âì ¥á«¨ ¯®ï¢¨âáï
áæ¥­ à¨©, £¤¥ ¢¨¤­  § ¤¥à¦ª  PgUp/PgDn (­ ¯à¨¬¥à, ¯à¨ âï¦ñ«®¬ inline-¯ àá¥à¥
v2 á UTF-8 / linkifier / â ¡«¨æ ¬¨).
**Šíè ®âä®à¬ â¨à®¢ ­­ëå áâப** (W3, ®â¤¥«ì­ ï EMM-áâà ­¨æ ):
```
Cache layout (16 KB EMM page, ¢á¥£® 16000 ¡ ©â ¨á¯®«ì§ã¥âáï):
slot 0: 80 chars + 80 attrs = 160 bytes @ offset 0
...
slot 99: 80 chars + 80 attrs = 160 bytes @ offset 15840
Cache tags (W2 static): uint16_t cache_tag[100] = 200 bytes
cache_tag[i] = line_id, ¨«¨ 0xFFFF = invalid
```
‘âà â¥£¨ï - **direct map (no LRU)**: `slot = line_id % 100`. Š®««¨§¨ï -> ¢ëâ¥á­¥­¨¥.
**Batched viewport render**: 2 page-swap'  ­  ‚…‘œ viewport (cache -> file -> cache),
­¥ 60 ª ª ¯à¨ ­ ¨¢­®© ॠ«¨§ æ¨¨.
ਠᡮથ: `cache_blk = mem_alloc_pages(1)` ¯®á«¥ `file_blk`; `mem_free_block` ­  exit.
---
## —â® ®â«®¦¥­® ¢ v2
- ~~” ©«ë >16 KB~~ - **ᤥ« ­® ¢ v1.5** (¤® 128 KB ç¥à¥§ 1-8 EMM-áâà ­¨æ + lazy map ¢ W3).
- ~~Word wrap~~ - **ᤥ« ­®** (Phase 5, F2 toggle).
- Search (Find / Find next) - F3 / F4.
- F8 Raw / Render toggle - ᯥæ¨ä¨ª æ¨ï ¢ Phase 8.
- Links `[text](url)` + image alt - Phase 7.
- Tables - Phase 6 (¯®«­ë© layout).
- Toggle highlight on/off - ç áâ­ë© á«ãç © F8 Raw.
+133
View File
@@ -0,0 +1,133 @@
# MDView — Просмотрщик Markdown для Sprinter
**MDView** — программа для просмотра документов в формате *Markdown* на компьютере
Sprinter (процессор Z80). Документ хранится в отдельном W3 окне и не занимает
основную RAM программы.
## Возможности
- Документы до **128 КБ** (8 страниц EMM по 16 КБ каждая)
- До **16 384** экранных строк в индексе
- Автоматический перенос слов по ширине экрана (80 столбцов)
- Горизонтальный сдвиг для широких строк (блоки кода, таблицы)
- Статус-бар: имя файла, диапазон строк, процент прокрутки
- Спиннер в строке состояния во время загрузки и индексации
- Поддержка «мягкого» склеивания строк в абзацах и цитатах
## Запуск
```
mdview [имя_файла.md]
```
Если имя файла не задано, загружается `README.MD`.
## Управление
```
Клавиша Действие
───────────── ────────────────────────────────────────
Up Down Прокрутка на одну строку вверх / вниз
PgUp PgDn Прокрутка на страницу (30 строк)
Home Начало документа
End Конец документа
Left Right Горизонтальный сдвиг (только nowrap-строки)
F1 Окно справки
F10 / Esc Выход из программы
```
## Синтаксис Markdown
### Заголовки
Поддерживаются уровни H1–H4. Уровни H5 и H6 отображаются как H4.
# Заголовок первого уровня
## Заголовок второго уровня
### Заголовок третьего уровня
#### Заголовок четвёртого уровня
### Текстовое форматирование
**Жирный текст** выделяется двойными звёздочками: `**текст**`
*Курсив* выделяется одиночными звёздочками `*текст*` или знаком подчёркивания `_текст_`
`Встроенный код` обозначается обратными кавычками
~~Зачёркнутый текст~~ — двойные тильды: `~~текст~~`
### Ненумерованный список
Маркеры `-`, `*` или `+`:
- Первый пункт списка
- Второй пункт списка
- Третий пункт с достаточно длинным текстом, который при необходимости
будет перенесён на следующую строку с сохранением отступа
### Нумерованный список
1. Первый элемент
2. Второй элемент
3. Третий элемент
### Цитата
> Блок цитаты начинается с символа `>`. Несколько последовательных
> строк одной цитаты склеиваются в единый абзац с автоматическим
> переносом слов.
### Блок кода (verbatim)
Блок кода заключается в тройные обратные кавычки. Внутри блока
текст отображается «как есть» без разбора Markdown:
```
#include <stdio.h>
#include <sprinter.h>
int main(void) {
puts("Hello, Sprinter!");
return 0;
}
```
### Горизонтальная линия
Три или более символов `---`, `***` или `___` на отдельной строке:
---
## Технические характеристики
- **Платформа:** Sprinter, процессор Z80 @ 21 МГц
- **Кодировка:** CP866 (DOS Cyrillic)
- **Максимальный размер файла:** 128 КБ
- **Максимальное число строк в индексе:** 16 384
- **Режим памяти:** small
- Код программы, cтек, данные, куча — окнa W1-W2 (32 КБ, адреса 0x40000xBFFF).
- Буфер файла — страницы EMM, отображаемые в W3 (0xC0000xFFFF)
## TODO
1. **Увеличение размера документов.** Снять лимит 128 КБ: Достаточно
разрешить работать с большим кол-вом страниц памяти, пока оттестированно
на работе с 8-мю страницами по 16Кб.
2. **Форматированные таблицы.** Разбирать строки вида `| ячейка | ячейка |`
с автоматическим выравниванием столбцов и отрисовкой разделительных
линий (строки `|---|---|`). На текущий момент таблицы отображаются
как обычные nowrap-строки без выравнивания.
3. **Поддержка кодировок CP1251 и UTF-8.** Автоопределение кодировки
по BOM, явное указание через аргумент командной строки (`--encoding cp1251`),
возможность переключения кодировки во время просмотра (`F8`).
Нужно прежде всего для документов на русском языке; CP866 кодировака уже поддерживается.
4. **Ускорение рендеринга.** Кэш строк экрана. Оптимизация цикла вывода
символов через BIOS WRCHAR (пакетный вывод, DMA).
---
*MDView v0.2 · (c) 2026 Петров А.Г.*
-5
View File
@@ -1,5 +0,0 @@
MDVIEW Sample Document
This is a sample Markdown file for testing the Sprinter *mdview text
viewer*. Phase 3 adds inline emphasis: bold, italic and underscore
runs render with distinct background colours.
+112 -2
View File
@@ -87,6 +87,20 @@ static const uint8_t md_pallete[16 * 4] = {
#define ATTR_MENU_T COLOR(COLOR_BLACK, COLOR_LIGHTCYAN)
#define ATTR_MENU_K COLOR(COLOR_YELLOW, COLOR_BLACK)
/* ---- Атрибуты диалога справки ------------------------------------ */
#define ATTR_HELP_BG COLOR(COLOR_LIGHTGRAY, COLOR_BLACK)
#define ATTR_HELP_BDR COLOR(COLOR_WHITE, COLOR_BLACK)
#define ATTR_HELP_TIT COLOR(COLOR_YELLOW, COLOR_BLACK)
#define ATTR_HELP_HDR COLOR(COLOR_WHITE, COLOR_BLACK)
#define ATTR_HELP_HINT COLOR(COLOR_YELLOW, COLOR_BLACK)
/* Геометрия диалога справки (в символьных координатах 80×32). */
#define HELP_X 8u /* левая граница рамки */
#define HELP_Y 4u /* верхняя граница рамки */
#define HELP_W 64u /* ширина рамки (включая │) */
#define HELP_H 22u /* высота рамки (включая ─) */
/* ---- Глобальное состояние ---------------------------------------- */
/* Файл до 128 КБ хранится в наборе EMM-страниц (до 8 шт. по 16 КБ).
@@ -1662,6 +1676,86 @@ static void die(const char *msg)
(void)getkey();
}
/* ==================================================================
* Диалог справки (F1)
* ================================================================== */
/* Заполняет одну внутреннюю строку диалога (r=0 — первая строка за рамкой).
* Строка 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++;
}
}
static void show_help(void)
{
/* " Помощь " в CP866 (8 байт) */
static const char title[] = " \x8F\xAE\xAC\xAE\xE9\xEC ";
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;
/* Верхняя граница рамки с заголовком по центру */
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);
wrchar(HELP_X + HELP_W - 1u, HELP_Y, 0xBF, ATTR_HELP_BDR); /* ┐ */
/* Боковые границы (левый и правый │ для каждой строки тела) */
for (uint8_t r = 1u; r < HELP_H - 1u; r++) {
wrchar(HELP_X, HELP_Y + r, 0xB3, ATTR_HELP_BDR); /* │ */
wrchar(HELP_X + HELP_W - 1u, HELP_Y + r, 0xB3, ATTR_HELP_BDR); /* │ */
}
/* Нижняя граница рамки */
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);
wrchar(HELP_X + HELP_W - 1u, HELP_Y + HELP_H - 1u, 0xD9, ATTR_HELP_BDR); /* ┘ */
/* Содержимое (20 внутренних строк) */
uint8_t r = 0;
help_line(r++, "", ATTR_HELP_BG);
help_line(r++, " MDView v0.2 -- Markdown Viewer for Sprinter", ATTR_HELP_HDR);
help_line(r++, " (c) 2026 Petrov A.G.", ATTR_HELP_BG);
help_line(r++, "", ATTR_HELP_BG);
help_line(r++, " Navigation:", ATTR_HELP_HDR);
help_line(r++, " \x18 \x19 Scroll one line up / down", ATTR_HELP_BG);
help_line(r++, " PgUp PgDn Scroll one page up / down", ATTR_HELP_BG);
help_line(r++, " Home End Jump to beginning / end of document", ATTR_HELP_BG);
help_line(r++, " \x1B \x1A Horizontal pan (code blocks/tables)", ATTR_HELP_BG);
help_line(r++, " Esc F10 Exit", ATTR_HELP_BG);
help_line(r++, "", ATTR_HELP_BG);
help_line(r++, " Markdown elements:", ATTR_HELP_HDR);
help_line(r++, " # ## ### Headings H1-H6", ATTR_HELP_BG);
help_line(r++, " **bold** *italic* `code` ~~strike~~", ATTR_HELP_BG);
help_line(r++, " > quote ``` ... ``` Fenced code block", ATTR_HELP_BG);
help_line(r++, " - * + item 1. 2. Ordered list", ATTR_HELP_BG);
help_line(r++, " |----|----| Tables", ATTR_HELP_BG);
help_line(r++, "", ATTR_HELP_BG);
help_line(r++, " File size: up to 128 KB (EMM). Lines: up to 16384.", ATTR_HELP_BG);
help_line(r++, "", ATTR_HELP_BG);
(void)getkey();
render_full_status();
render_viewport();
render_menu();
}
/* ==================================================================
* Точка входа
* ================================================================== */
@@ -1669,8 +1763,23 @@ static void die(const char *msg)
int main(int argc, char **argv)
{
const char *path;
if (argc >= 2) path = argv[1];
else path = "SAMPLE.MD";
if (argc >= 2) {
path = argv[1];
} else {
path = "README.MD";
/* Дефолтный файл отсутствует — выводим подсказку без смены режима экрана. */
{
int chk = open(path, O_RDONLY);
if (chk < 0) {
cputs("MDView v0.2 -- Markdown Viewer for Sprinter\r\n");
cputs("(c) 2026 Petrov A.G.\r\n\r\n");
cputs("Usage: mdview <file.md>\r\n");
cputs(" Displays a Markdown document (CP866, up to 128 KB).\r\n");
return 1;
}
close(chk);
}
}
/* Копируем путь в static filename[] для отображения в статус-баре. */
{
@@ -1730,6 +1839,7 @@ int main(int argc, char **argv)
continue;
}
switch (scan) {
case KEY_F1: show_help(); break;
case KEY_F10: goto exit_loop;
case KEY_UP: scroll_up(1); break;
case KEY_DOWN: scroll_down(1); break;