# План: текстовый 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*/fread/fgets - не использовать - Максимум 16 KB на один файл (одна EMM-страница); многостраничный режим - в v2 - Подсветка через цвет: **размер заголовка** > цвет шрифта; **bold/italic** > цвет фона (моноширинный фонт без жирного/курсивного начертания) --- ## Архитектура ### Раскладка экрана (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) ``` - 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-страницами: ├── file page: исходный текст .md файла (16 KB) └── cache page: кэш отформатированных строк (Phase 3+) ``` Почему small + W3: - 32 KB на код+данные с большим запасом > нет банкинга - W3 - стандартный paged window, под него у нас уже есть `bank_io_w3` API - v2-расширение (multi-page файлы >16 KB): просто `mem_alloc_pages(N)` и переключение страниц в W3 через `sprinter_page_w3()` Статики (в 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: один активный стиль одновременно (без вложенности); конфликтующий маркер при чужом активном стиле уходит литералом. Состояние сбрасывается на каждой строке. > **Кэш отформатированных строк** - отложен в самый конец, см. "Phase ?: оптимизации". > Скорости текущего наивного рендера хватает на 80x30 = 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" когда выключен). **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 #include #include #include #include #include #include #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 "); 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: многостраничное хранение через `mem_alloc_pages(N)`, ленивая загрузка страниц в W3 при доступе. - Search (Find / Find next) - F3 / F4. - Toggle highlight on/off - F2. - Word wrap. - Image alt-text rendering. - Tables.