diff --git a/examples/mdview/Makefile b/examples/mdview/Makefile index 679fff5..1353016 100644 --- a/examples/mdview/Makefile +++ b/examples/mdview/Makefile @@ -6,5 +6,5 @@ PROJ_ROOT := $(abspath $(CURDIR)/../..) EXAMPLE := mdview MEMORY := small -EXTRA_DATA := SAMPLE.MD +EXTRA_DATA := SAMPLE.MD PLAN_866.md include $(PROJ_ROOT)/app.mk diff --git a/examples/mdview/PLAN.md b/examples/mdview/PLAN.md index 801bdf8..2af3364 100644 --- a/examples/mdview/PLAN.md +++ b/examples/mdview/PLAN.md @@ -5,10 +5,36 @@ Тестовая крупная задача — проверить нашу libc на нетривиальном interactive-приложении (полноэкранный UI, файловый I/O, парсер). Параллельно даст хороший showcase платформы и поможет вытащить недоделки в conio/io. Конечная цель: viewer для `.md` файлов с подсветкой синтаксиса, навигацией по тексту и постраничным скроллингом. **Ограничения v1 (зафиксированы пользователем):** -- POSIX file API (`open/read/lseek/close`); FILE*/fread/fgets — не использовать -- Максимум 16 KB на один файл (одна EMM-страница); многостраничный режим — в v2 +- 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, ↑↓/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 ` (3 пробела между MDVIEW и filename, slot спиннера — col 8) +- Spinner крутится во время `load_file` (по странице) и `index_lines` (раз в 32 логических строки), включая `toggle_wrap` + --- ## Архитектура @@ -16,11 +42,14 @@ ### Раскладка экрана (80×32, текст mode 0x03) ``` -Row 0: ┃ mdview.md L 1-30 / 142 21% ┃ ← status bar (BG=blue, FG=white) -Row 1: ┃ ┃ -... ┃ document viewport (30 rows) ┃ -Row 30: ┃ ┃ -Row 31: ┃ F1 Help F10 Exit┃ ← menu bar (BG=blue, FG=cyan) +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 столбцов. @@ -34,15 +63,16 @@ Memory mode: **`small`** — DSS отводит под наш образ **дв 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-страницами: - ├── file page: исходный текст .md файла (16 KB) - └── cache page: кэш отформатированных строк (Phase 3+) +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 -- v2-расширение (multi-page файлы >16 KB): просто `mem_alloc_pages(N)` и переключение страниц в W3 через `sprinter_page_w3()` +- Файлы до 128 KB поддерживаются нативно: `mem_alloc_pages(pages_needed)` под весь файл; `map_page()` через `sprinter_page_w3()`; `fb(p)` — единая точка доступа из индексатора и рендера. Статики (в W2): - `line_offset[MAX_LINES]` — uint16_t смещение каждой строки в файловой странице (4 KB на 2048 строк) @@ -65,7 +95,7 @@ main → open() → read() chunks 1KB → write to W1 (mapped EMM page) → clos ## Реализация — поэтапная -### Phase 1 — Plain text viewer (MVP) +### Phase 1 — Plain text viewer (MVP) ✓ **Что работает:** - Загрузка файла (`open/read/close`) в W1-страницу @@ -81,7 +111,7 @@ main → open() → read() chunks 1KB → write to W1 (mapped EMM page) → clos - `examples/mdview/Makefile` - `examples/mdview/SAMPLE.MD` — тестовый markdown файл -### Phase 2 — Headers и горизонтальная линия +### Phase 2 — Headers и горизонтальная линия ✓ **MD фичи:** - `# H1` → ярко-жёлтый (COLOR_YELLOW = 14) на чёрном @@ -90,7 +120,7 @@ main → open() → read() chunks 1KB → write to W1 (mapped EMM page) → clos - `#### H4+` → серый (COLOR_GREY = 8) - `---` / `***` на отдельной строке → линия 0xC4 (горизонтальная рамка ASCII) во всю ширину -### Phase 3 — Inline emphasis +### Phase 3 — Inline emphasis ✓ **Парсер inline (per-line, runs в одну строку):** - `**bold**` → ATTR_TEXT_BOLD @@ -99,12 +129,23 @@ main → open() → read() chunks 1KB → write to W1 (mapped EMM page) → clos - `` `code` `` → ATTR_TEXT_CODE - Маркеры `**`/`*`/`_`/`` ` `` НЕ рендерятся (съедаются) -State machine: один активный стиль одновременно (без вложенности); конфликтующий маркер при чужом активном стиле уходит литералом. Состояние сбрасывается на каждой строке. +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 +### Phase 4 — Block elements ✓ (без таблиц) - Маркированные списки: `- foo`, `* foo`, `+ foo` → префикс `•` (0x07) + пробел; цвет маркера ярче основного - Нумерованные списки: `1. foo`, `2. foo` → как есть (число оставляем) @@ -138,10 +179,11 @@ State machine: один активный стиль одновременно (б ≥ content_col родителя, иначе считается breakout. Нужен мини-stack активных списков при индексации. -### Phase 5 — Wrap / Unwrap длинных строк +### Phase 5 — Wrap / Unwrap длинных строк ✓ Дефолт: **wrap on**. F2 переключает; в меню-баре подпись отражает действие -("Unwrap" когда wrap включён, "Wrap" когда выключен). +("Unwrap" когда wrap включён, "Wrap" когда выключен). Во время реиндексации +крутится спиннер на title bar. **v1 — реализовано:** - Один массив `line_offset[2048]` хранит ВИДИМЫЕ сегменты (а не логические @@ -519,9 +561,10 @@ Cache tags (W2 static): uint16_t cache_tag[100] = 200 bytes ## Что отложено в v2 -- Файлы >16 KB: многостраничное хранение через `mem_alloc_pages(N)`, ленивая загрузка страниц в W3 при доступе. +- ~~Файлы >16 KB~~ — **сделано в v1.5** (до 128 KB через 1–8 EMM-страниц + lazy map в W3). +- ~~Word wrap~~ — **сделано** (Phase 5, F2 toggle). - Search (Find / Find next) — F3 / F4. -- Toggle highlight on/off — F2. -- Word wrap. -- Image alt-text rendering. -- Tables. +- F8 Raw / Render toggle — спецификация в Phase 8. +- Links `[text](url)` + image alt — Phase 7. +- Tables — Phase 6 (полный layout). +- Toggle highlight on/off — частный случай F8 Raw. diff --git a/examples/mdview/PLAN_866.md b/examples/mdview/PLAN_866.md index ad455d3..1d632c1 100644 --- a/examples/mdview/PLAN_866.md +++ b/examples/mdview/PLAN_866.md @@ -5,9 +5,35 @@ ⮢ 㯭 - ஢ libc ਢ쭮 interactive-ਫ (࠭ UI, 䠩 I/O, ). ࠫ쭮 訩 showcase conio/io. 筠 楫: viewer `.md` 䠩 ᢥ⪮ ᨭ⠪, 樥 ⥪ ࠭ ஫. **࠭祭 v1 (䨪஢ 짮⥫):** -- POSIX file API (`open/read/lseek/close`); FILE*/fread/fgets - ᯮ짮 -- ᨬ 16 KB 䠩 ( EMM-࠭); ࠭ ० - v2 -- ᢥ⪠ १ 梥: **ࠧ ** > 梥 ; **bold/italic** > 梥 䮭 (ਭ 䮭 ୮/ᨢ ⠭) +- 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 ` (3 ஡ MDVIEW filename, slot ᯨ - col 8) +- Spinner ६ `load_file` ( ࠭) `index_lines` (ࠧ 32 ᪨ ப), `toggle_wrap` --- @@ -16,11 +42,14 @@ ### ᪫ ࠭ (80x32, ⥪ mode 0x03) ``` -Row 0: mdview.md L 1-30 / 142 21% < status bar (BG=blue, FG=white) -Row 1: -... document viewport (30 rows) -Row 30: -Row 31: F1 Help F10 Exit < menu bar (BG=blue, FG=cyan) +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 ⮫殢. @@ -34,15 +63,16 @@ Memory mode: **`small`** - DSS 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-࠭栬: - file page: 室 ⥪ .md 䠩 (16 KB) - cache page: ଠ஢ ப (Phase 3+) +W3 (0xC000-0xFFFF): paged window - ४砥 EMM-࠭栬 + 䠩 ( 8 ࠭ = 128 KB). `cur_page` + ⥪ mapping, `fb(p)` 㦭 ࠭ + ࢮ 饭. ``` 祬 small + W3: -- 32 KB + 訬 ᮬ > +- 32 KB + 訬 ᮬ -> - W3 - ⠭ paged window, 㦥 `bank_io_w3` API -- v2-७ (multi-page 䠩 >16 KB): `mem_alloc_pages(N)` ४祭 ࠭ W3 १ `sprinter_page_w3()` +- 128 KB ন ⨢: `mem_alloc_pages(pages_needed)` 䠩; `map_page()` १ `sprinter_page_w3()`; `fb(p)` - 窠 㯠 ७. ⨪ ( W2): - `line_offset[MAX_LINES]` - uint16_t ᬥ饭 ப 䠩 ࠭ (4 KB 2048 ப) @@ -54,9 +84,9 @@ W3 (0xC000-0xFFFF): paged window - ### ⮪ ``` -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() } +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()`. @@ -65,14 +95,14 @@ main > open() > read() chunks 1KB > write to W1 (mapped EMM page) > close() ## - ⠯ -### Phase 1 - Plain text viewer (MVP) +### 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) +- : ^/V (1 ப), PgUp/PgDn (30 ப), Home/End (砫/), Esc/F10 (室), F1 (help screen) - १ ப 80 ᨬ ( word-wrap) - : ⥪ ୮; status/menu - ᨭ @@ -81,46 +111,57 @@ main > open() > read() chunks 1KB > write to W1 (mapped EMM page) > close() - `examples/mdview/Makefile` - `examples/mdview/SAMPLE.MD` - ⮢ markdown 䠩 -### Phase 2 - Headers ਧ⠫쭠 +### Phase 2 - Headers ਧ⠫쭠 **MD :** -- `# H1` > મ- (COLOR_YELLOW = 14) ୮ -- `## H2` > મ-㡮 (COLOR_LBLUE = 11) -- `### H3` > મ- (COLOR_LGREEN = 10) -- `#### H4+` > (COLOR_GREY = 8) -- `---` / `***` ⤥쭮 ப > 0xC4 (ਧ⠫쭠 ࠬ ASCII) ਭ +- `# H1` -> મ- (COLOR_YELLOW = 14) ୮ +- `## H2` -> મ-㡮 (COLOR_LBLUE = 11) +- `### H3` -> મ- (COLOR_LGREEN = 10) +- `#### H4+` -> (COLOR_GREY = 8) +- `---` / `***` ⤥쭮 ப -> 0xC4 (ਧ⠫쭠 ࠬ ASCII) ਭ -### Phase 3 - Inline emphasis +### Phase 3 - Inline emphasis ** inline (per-line, runs ப):** -- `**bold**` > ATTR_TEXT_BOLD -- `*italic*` > ATTR_TEXT_ITALIC -- `_underscore_` > ATTR_TEXT_UNDERSORE -- `` `code` `` > ATTR_TEXT_CODE +- `**bold**` -> ATTR_TEXT_BOLD +- `*italic*` -> ATTR_TEXT_ITALIC +- `_underscore_` -> ATTR_TEXT_UNDERSORE +- `` `code` `` -> ATTR_TEXT_CODE - થ `**`/`*`/`_`/`` ` `` ७ (ꥤ) -State machine: ⨢ ⨫ ६ ( ); 䫨騩 થ 㦮 ⨢ ⨫ 室 ࠫ. ﭨ 뢠 ப. +State machine: ⨢ ⨫ ६ ( ); 䫨騩 થ 㦮 ⨢ ⨫ ࠢ  (zero-width) ᨭ஭樨 ਭ index_lines. ﭨ 뢠 ப. -> ** ଠ஢ ப** - ⫮ ᠬ , . "Phase ?: ⨬樨". +**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 +### Phase 4 - Block elements ( ⠡) -- ન஢ ᯨ᪨: `- foo`, `* foo`, `+ foo` > 䨪 `` (0x07) + ஡; 梥 થ ᭮ -- 㬥஢ ᯨ᪨: `1. foo`, `2. foo` > (᫮ ⠢塞) -- Blockquote: ப `> ` > 䨪 `` (0xB3) ண 梥, ⠫쭮 ⥪ ᫥ ਣ +- ન஢ ᯨ᪨: `- 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 + 頥 `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). +- Tab-indent -> ᯮ nesting (⮫쪮 spaces). **Phase 4-full - প (deferred):** - **Tab-indent**: tab = 4 ஡ । ஢. @@ -135,18 +176,19 @@ State machine: ।騬 bullet', த ⮣ bullet' (㠫쭮 - 騩 attr). - **Strict CommonMark indent rules**: 㭪 indent - ? content_col த⥫, ⠥ breakout. 㦥 -stack + >= content_col த⥫, ⠥ breakout. 㦥 -stack ⨢ ᯨ᪮ 樨. -### Phase 5 - Wrap / Unwrap ப +### Phase 5 - Wrap / Unwrap ப 䮫: **wrap on**. F2 ४砥; - ࠦ ⢨ -("Unwrap" wrap , "Wrap" 몫祭). +("Unwrap" wrap , "Wrap" 몫祭). ६ २樨 + ᯨ title bar. **v1 - ॠ:** - ᨢ `line_offset[2048]` ࠭ ᥣ ( ᪨ ப); 0..13 - ⮢ ᬥ饭, 15 - CONT-䫠 continuation. -- Wrap-०: soft wrap ᫥ ஡ ? 80; hard fallback ᫨ +- Wrap-०: soft wrap ᫥ ஡ <= 80; hard fallback ᫨ ஡ . - થ 䠧 (`**`/`*`/`_`/`` ` ``) header-䨪 (`#`/`##`/...) 뢠 visible-col ᪥ 窨 ७. @@ -171,15 +213,15 @@ State machine: seg (3 ). - Compact way: ࠫ ᨢ `seg_meta[MAX_SEGS]` 1 - marker_width (4 ) + emph_state (3 ) + base_attr_idx - (4 ⠡ > 㦥 2-⮢ seg_meta). -- **Hpan ப**: ᫨ wrap 몫祭, + (4 ⠡ -> 㦥 2-⮢ seg_meta). +- **Hpan ப**: ᫨ wrap 몫祭, <-/-> ਧ⠫쭮 ஫ >80 cols. 騩 堭 tables (Phase 6). - **** (light) - `viewport_x` + re-render . + **** (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 + (HPAN_STEP cols x 30 rows 240 wrchar 2400). ESTEX SCROLL ਧ⠫ ন - 㦭 WINCOPY- rdchar/wrchar loop. ⨢஢ ମ; ᥩ ⨯ markdown' ⭮. @@ -187,13 +229,13 @@ State machine: **v2 - ⤥쭠 , wrap:** - Toggle ᢥ⪨ 楫 (F3?) - Search ⥪ (Ctrl+F / F4) -- Links `[text](url)` > ᨭ ભ text, url -- Images `![alt](path)` > `[IMG: alt]` +- Links `[text](url)` -> ᨭ ભ text, url +- Images `![alt](path)` -> `[IMG: alt]` ### Phase 7 - Links (post-v1) -- `[text](url)` > ᮢ ⮫쪮 `text` મ-ᨭ FG (㠫쭮 ભ⮥) -- `![alt](path)` > `[IMG: alt]` ᪮ +- `[text](url)` -> ᮢ ⮫쪮 `text` મ-ᨭ FG (㠫쭮 ભ⮥) +- `![alt](path)` -> `[IMG: alt]` ᪮ - Search ⥪ (F3 / Ctrl+F): ६⠫, ᢥ⪠ ᮢ ### Phase 8 - F8 Raw / Render toggle @@ -223,7 +265,7 @@ State machine: **쭠 ॠ:** - static `uint8_t render_mode = 1;` -- `render_line()`: ᠬ `if (!render_mode) { ? raw render ? return; }` +- `render_line()`: ᠬ `if (!render_mode) { ... raw render ... return; }` - `render_menu()`: F8 冷 F2. - 横: `case KEY_F8: toggle_render(); break;` - `toggle_render()` ⫨砥 `toggle_wrap()` ⥬, ࠨ @@ -394,7 +436,7 @@ int main(int argc, char **argv) { uint8_t ascii = k & 0xFF; uint8_t scan = (k >> 8) & 0x7F; // strip mod bit if (ascii) { - if (ascii == 0x1B) break; // Esc > exit + if (ascii == 0x1B) break; // Esc -> exit continue; } switch (scan) { @@ -424,11 +466,11 @@ exit: ### 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` +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 ப + - ^/V: 1 ப - PgUp/PgDn: 30 ப, ४⭮ clamp ࠭ - Home: top_line=0 - End: top_line = total_lines - VIEW_H @@ -447,7 +489,7 @@ exit: ## 襭 筮 1. **Word-wrap vs truncate**: v1 = truncate (). v2 - F2 toggle wrap. -2. **ਧ⠫ ஫**: v1 - ; v2 - `` ᤢ viewport ⮫栬. +2. **ਧ⠫ ஫**: v1 - ; v2 - `<-/->` ᤢ viewport ⮫栬. 3. **Tab handling**: ८ࠧ ७, **tabstop = 4** (⠭ MD). ਣ W3 ண. 4. **UTF-8**: ७ਬ . ᫨ 䠩 CP866 - ਫ楩 १ ⥬ 䮭. UTF-8 - ন (㠫쭮 㤥 -ASCII ᨬ; ⥪ ।० - v2). @@ -466,13 +508,13 @@ exit: ९ ப FILE_BUF ⠪, ⮡ 祩 ਭ; top/bottom ࠬ (`Ŀ` / ``) ᨭ᪨ ப. ࠭ 1:1 ᮮ⢥⢨ "᪠ - ப > viewport row" ᯥ樠쭮 ७. + ப -> viewport row" ᯥ樠쭮 ७. - ****: re-emit 䠩 padding .ࠬ. ᫨ 16KB - १ ⠡ overflow'. - **ਧ⠫ ஫**: ᫨ ⮣ ਭ ⠡ (  - ப) > SCREEN_W = 80 - horizontal pan. 㤥 + ப) > SCREEN_W = 80 - <-/-> horizontal pan. 㤥 騩 堭 ப (. ⠪ wrap mode), ⮫쪮 ⠡. -- **ࠢ separator-row**: `:-` > left, `-:` > right, `:-:` > +- **ࠢ separator-row**: `:-` -> left, `-:` -> right, `:-:` -> center; 뢠 padding' ᮤন 祩. - ** ॠ樨**: 1. Walking pass 䥭ᠬ/⠡栬 אַ `index_lines()` - ᮡ @@ -485,12 +527,12 @@ exit: "ப ०" ⮫쪮 ⠡. > . ᪠ ⠭ ⥭ ⨯ 筨 -> markdown-䠩 (㧪 ⠫ > 筮 ⥪饣 ; -> ப README 訬 ⠡栬 > 㦥 layout). +> markdown-䠩 (㧪 ⠫ -> 筮 ⥪饣 ; +> ப README 訬 ⠡栬 -> 㦥 layout). --- -### Phase ? - ७७ ப (low priority) +### Phase - ७७ ப (low priority) ⫮: ⥪ ᪮ 祬 筠. ⨢஢ ᫨  業਩, প PgUp/PgDn (ਬ, 񫮬 inline- @@ -508,9 +550,9 @@ 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`. > ᭥. +⥣ - **direct map (no LRU)**: `slot = line_id % 100`. -> ᭥. -**Batched viewport render**: 2 page-swap' viewport (cache > file > cache), +**Batched viewport render**: 2 page-swap' viewport (cache -> file -> cache), 60 ॠ樨. ᡮથ: `cache_blk = mem_alloc_pages(1)` ᫥ `file_blk`; `mem_free_block` exit. @@ -519,9 +561,10 @@ Cache tags (W2 static): uint16_t cache_tag[100] = 200 bytes ## ⫮ v2 -- >16 KB: ࠭筮 ࠭ १ `mem_alloc_pages(N)`, 㧪 ࠭ W3 㯥. +- ~~ >16 KB~~ - **ᤥ v1.5** ( 128 KB १ 1-8 EMM-࠭ + lazy map W3). +- ~~Word wrap~~ - **ᤥ** (Phase 5, F2 toggle). - Search (Find / Find next) - F3 / F4. -- Toggle highlight on/off - F2. -- Word wrap. -- Image alt-text rendering. -- Tables. +- F8 Raw / Render toggle - ᯥ䨪 Phase 8. +- Links `[text](url)` + image alt - Phase 7. +- Tables - Phase 6 ( layout). +- Toggle highlight on/off - 砩 F8 Raw. diff --git a/examples/mdview/SAMPLE.MD b/examples/mdview/SAMPLE.MD index 7107f7e..130155c 100644 --- a/examples/mdview/SAMPLE.MD +++ b/examples/mdview/SAMPLE.MD @@ -1,165 +1,5 @@ -# MDVIEW Sample Document +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_ +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. - ---- - -## Section: Inline emphasis - -Plain words mixed with **bold words**, *italic words*, _underscore -words_ and `code words` to verify all four styles render with their -own colours. - -A single **bold** stretch, then a single *italic* stretch, then a single -_underscore_ stretch, then a single `code` stretch, all on the same line. - -A *long italic run that spans multiple words and several columns before -it closes here* and continues plain. - -Inline code with punctuation: call `printf("%d\n", x)` then check the -result; or use `argv[0]` to grab the program name. - -Unclosed emphasis (open **bold left dangling) — should auto-close at the -end of the line so the next line starts clean. - -Conflict cases: **bold with a stray * inside** stays bold, and *italic -with a stray _ inside* stays italic, and `code with **bold** inside` -stays code. - -## Section: Lorem ipsum - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod -tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim -veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea -commodo consequat. - -Duis aute irure dolor in reprehenderit in voluptate velit esse cillum -dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non -proident, sunt in culpa qui officia deserunt mollit anim id est laborum. - -## Section: Lists - -Unordered (dash): - -- alpha -- bravo -- charlie - -Unordered (asterisk): - -* delta -* echo - -Unordered (plus): - -+ foxtrot -+ golf - -Numbered list (digits + period highlighted): - -1. first -2. second -3. third -10. tenth — verifies multi-digit numbering -99. ninety-ninth - -Numbered with parens: - -1) parenthesised one -2) parenthesised two - -Nested lists (light v1 — leading spaces enable detection at non-zero col; -no hanging indent in wrap continuation yet): - -- top level alpha - - nested level 1 bravo - - nested level 1 charlie - - nested level 2 delta - - nested level 2 echo - - nested level 3 foxtrot - - nested level 1 golf -- top level hotel - -Mixed nested: - -1. first top - 1. nested numeric - 2. nested numeric - - mixed bullet under numeric - - another bullet - 3. nested numeric -2. second top - -## Section: Blockquote - -> Single-line blockquote. - -> Multi-line blockquote starts here. -> Each line keeps the > prefix highlighted -> while the body stays plain text. - -## Section: Code - -Inline code: `printf("hello")`. - -Fenced block (lines inside should render with code attribute on the -full row, no inline parsing — note the **stars** and *italics* below -remain literal): - -```c -int main(void) { - /* **not bold**, *not italic*, `not nested` */ - return 42; -} -``` - -After the block, normal **bold** parsing resumes. - -## Section: Long line truncation - -This line is intentionally long to verify that the viewer truncates at 80 columns instead of wrapping or scrolling horizontally. Anything past the 80th visible column should simply not appear on screen. - -## Section: Tabs - -Indented with tabs: - - tab-indent level 1 - tab-indent level 2 - tab-indent level 3 - -## Section: Blockquote - -> Markdown blockquotes start with a greater-than sign at column zero. -> Multiple lines look like this. - -### Subsection: deeper heading (H3) - -The line above is an H3 — it should render in a colour distinct from -H1 and H2. - -#### Subsubsection: H4 and below - -H4 (and the rarely-seen H5/H6) all share the H4 colour slot. - -*** - -## Section: Filler - -The remaining content exists to make the document scroll past one viewport. - -Line 50 ........... approx. -Line 51 ........... . -Line 52 ........... .. -Line 53 ........... ... -Line 54 ........... .... -Line 55 ........... ..... -Line 56 ........... ...... -Line 57 ........... ....... -Line 58 ........... ........ -Line 59 ........... ......... -Line 60 ........... .......... - -End-of-document marker. If you can see this line you can use Home / End -to bounce between the start and finish of the file. diff --git a/examples/mdview/SAMPLE.MD_ b/examples/mdview/SAMPLE.MD_ new file mode 100644 index 0000000..7107f7e --- /dev/null +++ b/examples/mdview/SAMPLE.MD_ @@ -0,0 +1,165 @@ +# 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. + +--- + +## Section: Inline emphasis + +Plain words mixed with **bold words**, *italic words*, _underscore +words_ and `code words` to verify all four styles render with their +own colours. + +A single **bold** stretch, then a single *italic* stretch, then a single +_underscore_ stretch, then a single `code` stretch, all on the same line. + +A *long italic run that spans multiple words and several columns before +it closes here* and continues plain. + +Inline code with punctuation: call `printf("%d\n", x)` then check the +result; or use `argv[0]` to grab the program name. + +Unclosed emphasis (open **bold left dangling) — should auto-close at the +end of the line so the next line starts clean. + +Conflict cases: **bold with a stray * inside** stays bold, and *italic +with a stray _ inside* stays italic, and `code with **bold** inside` +stays code. + +## Section: Lorem ipsum + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod +tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim +veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea +commodo consequat. + +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum +dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non +proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + +## Section: Lists + +Unordered (dash): + +- alpha +- bravo +- charlie + +Unordered (asterisk): + +* delta +* echo + +Unordered (plus): + ++ foxtrot ++ golf + +Numbered list (digits + period highlighted): + +1. first +2. second +3. third +10. tenth — verifies multi-digit numbering +99. ninety-ninth + +Numbered with parens: + +1) parenthesised one +2) parenthesised two + +Nested lists (light v1 — leading spaces enable detection at non-zero col; +no hanging indent in wrap continuation yet): + +- top level alpha + - nested level 1 bravo + - nested level 1 charlie + - nested level 2 delta + - nested level 2 echo + - nested level 3 foxtrot + - nested level 1 golf +- top level hotel + +Mixed nested: + +1. first top + 1. nested numeric + 2. nested numeric + - mixed bullet under numeric + - another bullet + 3. nested numeric +2. second top + +## Section: Blockquote + +> Single-line blockquote. + +> Multi-line blockquote starts here. +> Each line keeps the > prefix highlighted +> while the body stays plain text. + +## Section: Code + +Inline code: `printf("hello")`. + +Fenced block (lines inside should render with code attribute on the +full row, no inline parsing — note the **stars** and *italics* below +remain literal): + +```c +int main(void) { + /* **not bold**, *not italic*, `not nested` */ + return 42; +} +``` + +After the block, normal **bold** parsing resumes. + +## Section: Long line truncation + +This line is intentionally long to verify that the viewer truncates at 80 columns instead of wrapping or scrolling horizontally. Anything past the 80th visible column should simply not appear on screen. + +## Section: Tabs + +Indented with tabs: + + tab-indent level 1 + tab-indent level 2 + tab-indent level 3 + +## Section: Blockquote + +> Markdown blockquotes start with a greater-than sign at column zero. +> Multiple lines look like this. + +### Subsection: deeper heading (H3) + +The line above is an H3 — it should render in a colour distinct from +H1 and H2. + +#### Subsubsection: H4 and below + +H4 (and the rarely-seen H5/H6) all share the H4 colour slot. + +*** + +## Section: Filler + +The remaining content exists to make the document scroll past one viewport. + +Line 50 ........... approx. +Line 51 ........... . +Line 52 ........... .. +Line 53 ........... ... +Line 54 ........... .... +Line 55 ........... ..... +Line 56 ........... ...... +Line 57 ........... ....... +Line 58 ........... ........ +Line 59 ........... ......... +Line 60 ........... .......... + +End-of-document marker. If you can see this line you can use Home / End +to bounce between the start and finish of the file. diff --git a/examples/mdview/mdview.c b/examples/mdview/mdview.c index 3459abd..cf0ad85 100644 --- a/examples/mdview/mdview.c +++ b/examples/mdview/mdview.c @@ -1,17 +1,19 @@ /* - * mdview — markdown text viewer for Sprinter (Phase 1: plain text). + * mdview — markdown text viewer for Sprinter. * * Layout (80x32): * row 0 — status bar (filename / line range / total / %) * rows 1..30 — viewport (30 lines of document text) - * row 31 — menu bar (F1 Help ... F10 Exit) + * row 31 — menu bar (F1 Help / F2 Wrap / F10 Exit) * - * Navigation: arrows, PgUp/PgDn, Home/End, F1 help, F10/Esc exit. + * Navigation: arrows, PgUp/PgDn, Home/End, F1 help, F2 wrap toggle, + * F10/Esc exit. In Unwrap mode ←/→ pan horizontally. * * Memory: * CODE → W1 (sprinter-cc --memory small) * STACK/HEAP/DATA → W2 - * File buffer → EMM page mapped into W3 at 0xC000 (≤ 16 KB) + * File buffer → EMM pages (up to 8 × 16 KB = 128 KB) mapped + * into W3 at 0xC000 on demand via fb()/map_page(). */ #include @@ -22,6 +24,7 @@ #include #include #include +#include /* ---- screen geometry ---------------------------------------------- */ @@ -34,63 +37,120 @@ /* ---- file/memory layout ------------------------------------------- */ -#define MAX_FILE 16384 -#define MAX_LINES 2048 -#define FILE_BUF ((char *)0xC000) /* W3-mapped EMM page */ +#define PAGE_BITS 14u +#define PAGE_SIZE (1u << PAGE_BITS) /* 16 KB */ +#define PAGE_MASK ((uint16_t)(PAGE_SIZE - 1u)) +#define MAX_PAGES 8 /* 8 × 16 KB = 128 KB */ +#define MAX_FILE ((uint32_t)MAX_PAGES * PAGE_SIZE) /* 131072 */ +#define MAX_LINES 2048 +#define FILE_BUF ((char *)0xC000) /* W3-mapped EMM page */ /* ---- attribute palette -------------------------------------------- */ -#define ATTR_TEXT COLOR(COLOR_LIGHTGRAY, COLOR_BLACK) -#define ATTR_TEXT_TITLE1 COLOR(COLOR_YELLOW, COLOR_BLACK) -#define ATTR_TEXT_TITLE2 COLOR(COLOR_LIGHTBLUE, COLOR_BLACK) -#define ATTR_TEXT_TITLE3 COLOR(COLOR_LIGHTGREEN, COLOR_BLACK) -#define ATTR_TEXT_TITLE4 COLOR(COLOR_DARKGRAY, COLOR_BLACK) -#define ATTR_TEXT_BOLD COLOR(COLOR_WHITE, COLOR_BLUE) -#define ATTR_TEXT_ITALIC COLOR(COLOR_LIGHTGREEN, COLOR_BROWN) -#define ATTR_TEXT_UNDERSORE COLOR(COLOR_LIGHTBLUE, COLOR_GREEN) -#define ATTR_TEXT_CODE COLOR(COLOR_WHITE, COLOR_LIGHTGRAY) -#define ATTR_LIST_MARKER COLOR(COLOR_LIGHTCYAN, COLOR_BLACK) -#define ATTR_QUOTE_MARKER COLOR(COLOR_LIGHTMAGENTA,COLOR_BLACK) -#define ATTR_HR COLOR(COLOR_DARKGRAY, COLOR_BLACK) -#define ATTR_TRUNC COLOR(COLOR_WHITE, COLOR_BLACK) -#define ATTR_BAR COLOR(COLOR_WHITE, COLOR_BLUE) -#define ATTR_MENU_T COLOR(COLOR_WHITE, COLOR_BLUE) -#define ATTR_MENU_K COLOR(COLOR_YELLOW, COLOR_BLUE) +static const uint8_t md_pallete[16 * 4] = { + 0x00, 0x00, 0x00, 0x00, + 0x60, 0x00, 0x00, 0x00, + 0x00, 0x60, 0x00, 0x00, + 0x60, 0x60, 0x00, 0x00, + 0x00, 0x00, 0x60, 0x00, + 0x60, 0x00, 0x60, 0x00, + 0x00, 0x60, 0x60, 0x00, + 0xC8, 0xC8, 0xC8, 0x00, + 0xA0, 0xA0, 0x80, 0x00, + 0xFF, 0xAA, 0xAA, 0x00, + 0xAA, 0xFF, 0xAA, 0x00, + 0xFF, 0xFF, 0xAA, 0x00, + 0xAA, 0xAA, 0xFF, 0x00, + 0xFF, 0xAA, 0xFF, 0x00, + 0xAA, 0xFF, 0xFF, 0x00, + 0xFF, 0xFF, 0xFF, 0xFF +}; +// static uint8_t md_pallete[16 * 4] = { +// 0x00,0x00,0x00, 0x00, +// 0x10,0x10,0x10, 0x00, +// 0x20,0x20,0x20, 0x00, +// 0x30,0x30,0x30, 0x00, +// 0x40,0x40,0x40, 0x00, +// 0x50,0x50,0x50, 0x00, +// 0x60,0x60,0x60, 0x00, +// 0x70,0x70,0x70, 0x00, +// 0x80,0x80,0x80, 0x00, +// 0x90,0x90,0x90, 0x00, +// 0xA0,0xA0,0xA0, 0x00, +// 0xB0,0xB0,0xB0, 0x00, +// 0xC0,0xC0,0xC0, 0x00, +// 0xD0,0xD0,0xD0, 0x00, +// 0xE0,0xE0,0xE0, 0x00, +// 0xF0,0xF0,0xF0, 0x00 +// }; + +#define ATTR_TEXT COLOR(COLOR_LIGHTGRAY, COLOR_BLUE) +#define ATTR_TEXT_TITLE1 COLOR(COLOR_YELLOW, COLOR_BLUE) +#define ATTR_TEXT_TITLE2 COLOR(COLOR_LIGHTBLUE, COLOR_BLUE) +#define ATTR_TEXT_TITLE3 COLOR(COLOR_LIGHTGREEN, COLOR_BLUE) +#define ATTR_TEXT_TITLE4 COLOR(COLOR_LIGHTGREEN, COLOR_BLUE) +#define ATTR_TEXT_BOLD COLOR(COLOR_LIGHTRED, COLOR_BLUE) +#define ATTR_TEXT_ITALIC COLOR(COLOR_LIGHTGREEN, COLOR_BLUE) +#define ATTR_TEXT_UNDERSORE COLOR(COLOR_LIGHTMAGENTA, COLOR_BLUE) +#define ATTR_TEXT_CODE COLOR(COLOR_WHITE, COLOR_BLUE) +#define ATTR_LIST_MARKER COLOR(COLOR_LIGHTCYAN, COLOR_BLUE) +#define ATTR_QUOTE_MARKER COLOR(COLOR_CYAN, COLOR_BLUE) +#define ATTR_HR COLOR(COLOR_LIGHTGRAY, COLOR_BLUE) +#define ATTR_TRUNC COLOR(COLOR_YELLOW, COLOR_BLUE) +#define ATTR_BAR COLOR(COLOR_BLACK, COLOR_LIGHTCYAN) +#define ATTR_BAR_SPINNER COLOR(COLOR_WHITE, COLOR_LIGHTCYAN) +#define ATTR_MENU_T COLOR(COLOR_BLACK, COLOR_LIGHTCYAN) +#define ATTR_MENU_K COLOR(COLOR_YELLOW, COLOR_BLACK) /* ---- global state ------------------------------------------------- */ -/* line_offset[] semantics depend on wrap_mode: +/* Files up to MAX_FILE = 128 KB are supported by spreading content + * across up to 8 EMM pages. Only one page is mapped into W3 at any + * given time (FILE_BUF); fb() swaps pages on demand and tracks the + * currently-mapped one in cur_page to avoid redundant OUTs. + * + * line_offset[] semantics depend on wrap_mode: * wrap_mode == 0 (truncate): one entry per logical line, value = byte - * offset inside FILE_BUF. - * wrap_mode == 1 (wrap): one entry per VISIBLE viewport row. Low - * 14 bits = byte offset of segment start; - * bit 15 (OFF_CONT) marks a continuation of - * the previous logical line — rendered as - * plain text without markdown classification. - * MAX_FILE = 16 KB so offsets fit in 14 bits. + * offset inside the logical file. + * wrap_mode == 1 (wrap): one entry per VISIBLE viewport row. The + * continuation flag for wrap segments lives + * in the separate cont_flag[] bitmap. */ -#define OFF_MASK 0x3FFFu -#define OFF_CONT 0x8000u /* NOTE: explicit `= 0` initialisers are MANDATORY on SDCC z80. * Uninitialised static storage (`static T x;`) doesn't actually allocate * space — multiple such declarations end up at the SAME address and * silently overwrite each other. See memory/sdcc_static_storage_gotcha. */ -static uint16_t line_offset[MAX_LINES] = {0}; +uint32_t line_offset[MAX_LINES]; // = {0}; +/* Bitmap: seg i is a continuation of the previous logical line — rendered + * as plain text without markdown classification. */ +uint8_t cont_flag[MAX_LINES / 8]; // = {0}; /* Bitmap: seg i (non-CONT) is inside a fenced code block (between two * ``` lines). The fence delimiter lines themselves are NOT marked here — * they are detected dynamically via a 3-byte prefix check. */ -static uint8_t in_code[MAX_LINES / 8] = {0}; +uint8_t in_code[MAX_LINES / 8]; // = {0}; + +uint8_t init_style[MAX_LINES / 4]; // = {0}; + static uint16_t n_lines = 0; static uint16_t top_line = 0; -static uint16_t file_size = 0; +static uint32_t file_size = 0; static uint8_t file_blk = 0; +static uint8_t cur_page = 0xFF; /* 0xFF = no page mapped yet */ static uint8_t wrap_mode = 1; /* default: wrap on */ static uint8_t viewport_x = 0; /* horizontal pan (truncate mode only) */ static char filename[64] = {0}; #define HPAN_STEP 8u +/* Spinner on the title bar (col=8, row=0): a small animated glyph + * shown while load_file / index_lines run, so the user knows the + * viewer is busy and not frozen. */ +#define SPINNER_COL 8 +static const char spinner_chars[4] = { '|', '/', '-', '\\' }; +static uint8_t spinner_phase = 0; +static uint8_t spinner_active = 0; + /* ================================================================== * tiny helpers * ================================================================== */ @@ -115,6 +175,97 @@ static void fill_row(uint8_t y, uint8_t attr) } } +/* Advance the spinner by one frame. No-op when inactive — safe to call + * unconditionally from any loop that wants to show "still working". */ +static void spinner_tick(void) +{ + if (!spinner_active) return; + wrchar(SPINNER_COL, 0, spinner_chars[spinner_phase & 3], ATTR_BAR_SPINNER); + spinner_phase++; +} + +/* Enable / disable the spinner. Disabling also wipes the glyph from the + * title bar so the slot looks like a plain space. */ +static void spinner_show(uint8_t on) +{ + spinner_active = on; + if (!on) wrchar(SPINNER_COL, 0, ' ', ATTR_BAR); +} + +/* Map the EMM page that holds file offset >= page * PAGE_SIZE into W3. + * No-op if already mapped. */ +static void map_page(uint8_t page) +{ + if (page != cur_page) { + sprinter_page_w3(mem_get_page(file_blk, page)); + cur_page = page; + } +} + +/* Read one byte from the logical file at byte offset `p`. Swaps the W3 + * mapping if `p` falls into a different page than the one currently + * mapped. Bounds checking is the caller's job. */ +static char fb(uint32_t p) +{ + map_page((uint8_t)(p >> PAGE_BITS)); + return FILE_BUF[(uint16_t)(p & PAGE_MASK)]; +} + +/* Treat space/tab/EOL/NUL as whitespace for emphasis flanking checks. */ +static uint8_t ws_or_eol(char c) +{ + return (uint8_t)(c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == 0); +} + +static uint8_t ws_or_eol_or_delim(char c) +{ + return (uint8_t)(c == ' ' || c == ',' || c == '.' || c == '!' || c == '?' || c == ':' || c == ';' || c == '/' + || c == '\t' || c == '\n' || c == '\r' || c == 0); +} + +/* Inline emphasis marker (asterisk, double-asterisk, underscore) is + * recognised only when ONE side is whitespace/EOL and the other is not + * (XOR). This treats "2 * 3" and "2 ** 3" (whitespace both sides) as + * literal, as well as intraword cases like COLOR_YELLOW (no whitespace + * either side), while keeping *italic*, **bold**, _under_ working. */ +static uint8_t is_emph_flanked(char prev_ch, char next_ch) +{ + return (uint8_t)(ws_or_eol(prev_ch) != ws_or_eol_or_delim(next_ch)); +} + +/* Continuation-flag bitmap helpers (see cont_flag[] above). */ +static uint8_t is_cont(uint16_t idx) +{ + return (uint8_t)((cont_flag[idx >> 3] >> (idx & 7)) & 1u); +} + +static void set_cont(uint16_t idx) +{ + cont_flag[idx >> 3] |= (uint8_t)(1u << (idx & 7)); +} + +static void set_pallete(void) { + uint8_t buff[64]; + for(int i = 0; i < 16; i++) { + for(int j = 0; j < 16; j++) { + buff[j * 4] = md_pallete[i * 4]; + buff[j * 4 + 1] = md_pallete[i * 4 + 1]; + buff[j * 4 + 2] = md_pallete[i * 4 + 2]; + buff[j * 4 + 3] = md_pallete[i * 4 + 3]; + // buff[j * 4] = i * 16; + // buff[j * 4 + 1] = i * 16; + // buff[j * 4 + 2] = i * 16; + // buff[j * 4 + 3] = 0; + } + + text_pal_load(TEXT_PAL_INK, i * 16, 16, md_pallete); + text_pal_load(TEXT_PAL_BLINK_INK, i * 16, 16, md_pallete); + text_pal_load(TEXT_PAL_PAPER, i * 16, 16, buff); + text_pal_load(TEXT_PAL_BLINK_PAPER, i * 16, 8, buff + 32); + text_pal_load(TEXT_PAL_BLINK_PAPER, i * 16 + 8, 8, buff + 32); + } +} + /* ================================================================== * file loading * ================================================================== */ @@ -125,26 +276,42 @@ static int load_file(const char *path) if (fd < 0) return -1; long sz = lseek(fd, 0, SEEK_END); - if (sz < 0 || sz > MAX_FILE) { + if (sz < 0 || (uint32_t)sz > MAX_FILE) { close(fd); return -2; } - file_size = (uint16_t)sz; + file_size = (uint32_t)sz; lseek(fd, 0, SEEK_SET); - file_blk = mem_alloc_pages(1); + /* Allocate exactly the number of EMM pages we need (1..MAX_PAGES). */ + uint8_t pages_needed = (uint8_t)((file_size + PAGE_SIZE - 1u) / PAGE_SIZE); + if (pages_needed == 0) pages_needed = 1; + file_blk = mem_alloc_pages(pages_needed); if (file_blk == 0) { close(fd); return -3; } - uint8_t phys = mem_get_page(file_blk, 0); - sprinter_page_w3(phys); - int n = read(fd, FILE_BUF, file_size); - close(fd); - if (n != (int)file_size) { - return -4; + /* Read the file 16 KB at a time, mapping each page into W3 in turn. + * `remaining` decreases by exactly PAGE_SIZE on each non-final iter; + * the final iter reads whatever is left. */ + uint32_t remaining = file_size; + for (uint8_t page = 0; page < pages_needed; page++) { + spinner_tick(); + uint8_t phys = mem_get_page(file_blk, page); + sprinter_page_w3(phys); + cur_page = page; + uint16_t chunk = (remaining > PAGE_SIZE) + ? (uint16_t)PAGE_SIZE + : (uint16_t)remaining; + int n = read(fd, FILE_BUF, chunk); + if (n != (int)chunk) { + close(fd); + return -4; + } + remaining -= chunk; } + close(fd); return 0; } @@ -166,13 +333,12 @@ static void unload_file(void) static uint8_t is_fence_delim(uint16_t idx) { if (idx >= n_lines) return 0; - uint16_t raw = line_offset[idx]; - if (raw & OFF_CONT) return 0; - uint16_t off = raw & OFF_MASK; - if ((uint32_t)off + 2 >= (uint32_t)file_size) return 0; - return (uint8_t)(FILE_BUF[off] == '`' && - FILE_BUF[off+1] == '`' && - FILE_BUF[off+2] == '`'); + if (is_cont(idx)) return 0; + uint32_t off = line_offset[idx]; + if (off + 2 >= file_size) return 0; + return (uint8_t)(fb(off) == '`' && + fb(off + 1) == '`' && + fb(off + 2) == '`'); } static uint8_t is_code_body(uint16_t idx) @@ -181,26 +347,52 @@ static uint8_t is_code_body(uint16_t idx) return (uint8_t)((in_code[idx >> 3] >> (idx & 7)) & 1u); } +#define INIT_STYLE_PLAIN 0x0 +#define INIT_STYLE_BOLD 0x1 +#define INIT_STYLE_ITALIC 0x2 +#define INIT_STYLE_UNDER 0x3 + +static const uint8_t styles_map[] = { + ATTR_TEXT, ATTR_TEXT_BOLD, ATTR_TEXT_ITALIC, ATTR_TEXT_UNDERSORE +}; + +static uint8_t get_init_style(uint16_t idx) +{ + if (idx >= n_lines) return styles_map[INIT_STYLE_PLAIN]; + return styles_map[(uint8_t)((init_style[idx >> 2] >> ((idx & 3) << 1)) & 3u)]; +} + +static uint8_t get_init_style_raw(uint16_t idx) +{ + if (idx >= n_lines) return INIT_STYLE_PLAIN; + return (uint8_t)((init_style[idx >> 2] >> ((idx & 3) << 1)) & 3u); +} + +static void set_init_style_raw(uint16_t idx, uint8_t style) +{ + init_style[idx >> 2] = (init_style[idx >> 2] & ~(3u << ((idx & 3) << 1))) | ((style & 3u) << ((idx & 3) << 1)); +} + /* Returns 1 iff the logical line at file offset `p_start` should NOT be * wrapped — it must render on a single seg even if longer than SCREEN_W. * Currently covers fence delimiters and horizontal rules; tables will be * added when Phase 4 tables / Phase 6 table layout lands. */ -static uint8_t is_nowrap_line(uint16_t p_start) +static uint8_t is_nowrap_line(uint32_t p_start) { - if ((uint32_t)p_start + 2 < (uint32_t)file_size && - FILE_BUF[p_start] == '`' && - FILE_BUF[p_start + 1] == '`' && - FILE_BUF[p_start + 2] == '`') { + if (p_start + 2 < file_size && + fb(p_start) == '`' && + fb(p_start + 1) == '`' && + fb(p_start + 2) == '`') { return 1; } - char c0 = FILE_BUF[p_start]; + char c0 = fb(p_start); if (c0 == '-' || c0 == '*' || c0 == '_') { char marker = 0; uint8_t count = 0; uint8_t ok = 1; - uint16_t p = p_start; + uint32_t p = p_start; while (p < file_size) { - char ch = FILE_BUF[p++]; + char ch = fb(p++); if (ch == '\n' || ch == '\r') break; if (ch == ' ' || ch == '\t') continue; if (ch != '-' && ch != '*' && ch != '_') { ok = 0; break; } @@ -236,24 +428,27 @@ static uint8_t is_nowrap_line(uint16_t p_start) * For headers/lists/quotes, *out_content is updated to the offset of the * first content byte (past the marker and its trailing space). */ -static uint8_t classify_line(uint16_t p_start, uint16_t *out_content) +static uint8_t classify_line(uint32_t p_start, uint32_t *out_content) { if (p_start >= file_size) return LK_PLAIN; - char c0 = FILE_BUF[p_start]; + char c0 = fb(p_start); /* Header? */ if (c0 == '#') { uint8_t lvl = 0; - uint16_t p = p_start; - while (p < file_size && FILE_BUF[p] == '#' && lvl < 6) { + uint32_t p = p_start; + while (p < file_size && fb(p) == '#' && lvl < 6) { lvl++; p++; } - if (p < file_size && (FILE_BUF[p] == ' ' || FILE_BUF[p] == '\t')) { - p++; - *out_content = p; - if (lvl > 4) lvl = 4; - return lvl; /* LK_H1 .. LK_H4 */ + if (p < file_size) { + char ch = fb(p); + if (ch == ' ' || ch == '\t') { + p++; + *out_content = p; + if (lvl > 4) lvl = 4; + return lvl; /* LK_H1 .. LK_H4 */ + } } /* Bare '#' with no space → not a header, fall through. */ } @@ -263,9 +458,9 @@ static uint8_t classify_line(uint16_t p_start, uint16_t *out_content) char marker = 0; uint8_t count = 0; uint8_t ok = 1; - uint16_t p = p_start; + uint32_t p = p_start; while (p < file_size) { - char ch = FILE_BUF[p++]; + char ch = fb(p++); if (ch == '\n' || ch == '\r') break; if (ch == ' ' || ch == '\t') continue; if (ch != '-' && ch != '*' && ch != '_') { ok = 0; break; } @@ -279,33 +474,40 @@ static uint8_t classify_line(uint16_t p_start, uint16_t *out_content) /* For lists and blockquotes we additionally allow leading spaces * (light nested-list support). HR / header / fence delim stay * strict col-0 — done above with `c0`. */ - uint16_t lp = p_start; - while (lp < file_size && FILE_BUF[lp] == ' ') lp++; - char cl = (lp < file_size) ? FILE_BUF[lp] : 0; + uint32_t lp = p_start; + while (lp < file_size && fb(lp) == ' ') lp++; + char cl = (lp < file_size) ? fb(lp) : 0; /* Unordered list? */ if ((cl == '-' || cl == '*' || cl == '+') && (lp + 1) < file_size && - FILE_BUF[lp + 1] == ' ') { - *out_content = (uint16_t)(lp + 2); + fb(lp + 1) == ' ') { + *out_content = lp + 2; return LK_ULIST; } /* Ordered list? */ if (cl >= '0' && cl <= '9') { - uint16_t p = lp; - while (p < file_size && FILE_BUF[p] >= '0' && FILE_BUF[p] <= '9') p++; - if (p < file_size && (FILE_BUF[p] == '.' || FILE_BUF[p] == ')') && - (p + 1) < file_size && FILE_BUF[p + 1] == ' ') { - *out_content = (uint16_t)(p + 2); - return LK_OLIST; + uint32_t p = lp; + while (p < file_size) { + char ch = fb(p); + if (ch < '0' || ch > '9') break; + p++; + } + if (p < file_size) { + char ch = fb(p); + if ((ch == '.' || ch == ')') && + (p + 1) < file_size && fb(p + 1) == ' ') { + *out_content = p + 2; + return LK_OLIST; + } } } /* Blockquote? */ if (cl == '>') { - uint16_t p = (uint16_t)(lp + 1); - if (p < file_size && FILE_BUF[p] == ' ') p++; + uint32_t p = lp + 1; + if (p < file_size && fb(p) == ' ') p++; *out_content = p; return LK_QUOTE; } @@ -317,7 +519,7 @@ static uint8_t classify_line(uint16_t p_start, uint16_t *out_content) * any leading indent for nested lists/quotes (the render side keeps the * 1 byte = 1 visible col invariant for those, so content_off - p_start * is the visible width). */ -static uint8_t marker_visible_col(uint8_t kind, uint16_t p_start, uint16_t content_off) +static uint8_t marker_visible_col(uint8_t kind, uint32_t p_start, uint32_t content_off) { switch (kind) { case LK_ULIST: @@ -329,36 +531,43 @@ static uint8_t marker_visible_col(uint8_t kind, uint16_t p_start, uint16_t conte static void index_lines(void) { + uint8_t line_style = INIT_STYLE_PLAIN; n_lines = 0; memset(in_code, 0, sizeof(in_code)); + memset(cont_flag, 0, sizeof(cont_flag)); + memset(init_style, 0, sizeof(init_style)); if (file_size == 0) return; if (!wrap_mode) { /* Truncate: one entry per logical line. */ line_offset[n_lines++] = 0; - for (uint16_t p = 0; p < file_size; p++) { - if (FILE_BUF[p] == '\n' && + for (uint32_t p = 0; p < file_size; p++) { + if (fb(p) == '\n' && p + 1 < file_size && n_lines < MAX_LINES) { - line_offset[n_lines++] = (uint16_t)(p + 1); + line_offset[n_lines++] = p + 1; + if ((n_lines & 15) == 0) spinner_tick(); } } } else { /* Wrap: walk logical lines, split each into one or more segs. */ - uint16_t p = 0; + uint32_t p = 0; + line_style = INIT_STYLE_PLAIN; while (p < file_size && n_lines < MAX_LINES) { + set_init_style_raw(n_lines, line_style); + if ((n_lines & 15) == 0) spinner_tick(); /* Find end of logical line (\n or EOF). */ - uint16_t line_end = p; - while (line_end < file_size && FILE_BUF[line_end] != '\n') line_end++; + uint32_t line_end = p; + while (line_end < file_size && fb(line_end) != '\n') line_end++; /* Emit the first seg of this logical line (no CONT). */ line_offset[n_lines++] = p; if (!is_nowrap_line(p)) { /* Determine marker prefix and starting visible column. */ - uint16_t content_off = p; + uint32_t content_off = p; uint8_t kind = classify_line(p, &content_off); uint8_t col = marker_visible_col(kind, p, content_off); - uint16_t q; + uint32_t q; if (kind == LK_H1 || kind == LK_H2 || kind == LK_H3 || kind == LK_H4 || kind == LK_ULIST || kind == LK_OLIST || kind == LK_QUOTE) { q = content_off; @@ -366,38 +575,84 @@ static void index_lines(void) q = p; } - uint16_t last_space = 0xFFFFu; /* byte pos AFTER last space */ + uint32_t last_space = 0xFFFFFFFFu; /* byte pos AFTER last space */ + char prev_ch = ' '; /* for emphasis flanking */ while (q < line_end && n_lines < MAX_LINES) { - char ch = FILE_BUF[q]; - /* Inline emphasis markers are zero-width. */ - if (ch == '*' || ch == '_' || ch == '`') { + char ch = fb(q); + /* Backtick — always zero-width (inline code delimiter). */ + if (ch == '`') { q++; continue; } - if (ch == '\t') { + /* `**` — zero-width only when XOR-flanked; otherwise + * both stars render as two literal columns. */ + if (ch == '*' && (q + 1) < line_end && fb(q + 1) == '*') { + char next_ch = ((q + 2) < line_end) ? fb(q + 2) : '\n'; + prev_ch = fb(q - 1); + if (is_emph_flanked(prev_ch, next_ch)) { + if(line_style == INIT_STYLE_PLAIN && ws_or_eol(prev_ch)) { + line_style = INIT_STYLE_BOLD; + q += 2; + continue; + } else if(line_style == INIT_STYLE_BOLD && ws_or_eol_or_delim(next_ch)) { + line_style = INIT_STYLE_PLAIN; + q += 2; + continue; + } + } + col += 2; + q += 2; + prev_ch = '*'; + } + /* Single `*` / `_` — zero-width only when XOR-flanked. */ + else if (ch == '*' || ch == '_') { + prev_ch = fb(q - 1); + char next_ch = ((q + 1) < line_end) ? fb(q + 1) : '\n'; + if (is_emph_flanked(prev_ch, next_ch)) { + uint8_t new_style = (ch == '*') ? INIT_STYLE_ITALIC : INIT_STYLE_UNDER; + if (line_style == INIT_STYLE_PLAIN && ws_or_eol(prev_ch)) { + line_style = new_style; + q++; + continue; + } else if (line_style == new_style && ws_or_eol_or_delim(next_ch)) { + line_style = INIT_STYLE_PLAIN; + q++; + continue; + } + } + col++; + q++; + prev_ch = ch; + } + else if (ch == '\t') { uint8_t tgt = (uint8_t)((col & (uint8_t)~(TAB_STOP - 1)) + TAB_STOP); if (tgt > SCREEN_W) tgt = SCREEN_W; col = tgt; q++; + prev_ch = ' '; } else { - if (ch == ' ') last_space = (uint16_t)(q + 1); + if (ch == ' ') last_space = q + 1; col++; q++; + prev_ch = ch; } if (col >= SCREEN_W && q < line_end) { - uint16_t wrap_at = (last_space != 0xFFFFu && - last_space > (line_offset[n_lines - 1] & OFF_MASK)) + uint32_t wrap_at = (last_space != 0xFFFFFFFFu && + last_space > line_offset[n_lines - 1]) ? last_space : q; if (wrap_at >= line_end) break; - line_offset[n_lines++] = (uint16_t)(wrap_at | OFF_CONT); + line_offset[n_lines] = wrap_at; + set_cont(n_lines); + n_lines++; col = 0; - last_space = 0xFFFFu; + last_space = 0xFFFFFFFFu; + prev_ch = ' '; q = wrap_at; } } } - p = (line_end < file_size) ? (uint16_t)(line_end + 1) : line_end; + p = (line_end < file_size) ? (line_end + 1) : line_end; } } @@ -405,14 +660,25 @@ static void index_lines(void) { uint8_t in_block = 0; for (uint16_t i = 0; i < n_lines; i++) { - if (line_offset[i] & OFF_CONT) continue; + if (is_cont(i)) { + if(is_code_body(i - 1)) { + in_code[i >> 3] |= (uint8_t)(1u << (i & 7)); + set_init_style_raw(i, INIT_STYLE_PLAIN); + } + continue; + } else { + + } if (is_fence_delim(i)) { in_block = (uint8_t)!in_block; + set_init_style_raw(i, INIT_STYLE_PLAIN); } else if (in_block) { in_code[i >> 3] |= (uint8_t)(1u << (i & 7)); + set_init_style_raw(i, INIT_STYLE_PLAIN); } } } + } /* ================================================================== @@ -430,18 +696,30 @@ static void render_line(uint16_t line_idx, uint8_t row) { uint8_t col = 0; uint8_t base_attr = ATTR_TEXT; - uint8_t cur_attr = ATTR_TEXT; - uint8_t emph = EM_NONE; + uint8_t cur_attr = is_code_body(line_idx) ? ATTR_TEXT_CODE : get_init_style(line_idx); // ATTR_TEXT; + uint8_t emph = is_code_body(line_idx) ? EM_CODE : get_init_style_raw(line_idx); uint8_t parse_inline = 1; /* skip inline parsing for fenced code body */ + uint8_t x = wherex(); + uint8_t y = wherey(); + uint16_t from = 342; + if(line_idx >= from && line_idx <= from + 3) { + gotoxy(18 + (line_idx - from) * 9, 31); dec16(line_idx); + gotoxy(21 + (line_idx - from) * 9, 31); hex8(get_init_style_raw(line_idx)); + gotoxy(23 + (line_idx - from) * 9, 31); hex8(is_code_body(line_idx)); + } + gotoxy(61, 31); dec16(line_idx); + gotoxy(64, 31); hex8(get_init_style_raw(line_idx)); + gotoxy(66, 31); hex8(is_code_body(line_idx)); + gotoxy(x, y); + if (line_idx < n_lines) { - uint16_t raw = line_offset[line_idx]; - uint16_t p = raw & OFF_MASK; - uint16_t content_off = p; - uint8_t is_cont = (raw & OFF_CONT) ? 1 : 0; + uint32_t p = line_offset[line_idx]; + uint32_t content_off = p; + uint8_t cont = is_cont(line_idx); /* Right bound for this seg: start of next seg, or file end. */ - uint16_t seg_end = (line_idx + 1 < n_lines) - ? (uint16_t)(line_offset[line_idx + 1] & OFF_MASK) + uint32_t seg_end = (line_idx + 1 < n_lines) + ? line_offset[line_idx + 1] : file_size; if (seg_end > file_size) seg_end = file_size; uint8_t kind; @@ -451,21 +729,12 @@ static void render_line(uint16_t line_idx, uint8_t row) * classification, no marker prefixes, inline markers stay literal * (we drop them silently so the visible output matches the wrap * algorithm's column count). */ - if (is_cont) { - while (p < seg_end && col < SCREEN_W) { - char ch = FILE_BUF[p++]; - if (ch == '\n' || ch == '\r') break; - if (ch == '*' || ch == '_' || ch == '`') continue; - if (ch == '\t') { - uint8_t tgt = (uint8_t)((col & (uint8_t)~(TAB_STOP - 1)) + TAB_STOP); - if (tgt > SCREEN_W) tgt = SCREEN_W; - while (col < tgt) wrchar(col++, row, ' ', ATTR_TEXT); - } else { - wrchar(col++, row, ch, ATTR_TEXT); - } - } - while (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT); - return; + if (cont) { + // TODO - + // Для cont можно сделать такое - + // 1) если это продолжение quote - то отрисовать префикc QUOTE и дальше продолжить вывод + // 2) если это продолжение пункта list - то посмотреть какой отступ был, повторить его же и продолжить вывод + // 3) для обычного текста - продолжить вывод } /* Fenced code block: delimiter line → blank row; body lines render @@ -500,8 +769,8 @@ static void render_line(uint16_t line_idx, uint8_t row) /* Light nested-list support: any leading spaces before '-' / '*' * / '+' render as plain spaces (preserves nesting visually); * then a bullet + a content space. */ - uint16_t q = p; - while (q + 2 < content_off && col < SCREEN_W && FILE_BUF[q] == ' ') { + uint32_t q = p; + while (q + 2 < content_off && col < SCREEN_W && fb(q) == ' ') { wrchar(col++, row, ' ', ATTR_TEXT); q++; } @@ -511,21 +780,22 @@ static void render_line(uint16_t line_idx, uint8_t row) } else if (kind == LK_OLIST) { /* Indent (if any) → spaces; digits and '.' or ')' in marker * colour; trailing space in plain. */ - uint16_t q = p; - while (q < content_off && col < SCREEN_W && FILE_BUF[q] == ' ') { + uint32_t q = p; + while (q < content_off && col < SCREEN_W && fb(q) == ' ') { wrchar(col++, row, ' ', ATTR_TEXT); q++; } while (q < content_off - 1 && col < SCREEN_W) { - wrchar(col++, row, FILE_BUF[q++], ATTR_LIST_MARKER); + wrchar(col++, row, fb(q), ATTR_LIST_MARKER); + q++; } if (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT); p = content_off; } else if (kind == LK_QUOTE) { /* Indent (if any) → spaces; '│' (0xB3) prefix in marker * colour, then a space, then content. */ - uint16_t q = p; - while (q + 1 < content_off && col < SCREEN_W && FILE_BUF[q] == ' ') { + uint32_t q = p; + while (q + 1 < content_off && col < SCREEN_W && fb(q) == ' ') { wrchar(col++, row, ' ', ATTR_TEXT); q++; } @@ -540,47 +810,58 @@ static void render_line(uint16_t line_idx, uint8_t row) * so the user can pan past the marker. The marker itself was * already rendered above at fixed screen cols 0..(col-1). */ uint8_t cc = 0; + char prev_ch = ' '; /* for `_` flanking check */ while (p < seg_end && col < SCREEN_W) { - char ch = FILE_BUF[p]; + char ch = fb(p); if (ch == '\n' || ch == '\r') break; /* --- inline emphasis markers (consumed, not rendered) --- - * Only one style active at a time. A marker that would conflict - * with the currently active style is rendered as a literal char. - * Skipped entirely inside fenced code blocks (parse_inline=0). - */ + * Asterisk / double-asterisk / underscore are markers only + * when XOR-flanked (whitespace on exactly one side). Keeps + * COLOR_YELLOW, intraword slashes, "2 * 3", "2 ** 3" etc. + * as literal text. A marker that would conflict with the + * currently-active style is still consumed (zero-width) + * so the index_lines column count stays in sync with the + * rendered output. Skipped entirely inside fenced code + * blocks (parse_inline=0). */ if (parse_inline) { - if (ch == '*' && (p + 1) < file_size && FILE_BUF[p + 1] == '*') { - if (emph == EM_NONE) { - emph = EM_BOLD; cur_attr = ATTR_TEXT_BOLD; - p += 2; continue; + /* `**` (bold) */ + if (ch == '*' && (p + 1) < seg_end && fb(p + 1) == '*') { + char next_ch = ((p + 2) < seg_end) ? fb(p + 2) : '\n'; + if (is_emph_flanked(prev_ch, next_ch)) { + if (emph == EM_NONE && ws_or_eol(prev_ch)) { + emph = EM_BOLD; cur_attr = ATTR_TEXT_BOLD; + p += 2; continue; + } else if (emph == EM_BOLD && ws_or_eol_or_delim(next_ch)) { + emph = EM_NONE; cur_attr = base_attr; + p += 2; continue; + } } - if (emph == EM_BOLD) { - emph = EM_NONE; cur_attr = base_attr; - p += 2; continue; - } - /* conflict (italic/under active) — fall through, treat - * the first '*' as literal. */ + /* literal `**` — render BOTH stars, do NOT fall + * through to single-`*` handler. */ + p += 2; + if (cc >= viewport_x && col < SCREEN_W) wrchar(col++, row, '*', cur_attr); + cc++; + if (cc >= viewport_x && col < SCREEN_W) wrchar(col++, row, '*', cur_attr); + cc++; + prev_ch = '*'; + continue; } - if (ch == '*') { - if (emph == EM_NONE) { - emph = EM_ITALIC; cur_attr = ATTR_TEXT_ITALIC; - p++; continue; - } - if (emph == EM_ITALIC) { - emph = EM_NONE; cur_attr = base_attr; - p++; continue; - } - } - if (ch == '_') { - if (emph == EM_NONE) { - emph = EM_UNDER; cur_attr = ATTR_TEXT_UNDERSORE; - p++; continue; - } - if (emph == EM_UNDER) { - emph = EM_NONE; cur_attr = base_attr; - p++; continue; + /* `*` (italic) and `_` (underline) — same flanking rule */ + if (ch == '*' || ch == '_') { + char next_ch = ((p + 1) < seg_end) ? fb(p + 1) : '\n'; + if (is_emph_flanked(prev_ch, next_ch)) { + uint8_t active = (ch == '*') ? EM_ITALIC : EM_UNDER; + uint8_t active_attr = (ch == '*') ? ATTR_TEXT_ITALIC : ATTR_TEXT_UNDERSORE; + if (emph == EM_NONE && ws_or_eol(prev_ch)) { + emph = active; cur_attr = active_attr; + p++; continue; + } else if (emph != EM_NONE && ws_or_eol_or_delim(next_ch)) { + emph = EM_NONE; cur_attr = base_attr; + p++; continue; + } } + /* intraword / both-flanked — fall through, literal */ } if (ch == '`') { if (emph == EM_NONE) { @@ -603,11 +884,13 @@ static void render_line(uint16_t line_idx, uint8_t row) if (cc >= viewport_x) wrchar(col++, row, ' ', cur_attr); cc++; } + prev_ch = ' '; } else { if (cc >= viewport_x && col < SCREEN_W) { wrchar(col++, row, ch, cur_attr); } cc++; + prev_ch = ch; } } @@ -617,11 +900,22 @@ static void render_line(uint16_t line_idx, uint8_t row) * still real visible content before the end of the line, overlay * the last screen cell with a bright '>'. */ if (!wrap_mode && col >= SCREEN_W) { - uint16_t pp = p; + uint32_t pp = p; + uint32_t left_bound = line_offset[line_idx]; while (pp < seg_end) { - char c = FILE_BUF[pp]; + char c = fb(pp); if (c == '\n' || c == '\r') break; - if (c == '*' || c == '_' || c == '`') { pp++; continue; } + if (c == '`') { pp++; continue; } + if (c == '*' && (pp + 1) < seg_end && fb(pp + 1) == '*') { + char pc = (pp > left_bound) ? fb(pp - 1) : ' '; + char nc = (pp + 2 < seg_end) ? fb(pp + 2) : '\n'; + if (is_emph_flanked(pc, nc)) { pp += 2; continue; } + /* literal `**` — there IS visible content */ + } else if (c == '*' || c == '_') { + char pc = (pp > left_bound) ? fb(pp - 1) : ' '; + char nc = (pp + 1 < seg_end) ? fb(pp + 1) : '\n'; + if (is_emph_flanked(pc, nc)) { pp++; continue; } + } wrchar(SCREEN_W - 1, row, '>', ATTR_TRUNC); break; } @@ -661,7 +955,9 @@ static void render_full_status(void) fill_row(0, ATTR_BAR); gotoxy(1, 0); - printf("MDVIEW %s", filename); + /* "MDVIEW", then col 7 = space, col 8 = spinner slot, col 9 = space, + * col 10+ = filename. Spinner_tick draws over col 8 in ATTR_BAR. */ + printf("MDVIEW %s", filename); gotoxy(45, 0); printf("\xB3 L %5u-%u / %u", (top_line + 1), last, n_lines); gotoxy(70, 0); @@ -756,16 +1052,18 @@ static void scroll_h(int8_t delta) * the rebuild. Also resets horizontal pan — wrap mode doesn't use it. */ static void toggle_wrap(void) { - uint16_t top_off = (top_line < n_lines) - ? (uint16_t)(line_offset[top_line] & OFF_MASK) : 0; + uint32_t top_off = (top_line < n_lines) ? line_offset[top_line] : 0; wrap_mode = wrap_mode ? 0u : 1u; viewport_x = 0; + spinner_show(1); + spinner_tick(); index_lines(); + spinner_show(0); /* Largest i such that the i-th seg's offset is ≤ top_off. */ uint16_t i = 0; while ((uint16_t)(i + 1) < n_lines && - (uint16_t)(line_offset[i + 1] & OFF_MASK) <= top_off) { + line_offset[i + 1] <= top_off) { i++; } top_line = i; @@ -832,6 +1130,11 @@ static void die(const char *msg) int main(int argc, char **argv) { + memset(line_offset, 0, sizeof(line_offset)); + memset(cont_flag, 0, sizeof(cont_flag)); + memset(in_code, 0, sizeof(in_code)); + memset(init_style, 0, sizeof(init_style)); + const char *path; if (argc >= 2) path = argv[1]; else path = "SAMPLE.MD"; @@ -846,14 +1149,25 @@ int main(int argc, char **argv) filename[i] = 0; } + /* Bring the UI up FIRST so the user sees the menu + title bar (with + * a spinner) immediately, instead of a black screen while we read + * the file and index lines. */ + set_videotextmode(TEXT_MODE_80x32); + clrscr_attr(ATTR_TEXT); + set_pallete(); + render_menu(); + render_full_status(); + spinner_show(1); + int rc = load_file(path); if (rc < 0) { + spinner_show(0); clrscr_attr(ATTR_TEXT); puts("mdview: cannot load file"); puts(path); switch (rc) { case -1: puts("(open failed)"); break; - case -2: puts("(size > 16K or seek failed)"); break; + case -2: puts("(size > 128K or seek failed)"); break; case -3: puts("(mem_alloc_pages failed)"); break; case -4: puts("(short read)"); break; } @@ -862,15 +1176,13 @@ int main(int argc, char **argv) return 1; } index_lines(); + spinner_show(0); if (n_lines == 0) { die("mdview: empty file"); unload_file(); return 1; } - set_videotextmode(TEXT_MODE_80x32); - clrscr_attr(ATTR_TEXT); - render_menu(); render_full_status(); render_viewport(); @@ -893,10 +1205,12 @@ int main(int argc, char **argv) case KEY_RIGHT: scroll_h(+(int8_t)HPAN_STEP); break; case KEY_PGUP: scroll_up(VIEW_H); break; case KEY_PGDN: scroll_down(VIEW_H); break; - case KEY_HOME: top_line = 0; + case KEY_HOME: top_line = 0; + viewport_x = 0; render_viewport(); break; case KEY_END: top_line = (n_lines > VIEW_H) ? (uint16_t)(n_lines - VIEW_H) : 0; + viewport_x = 0; render_viewport(); break; default: continue; } @@ -904,6 +1218,7 @@ int main(int argc, char **argv) } exit_loop: unload_file(); + pal_reset(PAL_CGA); clrscr_attr(ATTR_TEXT); return 0; }