/* * mdview2_enc.c — кодировки и конвертация UTF-8 → CP866. * * Две связанные подсистемы: * 1. 8-битные кодировки (CP866/CP1251/KOI8-R) — различаются только ремапом * старших байтов [128-255] на ОТРИСОВКЕ (g_remap, win_rest_remap). * Автоопределение — detect_encoding по частоте ходовых букв. * 2. UTF-8 — отдельный набор: исходник инкрементально декодируется в * utf_phys[] как чистый CP866 (utf_convert_more), после чего весь * конвейер индексации/кэша работает по нему без ремапа. * * Владеет состоянием кодека (conv-буфер, позиция конвертации, UTF-страницы); * описание разделяемых символов и API — в mdview2.h. Оркестрацией (привязка * UTF-буфера к живому file_*, сборка наборов) занимается build_doc() ядра. */ #include #include /* COLOR()/COLOR_* для ATTR_* в attr_is_content */ #include /* sprinter_page_w3 */ #include /* bank_read/bank_write/mem_alloc_pages/mem_get_page */ #include "mdview2.h" /* ---- Состояние кодировки/UTF (владелец — этот модуль) ------------- */ uint8_t g_encoding = ENC_CP866; /* активная кодировка */ uint8_t utf_blk = 0; uint8_t utf_pages = 0; uint8_t utf_phys[MAX_PAGES] = {0}; uint32_t utf_size = 0; uint8_t utf_avail = 0; /* 1 = страницы UTF-8 выделены */ uint8_t g_utf_building = 0; /* 1 пока конвертация не дошла до конца исходника */ /* Приватное состояние конвертера (наружу не торчит — сброс через * utf_conv_reset(), чтение оригинала через cv_read). */ static uint32_t utf_src = 0; /* позиция чтения ОРИГИНАЛА (utf-8) */ static uint8_t cv_page = 0xFF; /* orig-страница в W3 для cv_read (своя от cur_page) */ /* Таблицы ремапа СТАРШИХ байтов [128-255] в CP866. Индексируются (ch-0x80): * младшие 128 (ASCII) — identity, в ремапе не участвуют (win_rest_remap * трогает только ch>=0x80), поэтому в таблицах их нет. Кириллица + ходовая * пунктуация; неизвестное → '?' (0x3F). Сгенерированы Python codecs. */ static const uint8_t cp1251_to_866[128] = { /* 80 */ 0x3F, 0x3F, 0x27, 0x3F, 0x22, 0x2E, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3C, 0x3F, 0x3F, 0x3F, 0x3F, /* 90 */ 0x3F, 0x27, 0x27, 0x22, 0x22, 0x07, 0x2D, 0x2D, 0x3F, 0x3F, 0x3F, 0x3E, 0x3F, 0x3F, 0x3F, 0x3F, /* A0 */ 0xFF, 0xF6, 0xF7, 0x3F, 0xFD, 0x3F, 0x3F, 0x3F, 0xF0, 0x63, 0xF2, 0x3C, 0x3F, 0x3F, 0x72, 0xF4, /* B0 */ 0xF8, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0xFA, 0xF1, 0xFC, 0xF3, 0x3E, 0x3F, 0x3F, 0x3F, 0xF5, /* C0 */ 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F, /* D0 */ 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F, /* E0 */ 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, /* F0 */ 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, }; static const uint8_t koi8r_to_866[128] = { /* 80 */ 0xC4, 0xB3, 0xDA, 0xBF, 0xC0, 0xD9, 0xC3, 0xB4, 0xC2, 0xC1, 0xC5, 0xDF, 0xDC, 0xDB, 0xDD, 0xDE, /* 90 */ 0xB0, 0xB1, 0xB2, 0x3F, 0xFE, 0xF9, 0xFB, 0x3F, 0x3F, 0x3F, 0xFF, 0x3F, 0xF8, 0x3F, 0xFA, 0x3F, /* A0 */ 0xCD, 0xBA, 0xD5, 0xF1, 0xD6, 0xC9, 0xB8, 0xB7, 0xBB, 0xD4, 0xD3, 0xC8, 0xBE, 0xBD, 0xBC, 0xC6, /* B0 */ 0xC7, 0xCC, 0xB5, 0xF0, 0xB6, 0xB9, 0xD1, 0xD2, 0xCB, 0xCF, 0xD0, 0xCA, 0xD8, 0xD7, 0xCE, 0x63, /* C0 */ 0xEE, 0xA0, 0xA1, 0xE6, 0xA4, 0xA5, 0xE4, 0xA3, 0xE5, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, /* D0 */ 0xAF, 0xEF, 0xE0, 0xE1, 0xE2, 0xE3, 0xA6, 0xA2, 0xEC, 0xEB, 0xA7, 0xE8, 0xED, 0xE9, 0xE7, 0xEA, /* E0 */ 0x9E, 0x80, 0x81, 0x96, 0x84, 0x85, 0x94, 0x83, 0x95, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, /* F0 */ 0x8F, 0x9F, 0x90, 0x91, 0x92, 0x93, 0x86, 0x82, 0x9C, 0x9B, 0x87, 0x98, 0x9D, 0x99, 0x97, 0x9A, }; /* Устанавливает активную кодировку и таблицу ремапа (для 8-битных). */ void set_encoding(uint8_t enc) { g_encoding = enc; g_remap = (enc == ENC_CP1251) ? cp1251_to_866 : (enc == ENC_KOI8R) ? koi8r_to_866 : (const uint8_t *)0; } /* Самые ходовые строчные русские буквы (о е а и н т с р в л) — их байты * различают 8-битные кодировки по частоте. */ static const uint8_t common866 [10] = {0xAE,0xA5,0xA0,0xA8,0xAD,0xE2,0xE1,0xE0,0xA2,0xAB}; static const uint8_t common1251[10] = {0xEE,0xE5,0xE0,0xE8,0xED,0xF2,0xF1,0xF0,0xE2,0xEB}; static const uint8_t commonkoi [10] = {0xCF,0xC5,0xC1,0xC9,0xCE,0xD4,0xD3,0xD2,0xD7,0xCC}; static uint8_t in_set10(const uint8_t *s, uint8_t b) { for (uint8_t i = 0; i < 10; i++) if (s[i] == b) return 1; return 0; } /* Автоопределение кодировки дешёвым сканом байтов (до построения индекса). * BOM → UTF8; иначе валидность UTF-8 (структура multibyte); иначе 8-бит по * частоте ходовых букв; фолбэк CP866. */ uint8_t detect_encoding(void) { if (file_size >= 3 && (uint8_t)fb(0) == 0xEF && (uint8_t)fb(1) == 0xBB && (uint8_t)fb(2) == 0xBF) return ENC_UTF8; uint32_t n = file_size; if (n > 4096u) n = 4096u; /* сэмпл: первые 4 КБ — детекции хватает */ uint8_t utf_ok = 1, has_mb = 0, has_high = 0, cont = 0; uint16_t s866 = 0, s1251 = 0, skoi = 0; for (uint32_t p = 0; p < n; p++) { uint8_t b = (uint8_t)fb(p); if (b < 0x80) { if (cont) { utf_ok = 0; cont = 0; } continue; } has_high = 1; if (in_set10(common866, b)) s866++; if (in_set10(common1251, b)) s1251++; if (in_set10(commonkoi, b)) skoi++; if (cont) { if ((b & 0xC0) == 0x80) cont--; else { utf_ok = 0; cont = 0; } } else if (b >= 0xC2 && b <= 0xDF) { cont = 1; has_mb = 1; } else if (b >= 0xE0 && b <= 0xEF) { cont = 2; has_mb = 1; } else if (b >= 0xF0 && b <= 0xF4) { cont = 3; has_mb = 1; } else utf_ok = 0; /* битый лид/одиночный континюэйшн */ } /* Незавершённая multibyte-последовательность на КОНЦЕ — нарушение только * если это настоящий EOF; на границе сэмпла (n= s866 && s1251 >= skoi) return ENC_CP1251; if (skoi >= s866) return ENC_KOI8R; return ENC_CP866; } const char *enc_name(uint8_t enc) { return (enc == ENC_CP1251) ? "CP1251" : (enc == ENC_KOI8R) ? "KOI8-R" : (enc == ENC_UTF8) ? "UTF-8 " : "CP866 "; } /* Контентный глиф (ремапим при смене кодировки) vs структурный (рамка/HR/ * маркеры — уже CP866, не трогаем). Различаем по attr. */ static uint8_t attr_is_content(uint8_t a) { return (uint8_t)(a != ATTR_BOX && a != ATTR_HR && a != ATTR_LIST_MARKER && a != ATTR_QUOTE_MARKER); } /* Вывод среза строки кэша с ремапом контентных глифов [128-255] через * g_remap (CP1251/KOI8). Без активной таблицы — прямой win_rest. */ void win_rest_remap(uint8_t row, uint8_t w, uint8_t page, uint16_t off) { if (!g_remap || w == 0) { win_rest(row, 0, 1, w, page, off); return; } uint8_t buf[SCREEN_W * 2]; bank_read(page, off, buf, (uint16_t)w * 2u); for (uint8_t i = 0; i < w; i++) { uint8_t ch = buf[(uint16_t)i * 2u]; if (ch >= 0x80 && attr_is_content(buf[(uint16_t)i * 2u + 1u])) buf[(uint16_t)i * 2u] = g_remap[ch - 0x80]; /* таблицы хранят только старшие 128 */ } bank_write(g_scratch_phys, 0, buf, (uint16_t)w * 2u); win_rest(row, 0, 1, w, g_scratch_phys, 0); } /* ========================= UTF-8 → CP866 (Фаза 2) ======================== */ /* Кириллица U+0400..U+045F → CP866. Русский набор + Ё/ё и часть украинских * (Є є Ї ї Ў ў), которые есть в CP866; пропуски → '?' (0x3F). */ static const uint8_t utf_cyr_to_866[96] = { /* 0400 */ 0x3F, 0xF0, 0x3F, 0x3F, 0xF2, 0x3F, 0x3F, 0xF4, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0xF6, 0x3F, /* 0410 */ 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F, /* 0420 */ 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F, /* 0430 */ 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, /* 0440 */ 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, /* 0450 */ 0x3F, 0xF1, 0x3F, 0x3F, 0xF3, 0x3F, 0x3F, 0xF5, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0x3F, 0xF7, 0x3F, }; /* Поток вывода конвертера: пишем в utf_phys чанками по странице, буфер в * near-памяти, флаш через один bank_write (не побайтовый swap W3). */ static uint8_t conv_buf[256]; static uint16_t conv_n = 0; /* байт в буфере, ещё не сброшено */ static uint8_t conv_page = 0; /* индекс текущей страницы в utf_phys */ static uint16_t conv_off = 0; /* уже сброшенное смещение в этой странице */ static void conv_flush(void) { if (conv_n) { bank_write(utf_phys[conv_page], conv_off, conv_buf, conv_n); conv_off = (uint16_t)(conv_off + conv_n); conv_n = 0; } } /* Добавляет один CP866-байт в выходной поток. Запись никогда не пересекает * границу страницы (256 делит 16384 нацело). */ static void conv_put(uint8_t b) { if ((uint16_t)(conv_off + conv_n) == PAGE_SIZE) { /* страница заполнена */ conv_flush(); conv_page++; conv_off = 0; } conv_buf[conv_n++] = b; if (conv_n == (uint16_t)sizeof(conv_buf)) conv_flush(); } /* Подстановки одиночных не-кириллических символов UTF-8 → один CP866-байт. * Таблица вместо switch (экономия кода); линейный поиск дёшев — вызывается * лишь при конвертации на редких символьных кодпойнтах. Чтобы добавить новый * символ — достаточно дописать одну строку {кодпойнт, байт-CP866}. * (Символы с НЕ-1:1 заменой, напр. … -> "...", обрабатываются отдельно ниже.) */ typedef struct { uint16_t utf8; uint8_t cp866; } utf_sym_t; static const utf_sym_t utf_sym[] = { {0x00A0, 0x20}, {0x00AB, 0x3C}, {0x00BB, 0x3E}, /* nbsp « » → space < > */ {0x00B0, 0xF8}, {0x00B7, 0xFA}, /* ° · */ {0x2013, 0x2D}, {0x2014, 0x2D}, /* – — → '-' */ {0x2018, 0x27}, {0x2019, 0x27}, /* ‘ ’ → '\'' */ {0x201C, 0x22}, {0x201D, 0x22}, /* “ ” → '"' */ {0x2022, 0xF9}, {0x2116, 0xFC}, /* • → ∙ № */ {0x2190, 0x1B}, {0x2191, 0x18}, {0x2192, 0x1A}, {0x2193, 0x19}, /* ← ↑ → ↓ */ {0x2500, 0xC4}, {0x2502, 0xB3}, {0x250C, 0xDA}, {0x2510, 0xBF}, /* ─ │ ┌ ┐ */ {0x2514, 0xC0}, {0x2518, 0xD9}, {0x251C, 0xC3}, {0x2524, 0xB4}, /* └ ┘ ├ ┤ */ {0x252C, 0xC2}, {0x2534, 0xC1}, {0x253C, 0xC5}, /* ┬ ┴ ┼ */ {0x2580, 0xDF}, {0x2584, 0xDC}, {0x2588, 0xDB}, /* ▀ ▄ █ */ {0x2591, 0xB0}, {0x2592, 0xB1}, {0x2593, 0xB2}, {0x25A0, 0xFE}, /* ░ ▒ ▓ ■ */ {0x2713, 0xFB}, {0x2714, 0xFB}, /* ✓ ✔ → галка */ {0x2715, 0x78}, {0x2717, 0x78}, {0x2718, 0x78}, /* ✕ ✗ ✘ → 'x' */ }; #define UTF_SYM_N (uint8_t)(sizeof(utf_sym) / sizeof(utf_sym[0])) /* Кодпойнт → CP866-глиф(ы). Кириллица по таблице; спецслучаи (…, BOM) явно; * остальные ходовые символы по таблице utf_sym[]; прочее → '?'. */ static void conv_emit_cp(uint32_t cp) { if (cp < 0x80) { conv_put((uint8_t)cp); return; } if (cp >= 0x0400 && cp <= 0x045F) { conv_put(utf_cyr_to_866[cp - 0x0400]); return; } if (cp == 0x2026) { conv_put('.'); conv_put('.'); conv_put('.'); return; } /* … → "..." */ if (cp == 0xFEFF) return; /* BOM/ZWNBSP — выкинуть */ for (uint8_t i = 0; i < UTF_SYM_N; i++) if (utf_sym[i].utf8 == (uint16_t)cp) { conv_put(utf_sym[i].cp866); return; } conv_put('?'); } /* Последовательное чтение байта ОРИГИНАЛА (utf-8) конвертером. Оригинал лежит * в orig_file_phys[]; мапим его страницу в W3 напрямую, со своей кэш-переменной * cv_page (отдельной от cur_page индексатора, т.к. оба используют W3 и * чередуются). На границе с fb()-чтением utf-буфера кэши взаимно сбрасываются. */ static uint8_t cv_read(uint32_t s) { uint8_t pg = (uint8_t)(s >> PAGE_BITS); if (pg != cv_page) { sprinter_page_w3(orig_file_phys[pg]); cv_page = pg; } return *((volatile uint8_t *)(0xC000u + (uint16_t)(s & PAGE_MASK))); } /* Возобновляемая конвертация: дописывает utf_phys из оригинала, пока * сконвертированных (flushed) байт меньше target и не достигнут конец * исходника. По выходу file_size = доступная (сконвертированная) часть; при * достижении конца фиксирует utf_size и снимает g_utf_building. */ void utf_convert_more(uint32_t target) { cv_page = 0xFF; /* W3 был на utf (fb индексатора) — пере-смаппим orig */ uint32_t n = orig_file_size; while (utf_src < n && (uint32_t)((uint32_t)conv_page * PAGE_SIZE + conv_off) < target) { uint8_t b = (uint8_t)cv_read(utf_src++); if (b < 0x80) { conv_emit_cp(b); continue; } uint32_t cp; uint8_t need; if ((b & 0xE0) == 0xC0) { cp = (uint32_t)(b & 0x1F); need = 1; } else if ((b & 0xF0) == 0xE0) { cp = (uint32_t)(b & 0x0F); need = 2; } else if ((b & 0xF8) == 0xF0) { cp = (uint32_t)(b & 0x07); need = 3; } else { conv_put('?'); continue; } uint8_t ok = 1; for (uint8_t k = 0; k < need; k++) { if (utf_src >= n) { ok = 0; break; } uint8_t cb = (uint8_t)cv_read(utf_src); if ((cb & 0xC0) != 0x80) { ok = 0; break; } cp = (cp << 6) | (uint32_t)(cb & 0x3F); utf_src++; } if (!ok || need == 3) { conv_put('?'); continue; } conv_emit_cp(cp); } conv_flush(); file_size = (uint32_t)conv_page * PAGE_SIZE + conv_off; /* доступно индексатору */ if (utf_src >= n) { utf_size = file_size; g_utf_building = 0; } cur_page = 0xFF; /* W3 был на orig — fb индексатора пере-смаппит utf */ } /* Выделяет страницы под UTF-8 набор (конвертированный ≤ оригинала по размеру) * и сбрасывает приватное состояние конвертера в начало (conv-буфер, utf_src, * g_utf_building). Сама конвертация — инкрементальная, через utf_convert_more(). * Возврат: 1 — успех, 0 — нет EMM. */ uint8_t utf_alloc(void) { utf_pages = orig_file_pages; utf_blk = mem_alloc_pages(utf_pages); if (utf_blk == 0) return 0; for (uint8_t i = 0; i < utf_pages; i++) utf_phys[i] = mem_get_page(utf_blk, i); conv_n = 0; conv_page = 0; conv_off = 0; /* converter reset (вызывается один раз) */ utf_src = 0; g_utf_building = 1; return 1; }