mirror of
https://github.com/crosspoint-reader/crosspoint-reader.git
synced 2026-08-13 14:21:57 -07:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d09c755cf2 | ||
|
|
bccb4cc43f | ||
|
|
f4378bae4f | ||
|
|
7ab9c21afc |
No files matched your search
@@ -0,0 +1,431 @@
|
||||
_language_name: "Български"
|
||||
_language_code: "BG"
|
||||
_order: "31"
|
||||
|
||||
STR_CROSSPOINT: "CrossPoint"
|
||||
STR_BOOTING: "ЗАРЕЖДАНЕ"
|
||||
STR_SLEEPING: "СПЯЩ РЕЖИМ"
|
||||
STR_ENTERING_SLEEP: "Преминаване в спящ режим"
|
||||
STR_BROWSE_FILES: "Разглеждане на файлове"
|
||||
STR_FILE_TRANSFER: "Прехвърляне на файлове"
|
||||
STR_SETTINGS_TITLE: "Настройки"
|
||||
STR_CONTINUE_READING: "Продължи четенето"
|
||||
STR_RESUME: "Продължи"
|
||||
STR_NO_OPEN_BOOK: "Няма отворена книга"
|
||||
STR_START_READING: "Започни четене по-долу"
|
||||
STR_NO_FILES_FOUND: "Не са намерени файлове"
|
||||
STR_SELECT_CHAPTER: "Избери глава"
|
||||
STR_NO_CHAPTERS: "Няма глави"
|
||||
STR_END_OF_BOOK: "Край на книгата"
|
||||
STR_EMPTY_CHAPTER: "Празна глава"
|
||||
STR_INDEXING: "Индексиране"
|
||||
STR_INDEX_FAILED: "Неуспешно индексиране - невалидна книга"
|
||||
STR_MEMORY_ERROR: "Грешка в паметта"
|
||||
STR_PAGE_LOAD_ERROR: "Грешка при зареждане на страницата"
|
||||
STR_EMPTY_FILE: "Празен файл"
|
||||
STR_INVALID_BMP_FILE: "Невалиден BMP файл"
|
||||
STR_FILE_OPEN_FAILED: "Файлът не може да бъде отворен"
|
||||
STR_OUT_OF_BOUNDS: "Извън обхвата"
|
||||
STR_LOADING: "Зареждане..."
|
||||
STR_LOADING_POPUP: "Зареждане"
|
||||
STR_LOOKUP: "Търсене"
|
||||
STR_DICT_LOOKING_UP: "Търсене..."
|
||||
STR_DICT_INDEXING: "Индексиране на речника..."
|
||||
STR_DICT_NOT_FOUND: "Не е намерено"
|
||||
STR_DICT_NO_DICT_SET: "Не е зададен речник"
|
||||
STR_DICT_ERROR: "Грешка в речника"
|
||||
STR_DICT_READ_FAILED: "Определението не може да бъде прочетено"
|
||||
STR_DICT_DECOMPRESS_ERROR: "Определението не може да бъде декомпресирано"
|
||||
STR_DICT_LOW_MEMORY: "Недостатъчна памет"
|
||||
STR_WIFI_NETWORKS: "Wi-Fi мрежи"
|
||||
STR_NO_NETWORKS: "Не са намерени мрежи"
|
||||
STR_NETWORKS_FOUND: "Намерени мрежи: %zu"
|
||||
STR_SCANNING: "Сканиране..."
|
||||
STR_FINDING_SAVED_WIFI: "Търсене на запазен Wi-Fi..."
|
||||
STR_CONNECTING: "Свързване..."
|
||||
STR_CONNECTING_SAVED_WIFI: "Свързване към запазен Wi-Fi..."
|
||||
STR_SHOW_NETWORKS: "Покажи"
|
||||
STR_CONNECTED: "Свързан!"
|
||||
STR_CONNECTION_FAILED: "Неуспешна връзка"
|
||||
STR_FORGET_NETWORK: "Забравяне на мрежата?"
|
||||
STR_SAVE_PASSWORD: "Запазване на паролата за следващия път?"
|
||||
STR_PRESS_OK_SCAN: "Натисни OK за ново сканиране"
|
||||
STR_JOIN_NETWORK: "Присъединяване към мрежа"
|
||||
STR_CREATE_HOTSPOT: "Създаване на hotspot"
|
||||
STR_JOIN_DESC: "Свързване към съществуваща Wi-Fi мрежа"
|
||||
STR_HOTSPOT_DESC: "Създаване на Wi-Fi мрежа, за други устройства"
|
||||
STR_STARTING_HOTSPOT: "Стартиране на hotspot..."
|
||||
STR_HOTSPOT_MODE: "Режим на hotspot"
|
||||
STR_CONNECT_WIFI_HINT: "Свържи устройството си към Wi-Fi мрежа"
|
||||
STR_OPEN_URL_HINT: "Отвори този URL адрес в браузъра си"
|
||||
STR_OR_HTTP_PREFIX: "или http://"
|
||||
STR_SCAN_QR_HINT: "или сканирай QR кода с телефона си:"
|
||||
STR_CALIBRE_WIRELESS: "Calibre безжично"
|
||||
STR_NETWORK_LEGEND: "* = Криптирана | + = Запазена"
|
||||
STR_MAC_ADDRESS: "MAC адрес:"
|
||||
STR_CHECKING_WIFI: "Проверка на Wi-Fi..."
|
||||
STR_ENTER_WIFI_PASSWORD: "Въведи Wi-Fi парола"
|
||||
STR_ADD_HIDDEN_NETWORK: "Добави скрита мрежа..."
|
||||
STR_ENTER_WIFI_SSID: "Въведи име на мрежата (SSID)"
|
||||
STR_TO_PREFIX: "към "
|
||||
STR_CALIBRE_RECEIVING: "Получаване: "
|
||||
STR_CALIBRE_RECEIVED: "Получено: "
|
||||
STR_CALIBRE_INSTRUCTION_1: "1) Инсталирай плъгина CrossPoint Reader"
|
||||
STR_CALIBRE_INSTRUCTION_2: "2) Бъди в същата Wi-Fi мрежа"
|
||||
STR_CALIBRE_INSTRUCTION_3: "3) В Calibre: \"Изпрати към устройство\""
|
||||
STR_CALIBRE_INSTRUCTION_4: "\"Дръж този екран отворен по време на изпращане\""
|
||||
STR_CAT_DISPLAY: "Екран"
|
||||
STR_CAT_READER: "Четец"
|
||||
STR_CAT_CONTROLS: "Контрол"
|
||||
STR_CAT_SYSTEM: "Система"
|
||||
STR_SLEEP_SCREEN: "Екран при спящ режим"
|
||||
STR_QUICK_RESUME_TIMEOUT: "Бързо продълж. при неактивност"
|
||||
STR_SLEEP_COVER_MODE: "Мащабиране на спящ екран"
|
||||
STR_HIDE_BATTERY: "Скрий % на батерията"
|
||||
STR_EXTRA_SPACING: "Доп. интервал между абзаци"
|
||||
STR_TEXT_AA: "Изглаждане на текста"
|
||||
STR_IMAGES: "Изображения"
|
||||
STR_IMAGES_DISPLAY: "Показване"
|
||||
STR_IMAGES_PLACEHOLDER: "Празен слот"
|
||||
STR_IMAGES_SUPPRESS: "Скриване"
|
||||
STR_EOB_HOME: "Начало"
|
||||
STR_EOB_CONTINUE_WITH: "Продължи с"
|
||||
STR_SHORT_PWR_BTN: "Натиск. бутона за захранване"
|
||||
STR_ORIENTATION: "Ориентация на четене"
|
||||
STR_SIDE_BTN_LAYOUT: "Странични бутони"
|
||||
STR_TOUCH_READER_CONTROLS: "Сензорно управление на четеца"
|
||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Ориентация на предните бутони"
|
||||
STR_LONG_PRESS_BEHAVIOR: "Задържане на преден бутон"
|
||||
STR_LONG_PRESS_BEHAVIOR_OFF: "ИЗКЛ."
|
||||
STR_LONG_PRESS_BEHAVIOR_SKIP: "Пропускане на глава"
|
||||
STR_LONG_PRESS_BEHAVIOR_ORIENTATION: "Промяна на ориентацията"
|
||||
STR_LONG_PRESS_MENU: "Задържане на меню бутон"
|
||||
STR_FONT_PREVIEW_TEXT: "Под южно дърво, цъфтящо в синьо, бягаше малко пухкаво зайче."
|
||||
STR_FONT_FAMILY: "Шрифт на четеца"
|
||||
STR_FONT_SIZE: "Размер на шрифта на четеца"
|
||||
STR_TEXT_SETTINGS: "Настройки на текста"
|
||||
STR_FONT: "Шрифт"
|
||||
STR_SIZE: "Размер"
|
||||
STR_LAYOUT: "Изглед"
|
||||
STR_ALIGNMENT: "Подравняване"
|
||||
STR_STYLE: "Стил"
|
||||
STR_NOT_IN_PREVIEW: "Не се показва в предварителния преглед"
|
||||
STR_LINE_SPACING: "Интервал между редовете"
|
||||
STR_SCREEN_MARGIN: "Отстъпи на екрана"
|
||||
STR_PARA_ALIGNMENT: "Подравняване на абзаца"
|
||||
STR_HYPHENATION: "Разделяне на срички"
|
||||
STR_TIME_TO_SLEEP: "Време до заспиване"
|
||||
STR_SHOW_HIDDEN_FILES: "Показване на скритите файлове"
|
||||
STR_REMOVE_READ_FROM_RECENTS: "Махни прочетените от скорошни"
|
||||
STR_MOVE_FINISHED_TO_READ: "Измести завършените в папка read"
|
||||
STR_REFRESH_FREQ: "Честота на опресняване"
|
||||
STR_KOREADER_SYNC: "KOReader синхронизация"
|
||||
STR_CHECK_UPDATES: "Проверка за актуализации"
|
||||
STR_LANGUAGE: "Език"
|
||||
STR_CLEAR_READING_CACHE: "Изчисти кеша на четенето"
|
||||
STR_USERNAME: "Потребителско име"
|
||||
STR_PASSWORD: "Парола"
|
||||
STR_SYNC_SERVER_URL: "URL адрес на сървъра за синхронизация"
|
||||
STR_DOCUMENT_MATCHING: "Съпоставяне на документи"
|
||||
STR_SEND_METADATA: "Изпращане на метаданни на документа"
|
||||
STR_SYNC_BEHAVIOR: "Поведение при синхронизация"
|
||||
STR_ASK_EVERY_TIME: "Питай всеки път"
|
||||
STR_SMART_SYNC: "Умна синхронизация"
|
||||
STR_AUTHENTICATE: "Удостовери се"
|
||||
STR_SIGN_UP: "Регистрация"
|
||||
STR_CREATING_ACCOUNT: "Създаване на профил..."
|
||||
STR_ACCOUNT_CREATED: "Профилът е създаден"
|
||||
STR_SIGNUP_FAILED: "Неуспешна регистрация"
|
||||
STR_USERNAME_TAKEN: "Потребителското име вече е регистрирано"
|
||||
STR_KOREADER_USERNAME: "KOReader потребителско име"
|
||||
STR_KOREADER_PASSWORD: "KOReader парола"
|
||||
STR_FILENAME: "Име на файла"
|
||||
STR_BINARY: "Двоичен"
|
||||
STR_SET_CREDENTIALS_FIRST: "Първо задай данни за вход"
|
||||
STR_WIFI_CONN_FAILED: "Неуспешна Wi-Fi връзка"
|
||||
STR_AUTHENTICATING: "Удостоверяване..."
|
||||
STR_AUTH_SUCCESS: "Успешно удостоверяване!"
|
||||
STR_KOREADER_AUTH: "KOReader удостоверяване"
|
||||
STR_SYNC_READY: "KOReader синхронизацията е готова за използване"
|
||||
STR_AUTH_FAILED: "Неуспешно удостоверяване"
|
||||
STR_DONE: "Готово"
|
||||
STR_CLEAR_CACHE_WARNING_1: "Това ще изчисти всички кеширани данни за книгите."
|
||||
STR_CLEAR_CACHE_WARNING_2: "Целият прогрес на четенето ще бъде загубен!"
|
||||
STR_CLEAR_CACHE_WARNING_3: "Книгите ще трябва да бъдат преиндексирани"
|
||||
STR_CLEAR_CACHE_WARNING_4: "при следващо отваряне."
|
||||
STR_CLEARING_CACHE: "Изчистване на кеша..."
|
||||
STR_CACHE_CLEARED: "Кешът е изчистен"
|
||||
STR_ITEMS_REMOVED: "премахнати елемента"
|
||||
STR_FAILED_LOWER: "неуспешно"
|
||||
STR_CLEAR_CACHE_FAILED: "Неуспешно изчистване на кеша"
|
||||
STR_CHECK_SERIAL_OUTPUT: "Провери серийния изход за подробности"
|
||||
STR_DARK: "Тъмен"
|
||||
STR_LIGHT: "Светъл"
|
||||
STR_CUSTOM: "Персонализиран"
|
||||
STR_COVER: "Корица"
|
||||
STR_NONE_OPT: "Няма"
|
||||
STR_FIT: "Побиране"
|
||||
STR_CROP: "Изрязване"
|
||||
STR_NEVER: "Никога"
|
||||
STR_IN_READER: "В четеца"
|
||||
STR_ALWAYS: "Винаги"
|
||||
STR_IGNORE: "Игнорирай"
|
||||
STR_SLEEP: "Спящ режим"
|
||||
STR_PAGE_TURN: "Обръщане на страница"
|
||||
STR_FORCE_REFRESH: "Опресни екрана"
|
||||
STR_PORTRAIT: "Портрет"
|
||||
STR_LANDSCAPE_CW: "Пейзаж (по часовниковата стрелка)"
|
||||
STR_INVERTED: "Обърнато"
|
||||
STR_ORIENTATION_INVERTED: "Портрет 180°"
|
||||
STR_LANDSCAPE_CCW: "Пейзаж (срещу часовниковата стрелка)"
|
||||
STR_PREV_NEXT: "Предишен/Следващ"
|
||||
STR_NEXT_PREV: "Следващ/Предишен"
|
||||
STR_KOSYNC: "KOSync"
|
||||
STR_BOOKMARK_OPTION: "Отметка"
|
||||
STR_DICTIONARY: "Речник"
|
||||
STR_DISABLED: "Изключено"
|
||||
STR_NOTO_SERIF: "Noto Serif"
|
||||
STR_NOTO_SANS: "Noto Sans"
|
||||
STR_TIGHT: "Тесен"
|
||||
STR_NORMAL: "Нормален"
|
||||
STR_WIDE: "Широк"
|
||||
STR_JUSTIFY: "Двустранно"
|
||||
STR_ALIGN_LEFT: "Ляво"
|
||||
STR_CENTER: "Центрирано"
|
||||
STR_ALIGN_RIGHT: "Дясно"
|
||||
STR_PAGES_1: "1 страница"
|
||||
STR_PAGES_5: "5 страници"
|
||||
STR_PAGES_10: "10 страници"
|
||||
STR_PAGES_15: "15 страници"
|
||||
STR_PAGES_30: "30 страници"
|
||||
STR_UPDATE: "Актуализация"
|
||||
STR_CHECKING_UPDATE: "Проверка за актуализация..."
|
||||
STR_NEW_UPDATE: "Налична е нова актуализация!"
|
||||
STR_CURRENT_VERSION: "Текуща версия: "
|
||||
STR_NEW_VERSION: "Нова версия: "
|
||||
STR_UPDATING: "Актуализиране..."
|
||||
STR_NO_UPDATE: "Няма налична актуализация"
|
||||
STR_UPDATE_FAILED: "Неуспешна актуализация"
|
||||
STR_UPDATE_COMPLETE: "Актуализацията завърши"
|
||||
STR_POWER_ON_HINT: "Натисни и задръж бутона за захранване за включване"
|
||||
STR_RESTARTING_HINT: "Рестартиране... Ако устройството не се рестартира, задръж бутона за захранване за няколко секунди."
|
||||
STR_NO_ENTRIES: "Не са намерени записи"
|
||||
STR_DOWNLOADING: "Изтегляне..."
|
||||
STR_DOWNLOAD_FAILED: "Неуспешно изтегляне"
|
||||
STR_ERROR_MSG: "Грешка:"
|
||||
STR_UNNAMED: "Без име"
|
||||
STR_HOLD_OPEN_TO_DELETE: "Задръж отворено за изтриване"
|
||||
STR_NO_SERVER_URL: "Не е зададен URL адрес на сървъра"
|
||||
STR_FETCH_FEED_FAILED: "Неуспешно извличане на емисията"
|
||||
STR_PARSE_FEED_FAILED: "Неуспешен анализ на емисията"
|
||||
STR_NEXT_PAGE: "Следваща страница »"
|
||||
STR_PREV_PAGE: "« Предишна страница"
|
||||
STR_NETWORK_PREFIX: "Мрежа: "
|
||||
STR_IP_ADDRESS_PREFIX: "IP адрес: "
|
||||
STR_ERROR_GENERAL_FAILURE: "Грешка: Обща неуспешна операция"
|
||||
STR_ERROR_NETWORK_NOT_FOUND: "Грешка: Мрежата не е намерена"
|
||||
STR_ERROR_CONNECTION_TIMEOUT: "Грешка: Изтекло време за връзка"
|
||||
STR_SD_CARD: "SD карта"
|
||||
STR_BACK: "« Назад"
|
||||
STR_EXIT: "« Изход"
|
||||
STR_HOME: "« Начало"
|
||||
STR_SELECT: "Избери"
|
||||
STR_SELECTED: "Избрано"
|
||||
STR_TOGGLE: "Избор"
|
||||
STR_TOGGLE_BOOKMARK: "Превключи отметка"
|
||||
STR_CONFIRM: "Потвърди"
|
||||
STR_CANCEL: "Отказ"
|
||||
STR_CONNECT: "Свържи"
|
||||
STR_OPEN: "Отвори"
|
||||
STR_DOWNLOAD: "Изтегли"
|
||||
STR_RETRY: "Повтори"
|
||||
STR_TAP_TO_RETRY: "Докосни за нов опит"
|
||||
STR_YES: "Да"
|
||||
STR_NO: "Не"
|
||||
STR_SHOW: "Покажи"
|
||||
STR_HIDE: "Скрий"
|
||||
STR_STATE_ON: "ВКЛ."
|
||||
STR_STATE_OFF: "ИЗКЛ."
|
||||
STR_NOT_SET: "Незададено"
|
||||
STR_DIR_LEFT: "Ляво"
|
||||
STR_DIR_RIGHT: "Дясно"
|
||||
STR_DIR_UP: "Нагоре"
|
||||
STR_DIR_DOWN: "Надолу"
|
||||
STR_OK_BUTTON: "OK"
|
||||
STR_KEY_SHIFT: "Shift"
|
||||
STR_KEY_MODE_SYMBOLS: "?123"
|
||||
STR_KEY_MODE_ABC: "abc"
|
||||
STR_SLEEP_COVER_FILTER: "Филтър корица на спящ екран"
|
||||
STR_FILTER_CONTRAST: "Контраст"
|
||||
STR_CUSTOMISE_STATUS_BAR: "Персонализирай лентата на състоянието"
|
||||
STR_CHAPTER_PAGE_COUNT: "Брой страници на главата"
|
||||
STR_BOOK_PROGRESS_PERCENTAGE: "Процент прогрес на книгата"
|
||||
STR_PROGRESS_BAR: "Лента за прогрес"
|
||||
STR_PROGRESS_BAR_THICKNESS: "Дебелина на лентата за прогрес"
|
||||
STR_PROGRESS_BAR_THIN: "Тънка"
|
||||
STR_PROGRESS_BAR_MEDIUM: "Средна"
|
||||
STR_PROGRESS_BAR_THICK: "Дебела"
|
||||
STR_BOOK: "Книга"
|
||||
STR_CHAPTER: "Глава"
|
||||
STR_EXAMPLE_CHAPTER: "Глава 21"
|
||||
STR_EXAMPLE_BOOK: "Заглавие на книгата"
|
||||
STR_PREVIEW: "Преглед"
|
||||
STR_TITLE: "Заглавие"
|
||||
STR_BATTERY: "Батерия"
|
||||
STR_XTC_STATUS_BAR: "XTC лента на състоянието"
|
||||
STR_BOTTOM: "Долу"
|
||||
STR_TOP: "Горе"
|
||||
STR_CLOCK: "Часовник"
|
||||
STR_CLOCK_UTC_OFFSET: "UTC отместване на часовника"
|
||||
STR_CLOCK_FORMAT: "Формат на часовника"
|
||||
STR_CLOCK_FORMAT_24H: "24-часов"
|
||||
STR_CLOCK_FORMAT_12H: "12-часов"
|
||||
STR_CURRENT_TIME: "Текущо време:"
|
||||
STR_NEXT_FIELD: "Следващо"
|
||||
STR_CLOCK_SYNC: "Синхронизирай часовника"
|
||||
STR_CLOCK_SYNC_NOW: "Синхронизирай часовника сега"
|
||||
STR_CLOCK_SYNCING: "Синхронизиране от NTP..."
|
||||
STR_CLOCK_SYNC_OK: "Часовникът е синхронизиран"
|
||||
STR_CLOCK_SYNC_FAIL: "Неуспешна синхронизация"
|
||||
STR_CLOCK_SYNC_NO_WIFI: "Wi-Fi не е свързан"
|
||||
STR_CLOCK_SYNC_NO_WIFI_HINT: "Свържи се с Wi-Fi и опитай отново."
|
||||
STR_CLOCK_SYNCED: "Часовникът е синхронизиран"
|
||||
STR_UI_THEME: "Тема на интерфейса"
|
||||
STR_THEME_CLASSIC: "Класическа"
|
||||
STR_THEME_LYRA: "Lyra"
|
||||
STR_THEME_ROUNDEDRAFF: "RoundedRaff"
|
||||
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
|
||||
STR_SUNLIGHT_FADING_FIX: "Защита от избледняване при слънце"
|
||||
STR_REMAP_FRONT_BUTTONS: "Преназначаване на предните бутони"
|
||||
STR_BOOKMARKS: "Отметки"
|
||||
STR_BOOKMARK_ADDED: "Отметката е добавена."
|
||||
STR_BOOKMARK_REMOVED: "Отметката е премахната."
|
||||
STR_OPDS_BROWSER: "OPDS браузър"
|
||||
STR_SEARCH: "Търсене"
|
||||
STR_COVER_CUSTOM: "Корица + Персонализиран"
|
||||
STR_QUICK_RESUME: "Бързо продължаване"
|
||||
STR_MENU_RECENT_BOOKS: "Скорошни книги"
|
||||
STR_REMOVE_FROM_RECENTS: "Премахване от скорошните книги?"
|
||||
STR_NO_RECENT_BOOKS: "Няма скорошни книги"
|
||||
STR_CALIBRE_DESC: "Използвай безжично прехвърляне през Calibre"
|
||||
STR_FORGET_AND_REMOVE: "Забравяне на мрежата и премахване на запазената парола?"
|
||||
STR_FORGET_BUTTON: "Забрави"
|
||||
STR_CALIBRE_STARTING: "Стартиране на Calibre..."
|
||||
STR_CALIBRE_SETUP: "Настройка"
|
||||
STR_CALIBRE_STATUS: "Състояние"
|
||||
STR_CLEAR_BUTTON: "Изчисти"
|
||||
STR_DEFAULT_VALUE: "По подразбиране"
|
||||
STR_REMAP_PROMPT: "Натисни предния бутон за всяка роля"
|
||||
STR_UNASSIGNED: "Незададено"
|
||||
STR_ALREADY_ASSIGNED: "Вече е зададено"
|
||||
STR_REMAP_RESET_HINT: "Страничен бутон Нагоре: Рестарт на настройки"
|
||||
STR_REMAP_CANCEL_HINT: "Страничен бутон Надолу: Отказ от преназначаване"
|
||||
STR_HW_BACK_LABEL: "Назад (1-ви бутон)"
|
||||
STR_HW_CONFIRM_LABEL: "Потвърди (2-ри бутон)"
|
||||
STR_HW_LEFT_LABEL: "Ляво (3-ти бутон)"
|
||||
STR_HW_RIGHT_LABEL: "Дясно (4-ти бутон)"
|
||||
STR_GO_TO_PERCENT: "Отиди на %"
|
||||
STR_GO_HOME_BUTTON: "Начало"
|
||||
STR_SYNC_PROGRESS: "Синхронизирай прогреса"
|
||||
STR_DELETE_CACHE: "Изтрий кеша на книгата"
|
||||
STR_DELETE: "Изтрий"
|
||||
STR_CONFIRM_DELETE_BOOKMARK: "Изтриване на тази отметка?"
|
||||
STR_DISPLAY_QR: "Покажи страницата като QR код"
|
||||
STR_CHAPTER_PREFIX: "Глава: "
|
||||
STR_PAGES_SEPARATOR: " страници | "
|
||||
STR_BOOK_PREFIX: "Книга: "
|
||||
STR_CALIBRE_URL_HINT: "За Calibre добави /opds към твоя URL адрес"
|
||||
STR_SYNCING_TIME: "Синхронизиране на времето..."
|
||||
STR_CALC_HASH: "Изчисляване на хеш на документа..."
|
||||
STR_HASH_FAILED: "Неуспешно изчисляване на хеша на документа"
|
||||
STR_FETCH_PROGRESS: "Извличане на прогреса от сървъра..."
|
||||
STR_UPLOAD_PROGRESS: "Качване на прогреса..."
|
||||
STR_NO_CREDENTIALS_MSG: "Не са зададени данни за вход"
|
||||
STR_KOREADER_SETUP_HINT: "Настрой KOReader профил в Настройки"
|
||||
STR_PROGRESS_FOUND: "Прогресът е намерен!"
|
||||
STR_REMOTE_LABEL: "От сървъра:"
|
||||
STR_LOCAL_LABEL: "Локален:"
|
||||
STR_PAGE_OVERALL_FORMAT: "Страница %d, %.2f%% общо"
|
||||
STR_PAGE_TOTAL_OVERALL_FORMAT: "Страница %d/%d, %.2f%% общо"
|
||||
STR_DEVICE_FROM_FORMAT: " От: %s"
|
||||
STR_APPLY_REMOTE: "Приложи прогреса от сървъра"
|
||||
STR_UPLOAD_LOCAL: "Качи локалния прогрес"
|
||||
STR_NO_REMOTE_MSG: "Не е намерен прогрес на сървъра"
|
||||
STR_UPLOAD_PROMPT: "Качване на текущата позиция?"
|
||||
STR_UPLOAD_SUCCESS: "Прогресът е качен!"
|
||||
STR_ALREADY_SYNCED: "Вече е синхронизиран"
|
||||
STR_SYNC_FAILED_MSG: "Неуспешна синхронизация"
|
||||
STR_SAVE_PROGRESS_FAILED: "Прогресът не може да бъде запазен"
|
||||
STR_SECTION_PREFIX: "Раздел "
|
||||
STR_UPLOAD: "Качи"
|
||||
STR_BOOK_S_STYLE: "Стил на книгата"
|
||||
STR_EMBEDDED_STYLE: "Вграден стил"
|
||||
STR_FOCUS_READING: "Фокусирано четене"
|
||||
STR_OPDS_SERVER_URL: "URL адрес на OPDS сървъра"
|
||||
STR_PWR_BTN_FOOTNOTE_BACK: "Бързо връщане от бележки под линия"
|
||||
STR_BACK_SHORT_TO_FILE_BROWSER: "Натиск. назад към файловия браузър"
|
||||
STR_SET_SLEEP_COVER: "Задай корица"
|
||||
STR_FOOTNOTES: "Бележки под линия"
|
||||
STR_NO_FOOTNOTES: "Няма бележки под линия на тази страница"
|
||||
STR_LINK: "[връзка]"
|
||||
STR_SCREENSHOT_BUTTON: "Направи снимка на екрана"
|
||||
STR_SLEEP_TIMER_VALUE_FORMAT: "%u мин"
|
||||
STR_SLEEP_NEVER: "Никога"
|
||||
STR_STEP_HINT_FRONT: "Предни бутони:"
|
||||
STR_STEP_HINT_SIDE: "Странични бутони:"
|
||||
STR_ADD_SERVER: "Добави сървър"
|
||||
STR_SERVER_NAME: "Име на сървъра"
|
||||
STR_NO_SERVERS: "Няма конфигурирани OPDS сървъри"
|
||||
STR_DELETE_SERVER: "Изтрий сървъра"
|
||||
STR_OPDS_SERVERS: "OPDS сървъри"
|
||||
STR_OPDS_DOWNLOAD_FOLDER: "Папка за изтегляне"
|
||||
STR_OPDS_FILENAME_FORMAT: "Формат на името на файла"
|
||||
STR_FMT_AUTHOR_TITLE: "Автор - Заглавие"
|
||||
STR_FMT_TITLE_AUTHOR: "Заглавие - Автор"
|
||||
STR_FMT_TITLE: "Заглавие"
|
||||
STR_OPDS_SD_ROOT: "Корен на SD картата"
|
||||
STR_AUTO_TURN_ENABLED: "Автоматично обръщане на страница: "
|
||||
STR_AUTO_TURN_PAGES_PER_MIN: "Автоматично обръщане (страници в минута)"
|
||||
STR_MANAGE_FONTS: "Управление на шрифтовете"
|
||||
STR_FONT_BROWSER: "Браузър на шрифтове"
|
||||
STR_LOADING_FONT_LIST: "Зареждане на списъка с шрифтове..."
|
||||
STR_NO_FONTS_AVAILABLE: "Няма налични шрифтове"
|
||||
STR_FONT_INSTALLED: "Шрифтът е инсталиран!"
|
||||
STR_FONT_INSTALL_FAILED: "Неуспешна инсталация на шрифта"
|
||||
STR_INSTALLED: "Инсталиран"
|
||||
STR_DOWNLOAD_ALL: "Изтегли всички"
|
||||
STR_UPDATE_ALL: "Актуализирай всички"
|
||||
STR_UPDATE_AVAILABLE: "Актуализация"
|
||||
STR_CRASH_TITLE: "Системен срив"
|
||||
STR_CRASH_DESCRIPTION: "Подробен отчет е запазен в crash_report.txt. Моля, приложи този файл към твоя бъг репорт."
|
||||
STR_CRASH_REASON: "Причина за срива:"
|
||||
STR_CRASH_NO_REASON: "(Не е записана причина)"
|
||||
STR_TILT_PAGE_TURN: "Обръщане на страница чрез накланяне"
|
||||
STR_KB_HINT_MOVE_CURSOR: "Натисни ЛЯВО или ДЯСНО за преместване на курсора"
|
||||
STR_KB_HINT_RETURN_CURSOR: "Натисни ЛЯВО за връщане към позицията на курсора"
|
||||
STR_KB_HINT_HIDE_PASSWORD: "Задръж ДЯСНО и натисни [***] за скриване на паролата"
|
||||
STR_KB_HINT_SHOW_PASSWORD: "Задръж ДЯСНО и натисни [abc] за показване на паролата"
|
||||
STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Натисни [***] за скриване на паролата"
|
||||
STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Натисни [abc] за показване на паролата"
|
||||
STR_KB_HINT_EDIT_ENTRY: "Задръж НАГОРЕ за редактиране на записа"
|
||||
STR_KB_TIPS: "Съвети:"
|
||||
STR_KB_HINT_RETURN_KEYBOARD: "Натисни НАДОЛУ за връщане към клавиатурата"
|
||||
STR_KB_HINT_EXIT_URL_MODE: "Натисни ABC за изход от URL режим"
|
||||
STR_KB_HINT_CLEAR_TEXT: "Задръж DEL за изчистване на целия текст"
|
||||
STR_KB_HINT_SECONDARY_CHAR: "Задръж SELECT за символи"
|
||||
STR_KB_HINT_UPPER_SECONDARY: "Задръж SELECT за ГЛАВНИ БУКВИ или символи"
|
||||
STR_KB_HINT_LOWER_SECONDARY: "Задръж SELECT за малки букви или символи"
|
||||
STR_KB_HINT_URL_SNIPPETS: "Натисни URL за фрагменти"
|
||||
STR_SD_FIRMWARE_UPDATE: "Актуализация на фърмуера чрез SD карта"
|
||||
STR_SELECT_FIRMWARE_FILE: "Избери файл с фърмуер (.bin)"
|
||||
STR_NO_BIN_FILES: "Не са намерени .bin файлове"
|
||||
STR_VALIDATING_FIRMWARE: "Валидиране на фърмуера..."
|
||||
STR_INVALID_FIRMWARE: "Невалиден файл с фърмуер"
|
||||
STR_FIRMWARE_TOO_LARGE: "Фърмуерът е твърде голям за дяла"
|
||||
STR_FIRMWARE_TOO_SMALL: "Файлът с фърмуер е твърде малък"
|
||||
STR_FIRMWARE_UPDATE_PROMPT: "Актуализиране на фърмуера?"
|
||||
STR_FIRMWARE_FILE_OPEN_FAILED: "Файлът не може да бъде отворен"
|
||||
STR_FIRMWARE_WRITE_FAILED: "Неуспешен запис на фърмуера"
|
||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Не изключвай захранването!"
|
||||
STR_RECOVERY_MODE: "Режим за възстановяване"
|
||||
STR_RECOVERY_MODE_HINT: "Постави firmware.bin в основната директория на SD картата и го избери"
|
||||
@@ -273,6 +273,7 @@ STR_SUNLIGHT_FADING_FIX: "Correcció de l'esvaïment pel sol"
|
||||
STR_QUICK_RESUME_TIMEOUT: "Represa ràpida per inactivitat"
|
||||
STR_REMAP_FRONT_BUTTONS: "Reassigna els botons frontals"
|
||||
STR_BOOKMARKS: "Punts de llibre"
|
||||
STR_NO_BOOKMARKS: "No hi ha punts de llibre"
|
||||
STR_BOOKMARK_ADDED: "S'ha afegit el punt de llibre."
|
||||
STR_BOOKMARK_REMOVED: "S'ha eliminat el punt de llibre."
|
||||
STR_OPDS_BROWSER: "Navegador OPDS"
|
||||
@@ -423,6 +424,7 @@ STR_VALIDATING_FIRMWARE: "S'està validant el firmware..."
|
||||
STR_INVALID_FIRMWARE: "Fitxer de firmware no vàlid"
|
||||
STR_FIRMWARE_TOO_LARGE: "El firmware és massa gran per a la partició"
|
||||
STR_FIRMWARE_TOO_SMALL: "El fitxer de firmware és massa petit"
|
||||
STR_FIRMWARE_WRONG_DEVICE: "El firmware és per a un altre dispositiu"
|
||||
STR_FIRMWARE_UPDATE_PROMPT: "Vols actualitzar el firmware?"
|
||||
STR_FIRMWARE_FILE_OPEN_FAILED: "No es pot obrir el fitxer"
|
||||
STR_FIRMWARE_WRITE_FAILED: "Ha fallat l'escriptura del firmware"
|
||||
|
||||
@@ -297,6 +297,7 @@ STR_SUNLIGHT_FADING_FIX: "Corrección de desvanecimiento"
|
||||
STR_QUICK_RESUME_TIMEOUT: "Reanudación rápida tras tiempo"
|
||||
STR_REMAP_FRONT_BUTTONS: "Reconfigurar botones frontales"
|
||||
STR_BOOKMARKS: "Marcadores"
|
||||
STR_NO_BOOKMARKS: "No hay marcadores"
|
||||
STR_BOOKMARK_ADDED: "Marcador añadido."
|
||||
STR_BOOKMARK_REMOVED: "Marcador eliminado."
|
||||
STR_OPDS_BROWSER: "Navegador OPDS"
|
||||
@@ -423,6 +424,7 @@ STR_VALIDATING_FIRMWARE: "Validando el firmware..."
|
||||
STR_INVALID_FIRMWARE: "Archivo de firmware no válido"
|
||||
STR_FIRMWARE_TOO_LARGE: "Firmware demasiado grande p/la partición"
|
||||
STR_FIRMWARE_TOO_SMALL: "Firmware demasiado pequeño"
|
||||
STR_FIRMWARE_WRONG_DEVICE: "El firmware es para otro dispositivo"
|
||||
STR_FIRMWARE_UPDATE_PROMPT: "¿Actualizar el firmware?"
|
||||
STR_FIRMWARE_FILE_OPEN_FAILED: "No se puede abrir el archivo"
|
||||
STR_FIRMWARE_WRITE_FAILED: "Falló la escritura del firmware"
|
||||
|
||||
@@ -381,6 +381,7 @@ STR_FIRMWARE_TOO_SMALL: "Firmware dosyası çok küçük"
|
||||
STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Cihazı kapatmayın!"
|
||||
STR_FIRMWARE_UPDATE_PROMPT: "Firmware güncellensin mi?"
|
||||
STR_FIRMWARE_WRITE_FAILED: "Firmware yazılamadı"
|
||||
STR_FIRMWARE_WRONG_DEVICE: "Firmware farklı bir cihaz için"
|
||||
STR_FONT_BROWSER: "Yazı Tipi Tarayıcısı"
|
||||
STR_FONT_INSTALLED: "Yazı tipi yüklendi!"
|
||||
STR_FONT_INSTALL_FAILED: "Yazı tipi yüklenemedi"
|
||||
|
||||
@@ -278,6 +278,7 @@ STR_THEME_LYRA_EXTENDED: "Lyra Ampliat"
|
||||
STR_SUNLIGHT_FADING_FIX: "Correcció de l'esvaïment pel sol"
|
||||
STR_REMAP_FRONT_BUTTONS: "Reassigna els botons frontals"
|
||||
STR_BOOKMARKS: "Punts de llibre"
|
||||
STR_NO_BOOKMARKS: "No hi ha punts de llibre"
|
||||
STR_BOOKMARK_ADDED: "S'ha afegit el punt de llibre."
|
||||
STR_BOOKMARK_REMOVED: "S'ha eliminat el punt de llibre."
|
||||
STR_OPDS_BROWSER: "Navegador OPDS"
|
||||
@@ -423,6 +424,7 @@ STR_VALIDATING_FIRMWARE: "S'està validant el firmware..."
|
||||
STR_INVALID_FIRMWARE: "Arxiu de firmware no vàlid"
|
||||
STR_FIRMWARE_TOO_LARGE: "El firmware és massa gran per a la partició"
|
||||
STR_FIRMWARE_TOO_SMALL: "L'arxiu de firmware és massa menut"
|
||||
STR_FIRMWARE_WRONG_DEVICE: "El firmware és per a un altre dispositiu"
|
||||
STR_FIRMWARE_UPDATE_PROMPT: "Vols actualitzar el firmware?"
|
||||
STR_FIRMWARE_FILE_OPEN_FAILED: "No es pot obrir l'arxiu"
|
||||
STR_FIRMWARE_WRITE_FAILED: "Ha fallat l'escriptura del firmware"
|
||||
|
||||
@@ -1,346 +0,0 @@
|
||||
#include "LibraryText.h"
|
||||
|
||||
#include <Utf8.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
|
||||
namespace library {
|
||||
namespace {
|
||||
|
||||
// Letters with no canonical decomposition, plus the punctuation that would
|
||||
// otherwise become a space and break word shapes. Everything here is a distinct
|
||||
// letter in Unicode, so no amount of NFD gets us the ASCII form.
|
||||
struct CharMap {
|
||||
uint32_t cp;
|
||||
const char* replacement;
|
||||
};
|
||||
constexpr CharMap EXPLICIT_MAP[] = {
|
||||
{0x00D8, "o"}, // Ø
|
||||
{0x00F8, "o"}, // ø — "Søren" folds to "soren", not "nesb"
|
||||
{0x00C6, "ae"}, // Æ
|
||||
{0x00E6, "ae"}, // æ
|
||||
{0x0152, "oe"}, // Œ
|
||||
{0x0153, "oe"}, // œ
|
||||
{0x00DF, "ss"}, // ß
|
||||
{0x0141, "l"}, // Ł
|
||||
{0x0142, "l"}, // ł
|
||||
{0x00D0, "d"}, // Ð
|
||||
{0x00F0, "d"}, // ð
|
||||
{0x0110, "d"}, // Đ
|
||||
{0x0111, "d"}, // đ
|
||||
{0x00DE, "th"}, // Þ
|
||||
{0x00FE, "th"}, // þ
|
||||
{0x0131, "i"}, // ı
|
||||
{0x0027, "'"}, // ' — kept, not turned into a space, so "O'Brien" stays one word
|
||||
{0x2019, "'"}, // ’ — and so the curly form folds to the same thing
|
||||
{0x2018, "'"}, // ‘
|
||||
};
|
||||
|
||||
const char* explicitMapping(const uint32_t cp) {
|
||||
for (const auto& e : EXPLICIT_MAP) {
|
||||
if (e.cp == cp) return e.replacement;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Reverse lookup in the NFC table: given a precomposed codepoint, return the
|
||||
// base letter it was composed from, or 0.
|
||||
//
|
||||
// The table is sorted by (base, mark) so this is a linear scan rather than a
|
||||
// binary search. It runs once per non-ASCII character at index-build time and
|
||||
// over a short query per keystroke, never over the whole index, so the scan is
|
||||
// not on any hot path. Reusing the generated table costs no extra flash — it is
|
||||
// already linked for utf8ComposeNfc().
|
||||
// Fully decompose: é -> e, and the rare doubly-accented forms (ế -> ê -> e).
|
||||
uint32_t stripDiacritics(uint32_t cp) {
|
||||
for (int guard = 0; guard < 4; guard++) {
|
||||
const uint32_t base = utf8DecomposedBase(cp);
|
||||
if (base == 0) break;
|
||||
cp = base;
|
||||
}
|
||||
return cp;
|
||||
}
|
||||
|
||||
bool isAsciiAlnum(const uint32_t cp) {
|
||||
return (cp >= '0' && cp <= '9') || (cp >= 'a' && cp <= 'z') || (cp >= 'A' && cp <= 'Z');
|
||||
}
|
||||
|
||||
void appendLowerAscii(const uint32_t cp, std::string& out) {
|
||||
out.push_back(static_cast<char>(cp >= 'A' && cp <= 'Z' ? cp - 'A' + 'a' : cp));
|
||||
}
|
||||
|
||||
// Articles stripped from the head of sort and search keys. Display text never
|
||||
// goes through this.
|
||||
constexpr const char* ARTICLES[] = {"the ", "a ", "an ", "le ", "la ", "les ", "l'", "un ",
|
||||
"une ", "de ", "du ", "des ", "der ", "die ", "das ", "el ",
|
||||
"los ", "las ", "il ", "lo ", "gli ", "i ", "o ", "os "};
|
||||
|
||||
// Views into `folded`, not copies: the caller keeps that string alive for as
|
||||
// long as the tokens, and a std::string per token costs an allocation each plus
|
||||
// 24 bytes of stack apiece -- 288 B for the twelve, over the 256 B this repo
|
||||
// asks callers to justify.
|
||||
void splitTokens(std::string_view folded, std::string_view* out, size_t maxTokens, size_t& count) {
|
||||
count = 0;
|
||||
size_t i = 0;
|
||||
while (i < folded.size() && count < maxTokens) {
|
||||
while (i < folded.size() && folded[i] == ' ') i++;
|
||||
const size_t start = i;
|
||||
while (i < folded.size() && folded[i] != ' ') i++;
|
||||
if (i > start) out[count++] = folded.substr(start, i - start);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string fold(const std::string_view text, const bool stripArticle) {
|
||||
std::string out;
|
||||
out.reserve(text.size());
|
||||
|
||||
const auto* cursor = reinterpret_cast<const unsigned char*>(text.data());
|
||||
const auto* end = cursor + text.size();
|
||||
bool pendingSpace = false;
|
||||
|
||||
while (cursor < end) {
|
||||
// The shared decoder stops on NUL and every std::string source is
|
||||
// NUL-terminated, but fold's contract is string_view — and a view may end
|
||||
// mid-sequence. Refuse to decode a lead byte whose continuation bytes lie
|
||||
// past the end rather than trusting whatever sits there.
|
||||
const unsigned char lead = *cursor;
|
||||
const ptrdiff_t promised = lead < 0x80 ? 1
|
||||
: (lead >> 5) == 0x06 ? 2
|
||||
: (lead >> 4) == 0x0E ? 3
|
||||
: (lead >> 3) == 0x1E ? 4
|
||||
: 1;
|
||||
if (promised > end - cursor) break;
|
||||
const uint32_t cp = utf8NextCodepoint(&cursor);
|
||||
if (cp == 0) break;
|
||||
|
||||
if (utf8IsCombiningMark(cp)) continue; // already-decomposed input
|
||||
|
||||
const char* mapped = explicitMapping(cp);
|
||||
if (mapped != nullptr) {
|
||||
if (pendingSpace && !out.empty()) out.push_back(' ');
|
||||
pendingSpace = false;
|
||||
out.append(mapped);
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint32_t base = stripDiacritics(cp);
|
||||
if (isAsciiAlnum(base)) {
|
||||
if (pendingSpace && !out.empty()) out.push_back(' ');
|
||||
pendingSpace = false;
|
||||
appendLowerAscii(base, out);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Everything else — punctuation, symbols, unmapped scripts — separates
|
||||
// words. Deferring the space keeps runs collapsed and drops trailing ones.
|
||||
if (!out.empty()) pendingSpace = true;
|
||||
}
|
||||
|
||||
if (stripArticle) {
|
||||
for (const char* article : ARTICLES) {
|
||||
const size_t len = strlen(article);
|
||||
if (out.size() > len && out.compare(0, len, article) == 0) {
|
||||
out.erase(0, len);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string cleanPersonName(const std::string_view author) {
|
||||
std::string out;
|
||||
out.reserve(author.size());
|
||||
int depth = 0;
|
||||
for (size_t i = 0; i < author.size(); i++) {
|
||||
const char c = author[i];
|
||||
if (c == '[' || c == '(') {
|
||||
depth++;
|
||||
continue;
|
||||
}
|
||||
if (c == ']' || c == ')') {
|
||||
if (depth > 0) depth--;
|
||||
continue;
|
||||
}
|
||||
if (c == ';') break; // secondary authors
|
||||
if (depth > 0) continue;
|
||||
if (c == '_') {
|
||||
// "Herbert G_ Wells" — the underscore stands in for a full stop the
|
||||
// filesystem would not take. Between letters it is an abbreviation dot;
|
||||
// at the end of a word it is just noise.
|
||||
const bool betweenLetters = i > 0 && i + 1 < author.size() &&
|
||||
isalpha(static_cast<unsigned char>(author[i - 1])) &&
|
||||
isalpha(static_cast<unsigned char>(author[i + 1]));
|
||||
out.push_back(betweenLetters ? '.' : ' ');
|
||||
continue;
|
||||
}
|
||||
out.push_back(c);
|
||||
}
|
||||
|
||||
while (!out.empty() &&
|
||||
(out.back() == ' ' || out.back() == ',' || out.back() == '-' || out.back() == '.' || out.back() == '_')) {
|
||||
out.pop_back();
|
||||
}
|
||||
size_t start = 0;
|
||||
while (start < out.size() && out[start] == ' ') start++;
|
||||
out.erase(0, start);
|
||||
|
||||
// Collapse the space runs left behind by the removals.
|
||||
std::string collapsed;
|
||||
collapsed.reserve(out.size());
|
||||
bool space = false;
|
||||
for (const char c : out) {
|
||||
if (c == ' ') {
|
||||
space = true;
|
||||
continue;
|
||||
}
|
||||
if (space && !collapsed.empty()) collapsed.push_back(' ');
|
||||
space = false;
|
||||
collapsed.push_back(c);
|
||||
}
|
||||
|
||||
// "Austen, Jane" is the same person as "Jane Austen", and publishers use both.
|
||||
// The spelling vote cannot settle it — with one book per author there is no
|
||||
// majority — so the inverted form is turned round here instead. Only a single
|
||||
// comma qualifies: "Smith, John, Jr." and lists of several authors are left
|
||||
// exactly as they are rather than being scrambled.
|
||||
const size_t comma = collapsed.find(',');
|
||||
if (comma != std::string::npos && collapsed.find(',', comma + 1) == std::string::npos) {
|
||||
std::string_view last(collapsed.data(), comma);
|
||||
std::string_view first(collapsed.data() + comma + 1, collapsed.size() - comma - 1);
|
||||
while (!first.empty() && first.front() == ' ') first.remove_prefix(1);
|
||||
while (!last.empty() && last.back() == ' ') last.remove_suffix(1);
|
||||
if (!first.empty() && !last.empty()) {
|
||||
std::string swapped;
|
||||
swapped.reserve(collapsed.size());
|
||||
swapped.append(first);
|
||||
swapped.push_back(' ');
|
||||
swapped.append(last);
|
||||
return swapped;
|
||||
}
|
||||
}
|
||||
return collapsed;
|
||||
}
|
||||
|
||||
std::string authorKey(const std::string_view author) {
|
||||
// Drop bracketed spans ("George Sand [Sand, George]") and everything after
|
||||
// a multi-author separator.
|
||||
std::string cleaned;
|
||||
cleaned.reserve(author.size());
|
||||
int depth = 0;
|
||||
for (const char c : author) {
|
||||
if (c == '[' || c == '(') {
|
||||
depth++;
|
||||
continue;
|
||||
}
|
||||
if (c == ']' || c == ')') {
|
||||
if (depth > 0) depth--;
|
||||
continue;
|
||||
}
|
||||
if (c == ';') break;
|
||||
if (depth == 0) cleaned.push_back(c);
|
||||
}
|
||||
|
||||
const std::string folded = fold(cleaned);
|
||||
|
||||
constexpr size_t MAX_TOKENS = 12;
|
||||
std::string_view tokens[MAX_TOKENS];
|
||||
size_t count = 0;
|
||||
splitTokens(folded, tokens, MAX_TOKENS, count);
|
||||
|
||||
// Initials carry no identity and appear inconsistently ("Herbert G Wells" vs
|
||||
// "Herbert Wells"), so they must not change the key.
|
||||
size_t kept = 0;
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
if (tokens[i].size() > 1) tokens[kept++] = tokens[i];
|
||||
}
|
||||
std::sort(tokens, tokens + kept);
|
||||
|
||||
std::string key;
|
||||
for (size_t i = 0; i < kept; i++) {
|
||||
if (!key.empty()) key.push_back(' ');
|
||||
key.append(tokens[i]);
|
||||
}
|
||||
// Truncate on bytes, not on a token boundary. Sorting puts a short forename
|
||||
// first, so a whole-token cut would reduce "Wollstonecraft, Mary" to the key
|
||||
// "alex" and merge every Alex in the library; the byte cut keeps
|
||||
// "mary wollsto", which stays a prefix of the full key and discriminates.
|
||||
if (key.size() > AUTHOR_KEY_MAX_BYTES) key.resize(AUTHOR_KEY_MAX_BYTES);
|
||||
while (!key.empty() && key.back() == ' ') key.pop_back();
|
||||
return key;
|
||||
}
|
||||
|
||||
// fold() keeps the apostrophe, which is right for sorting — "L'Eneide" belongs
|
||||
// under L. For searching it is wrong: in French the word worth typing is the one
|
||||
// AFTER the apostrophe, so "eneide" must reach "L'Eneide" and "cote" must
|
||||
// reach "d'a cote". Treating it as a word boundary here leaves the sort untouched.
|
||||
bool isWordBreak(const char c) { return c == ' ' || c == '\''; }
|
||||
|
||||
bool matchesQuery(const std::string_view haystack, const std::string_view needle) {
|
||||
if (needle.empty()) return true;
|
||||
|
||||
// Walk the query one word at a time, and for each one scan the book's words for
|
||||
// a prefix hit. Both strings are at most a couple of hundred bytes and this
|
||||
// runs once per book per keypress, so a plain scan is cheaper than anything
|
||||
// that would need building first.
|
||||
size_t qs = 0;
|
||||
while (qs < needle.size()) {
|
||||
while (qs < needle.size() && isWordBreak(needle[qs])) qs++;
|
||||
if (qs >= needle.size()) break;
|
||||
size_t qe = qs;
|
||||
while (qe < needle.size() && !isWordBreak(needle[qe])) qe++;
|
||||
const std::string_view word = needle.substr(qs, qe - qs);
|
||||
|
||||
bool found = false;
|
||||
size_t hs = 0;
|
||||
while (hs < haystack.size() && !found) {
|
||||
while (hs < haystack.size() && isWordBreak(haystack[hs])) hs++;
|
||||
if (hs >= haystack.size()) break;
|
||||
if (haystack.compare(hs, word.size(), word) == 0) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
while (hs < haystack.size() && !isWordBreak(haystack[hs])) hs++;
|
||||
}
|
||||
if (!found) return false;
|
||||
qs = qe;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string surnameKey(const std::string_view displayAuthor) {
|
||||
const std::string folded = fold(displayAuthor);
|
||||
if (folded.empty()) return {};
|
||||
|
||||
size_t lastStart = std::string::npos;
|
||||
size_t lastEnd = folded.size();
|
||||
size_t i = folded.size();
|
||||
while (i > 0) {
|
||||
i--;
|
||||
if (folded[i] != ' ') {
|
||||
if (lastStart == std::string::npos) lastEnd = i + 1;
|
||||
lastStart = i;
|
||||
} else if (lastStart != std::string::npos) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (lastStart == std::string::npos) return {};
|
||||
|
||||
std::string key;
|
||||
key.reserve(folded.size() + 1);
|
||||
key.append(folded, lastStart, lastEnd - lastStart);
|
||||
// The given names follow, so two people sharing a surname stay in a stable,
|
||||
// readable order rather than whichever the disk walk happened to produce.
|
||||
if (lastStart > 0) {
|
||||
key.push_back(' ');
|
||||
key.append(folded, 0, lastStart);
|
||||
while (!key.empty() && key.back() == ' ') key.pop_back();
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
} // namespace library
|
||||
@@ -1,111 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
// Text normalisation for the Library index: the fold used for search and sort
|
||||
// keys, and the author key that merges spellings of one person.
|
||||
//
|
||||
// Nothing here reads a filename. Titles and authors come from the book's own
|
||||
// metadata; a book whose metadata carries no title falls back to its filename
|
||||
// stem, unparsed, and one with no author is grouped under Unknown.
|
||||
//
|
||||
// Pure functions over UTF-8, no hardware and no allocation beyond the returned
|
||||
// strings, so the whole unit is host-testable (test/library_text).
|
||||
//
|
||||
// Design notes that are easy to get wrong and were measured on a real card
|
||||
// (docs/superpowers/specs/2026-08-05-addendum-a-findability.md, A2.1-A2.8):
|
||||
//
|
||||
// * The fold DECOMPOSES. `utf8ComposeNfc()` goes the other way, so a fold
|
||||
// built on it passes on a card holding only decomposed text and then mangles
|
||||
// the first precomposed file to arrive from Windows or Calibre. Both forms
|
||||
// must produce the same output, and the host tests assert exactly that.
|
||||
// * Some letters have no canonical decomposition at all — U+00F8 (ø) is a
|
||||
// distinct letter, not o-with-stroke — so a decompose-only fold turns
|
||||
// "Søren" into "Sren". Those need an explicit map.
|
||||
// * The author KEY sorts its tokens, so for identity purposes name order
|
||||
// stops mattering and no First/Last guess is needed there. Display-side
|
||||
// helpers do use narrow rules — cleanPersonName inverts a single-comma
|
||||
// "Last, First", surnameKey takes the last word — because showing
|
||||
// "Austen, Jane" and "Jane Austen" as two people is worse than the guess.
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace library {
|
||||
|
||||
// Longest author key written into an index record. Sized so a key fits the
|
||||
// record's fixed field; measured to collide 0 times over a 69-book library.
|
||||
inline constexpr size_t AUTHOR_KEY_MAX_BYTES = 12;
|
||||
|
||||
// Casefold and strip diacritics for matching and sorting.
|
||||
//
|
||||
// Maps a handful of letters that have no canonical decomposition, decomposes the
|
||||
// rest and drops combining marks, lowercases ASCII alphanumerics, and turns
|
||||
// everything else into a single space. Space runs collapse and the result is
|
||||
// trimmed. Apostrophes survive as ASCII '\'' so "O'Malley" and "L'\xC3\x89n\xC3\xA9ideé"
|
||||
// keep their shape.
|
||||
//
|
||||
// `stripArticle` additionally removes one leading article ("the ", "le ", "la ",
|
||||
// ...) — correct for sort keys and search text, wrong for anything displayed.
|
||||
std::string fold(std::string_view text, bool stripArticle = false);
|
||||
|
||||
// Tidy a person's name for DISPLAY, without reordering it.
|
||||
//
|
||||
// Drops bracketed spans ("George Sand [Sand, George]"), everything after a
|
||||
// multi-author separator, and the trailing underscores and punctuation that
|
||||
// exporters leave behind ("Lu Xun_", "Herbert G_ Wells"). An underscore
|
||||
// between letters becomes a full stop, since that is what it replaced in a name
|
||||
// a filesystem refused to hold.
|
||||
//
|
||||
// A single-comma "Austen, Jane" IS turned round into "Jane Austen" — with one
|
||||
// book per author the spelling vote has no majority to settle it, so the comma
|
||||
// is the one signal acted on here. Multi-comma names ("Smith, John, Jr.") and
|
||||
// author lists are left exactly as written. Harmonising the several spellings
|
||||
// of one person is done by picking the most common one that actually occurs,
|
||||
// which needs the whole library and so belongs to the index build.
|
||||
std::string cleanPersonName(std::string_view author);
|
||||
|
||||
// Order-insensitive identity for one person, at most AUTHOR_KEY_MAX_BYTES.
|
||||
//
|
||||
// Drops bracketed spans and everything after ';' (multi-author separator), folds,
|
||||
// drops single-character tokens (initials), sorts the remaining tokens and joins
|
||||
// them. "Lu, Xun", "Xun, Lu" and "Lu Xun [Xun, Lu]" all
|
||||
// collapse to one key. Truncation is on BYTES, not a token boundary: the sort
|
||||
// puts a short forename first, so a whole-token cut would reduce
|
||||
// "Wollstonecraft, Mary" to "alex" and merge every Alex in the library; the byte
|
||||
// cut keeps "mary wollsto", still a prefix of the full key.
|
||||
std::string authorKey(std::string_view author);
|
||||
|
||||
// Does a book match what has been typed so far?
|
||||
//
|
||||
// Both sides are already folded — accents stripped, case dropped, punctuation
|
||||
// turned to spaces — so "eneide" finds "L'Énéide" and "eluard" finds
|
||||
// "Éluard". `haystack` is the record's stored fold; `needle` is the query put
|
||||
// through the same fold.
|
||||
//
|
||||
// Every query word must PREFIX some word of the book. That is the rule that fits
|
||||
// the hardware: with no partial refresh, each keypress costs a full ~185 ms panel
|
||||
// repaint, so the reader wants to stop typing as early as possible. "dar mat"
|
||||
// — six keys — finds "Wuthering Heights", where a plain substring test would demand the
|
||||
// whole of one word and give nothing for the effort of a second.
|
||||
//
|
||||
// An empty query matches everything, so the list is the unfiltered shelf before
|
||||
// the first key is pressed.
|
||||
bool matchesQuery(std::string_view haystack, std::string_view needle);
|
||||
|
||||
// Ordering key for a shelf sorted by author: surname first, then the rest.
|
||||
// "Herman Melville" becomes "melville herman", so the shelf reads C where a library
|
||||
// would put it.
|
||||
//
|
||||
// Deliberately NOT the same key as authorKey(). That one sorts a name's words so
|
||||
// that "Victor Hugo" and "Hugo Victor" hash alike and are recognised as one person;
|
||||
// it is a GROUPING key and would be wrong to order by. This is derived from the
|
||||
// DISPLAY name instead, which is safe because the spelling vote has already made
|
||||
// every book by one author show the same name — so a group cannot split across
|
||||
// two places on the shelf.
|
||||
//
|
||||
// The last word is taken as the surname. That is right for the western names on
|
||||
// this card and wrong for some others, which is a limit worth stating rather than
|
||||
// hiding: a single word name simply keys on itself.
|
||||
std::string surnameKey(std::string_view displayAuthor);
|
||||
|
||||
} // namespace library
|
||||
@@ -23,14 +23,6 @@ uint32_t utf8ComposePair(const uint32_t base, const uint32_t mark) {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
uint32_t utf8DecomposedBase(const uint32_t cp) {
|
||||
if (cp < 0x00C0) return 0; // no precomposed Latin letter below this
|
||||
for (const auto& e : kUtf8ComposeTable) {
|
||||
if (e.composed == cp) return e.base;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string utf8ComposeNfc(const std::string& in) {
|
||||
// Fast path: NFC composition can only change text that contains a combining
|
||||
// diacritical mark U+0300-036F (UTF-8 lead byte 0xCC or 0xCD). Plain ASCII and
|
||||
|
||||
@@ -18,18 +18,6 @@ void utf8TruncateChars(std::string& str, size_t numChars);
|
||||
// stored in NFD (e.g. some EPUB chapter titles) otherwise renders broken.
|
||||
std::string utf8ComposeNfc(const std::string& in);
|
||||
|
||||
// The base letter a precomposed codepoint decomposes to, or 0 when there is
|
||||
// none ("é" -> "e", but "ø" -> 0: it is a letter in its own right, not
|
||||
// o-with-stroke). Lives here rather than in a caller because the compose table
|
||||
// is a ~5 KB static array in the header: a second includer is a second copy in
|
||||
// flash.
|
||||
//
|
||||
// A linear scan, unlike utf8ComposePair's binary search above it: the table is
|
||||
// sorted by (base, mark), which this lookup searches against the grain. Adding
|
||||
// a second table sorted by composed would cost more flash than sharing this one
|
||||
// saves.
|
||||
uint32_t utf8DecomposedBase(uint32_t cp);
|
||||
|
||||
// Truncate a raw char buffer to the last complete UTF-8 codepoint boundary.
|
||||
// Returns the new length (<= len). If the buffer ends mid-sequence, the
|
||||
// incomplete trailing bytes are excluded.
|
||||
|
||||
@@ -44,7 +44,6 @@ add_subdirectory(differential_rounding)
|
||||
add_subdirectory(hyphenation_eval)
|
||||
add_subdirectory(utf8_compose)
|
||||
add_subdirectory(opds_filename)
|
||||
add_subdirectory(library_text)
|
||||
add_subdirectory(minibidi_arabic)
|
||||
add_subdirectory(combining_marks)
|
||||
add_subdirectory(credential_integrity)
|
||||
@@ -1,17 +0,0 @@
|
||||
add_executable(LibraryTextTest
|
||||
LibraryTextTest.cpp
|
||||
${REPO_ROOT}/lib/LibraryIndex/LibraryText.cpp
|
||||
${REPO_ROOT}/lib/Utf8/Utf8.cpp
|
||||
)
|
||||
|
||||
target_include_directories(LibraryTextTest PRIVATE
|
||||
${REPO_ROOT}/lib/LibraryIndex
|
||||
${REPO_ROOT}/lib/Utf8
|
||||
)
|
||||
|
||||
target_link_libraries(LibraryTextTest PRIVATE
|
||||
crosspoint_test_common
|
||||
GTest::gtest_main
|
||||
)
|
||||
|
||||
gtest_discover_tests(LibraryTextTest)
|
||||
@@ -1,243 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "LibraryText.h"
|
||||
|
||||
using library::authorKey;
|
||||
using library::fold;
|
||||
|
||||
namespace {
|
||||
|
||||
// The same text in both Unicode normal forms. Every fold test runs both, because
|
||||
// a card holding only one form makes a one-sided test pass by accident — which
|
||||
// is exactly how a fold built on utf8ComposeNfc() survives until the first file
|
||||
// arrives from the other kind of machine.
|
||||
struct NormalisationPair {
|
||||
const char* nfc; // precomposed: e-acute is one codepoint
|
||||
const char* nfd; // decomposed: e followed by a combining acute
|
||||
const char* expected;
|
||||
};
|
||||
|
||||
constexpr NormalisationPair PAIRS[] = {
|
||||
{"pand\xC3\xA9mie", "pande\xCC\x81mie", "pandemie"},
|
||||
{"\xC3\x89"
|
||||
"clipse totale",
|
||||
"E\xCC\x81"
|
||||
"clipse totale",
|
||||
"eclipse totale"},
|
||||
{"M\xC3\xA9moires", "Me\xCC\x81moires", "memoires"},
|
||||
{"Ang\xC3\xA9lina", "Ange\xCC\x81lina", "angelina"},
|
||||
{"R\xC3\xA9"
|
||||
"camier",
|
||||
"Re\xCC\x81"
|
||||
"camier",
|
||||
"recamier"},
|
||||
{"Derri\xC3\xA8re les collines", "Derrie\xCC\x80re les collines", "derriere les collines"},
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(LibraryFold, BothNormalisationsAgree) {
|
||||
for (const auto& p : PAIRS) {
|
||||
EXPECT_EQ(fold(p.nfc), p.expected) << "NFC input: " << p.nfc;
|
||||
EXPECT_EQ(fold(p.nfd), p.expected) << "NFD input: " << p.nfd;
|
||||
EXPECT_EQ(fold(p.nfc), fold(p.nfd)) << "forms disagree for " << p.expected;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(LibraryFold, LettersWithoutCanonicalDecomposition) {
|
||||
// These have no NFD form at all, so a decompose-only fold silently deletes
|
||||
// them. "Søren" losing its last letter is the case that motivated the map.
|
||||
EXPECT_EQ(fold("S\xC3\xB8ren"), "soren");
|
||||
EXPECT_EQ(fold("\xC3\x98rsted"), "orsted");
|
||||
EXPECT_EQ(fold("\xC3\x86"
|
||||
"sop"),
|
||||
"aesop");
|
||||
EXPECT_EQ(fold("Stra\xC3\x9F"
|
||||
"e"),
|
||||
"strasse");
|
||||
EXPECT_EQ(fold("\xC5\x81odz"), "lodz");
|
||||
EXPECT_EQ(fold("s\xC5\x93ur"), "soeur");
|
||||
}
|
||||
|
||||
TEST(LibraryFold, ApostrophesSurviveInNamesAndElisions) {
|
||||
// U+2019 is what exporters actually emit; folding it to a space would split
|
||||
// "O'Malley" into two tokens and change both its sort place and its search.
|
||||
// ("Malley", not "Brien": C++ hex escapes are greedy, so \x99B would parse
|
||||
// as the single escape 0x99B.)
|
||||
EXPECT_EQ(fold("O\xE2\x80\x99Malley"), "o'malley");
|
||||
EXPECT_EQ(fold("O'Malley"), "o'malley");
|
||||
EXPECT_EQ(fold("L\xE2\x80\x99\xC3\x89n\xC3\xA9ide"), "l'eneide");
|
||||
}
|
||||
|
||||
TEST(LibraryFold, TypographicDashesAndQuotesFoldLikeTheirAsciiForms) {
|
||||
// An em dash used to stay inside the fold word while an ASCII hyphen broke
|
||||
// it, so the same title written both ways sorted and searched differently.
|
||||
EXPECT_EQ(library::fold("a\u2014b"), library::fold("a-b"));
|
||||
EXPECT_EQ(library::fold("a\u2013b"), library::fold("a-b"));
|
||||
EXPECT_EQ(library::fold("\u201Cquoted\u201D"), library::fold("\"quoted\""));
|
||||
}
|
||||
|
||||
TEST(LibraryFold, PunctuationSeparatesAndSpaceRunsCollapse) {
|
||||
EXPECT_EQ(fold("Le juge Untel.T2.Le po\xC3\xA8me"), "le juge untel t2 le poeme");
|
||||
EXPECT_EQ(fold(" spaced out "), "spaced out");
|
||||
EXPECT_EQ(fold("a---b"), "a b");
|
||||
EXPECT_EQ(fold("2085 _ Artificial"), "2085 artificial");
|
||||
EXPECT_EQ(fold(""), "");
|
||||
EXPECT_EQ(fold("!!!"), "");
|
||||
}
|
||||
|
||||
TEST(LibraryFold, ArticleStrippingOnlyWhenAsked) {
|
||||
EXPECT_EQ(fold("The Iliad"), "the iliad");
|
||||
EXPECT_EQ(fold("The Iliad", true), "iliad");
|
||||
EXPECT_EQ(fold("Les Mis\xC3\xA9rables", true), "miserables");
|
||||
EXPECT_EQ(fold("L\xE2\x80\x99\xC3\x89n\xC3\xA9ide", true), "eneide");
|
||||
// A title that IS an article-like word must not vanish.
|
||||
EXPECT_EQ(fold("The", true), "the");
|
||||
}
|
||||
|
||||
TEST(LibraryAuthorKey, OrderAndPunctuationDoNotMatter) {
|
||||
const std::string expected = authorKey("Lu Xun");
|
||||
EXPECT_FALSE(expected.empty());
|
||||
for (const char* spelling : {"Lu, Xun", "Xun, Lu", "Lu Xun_", "Lu Xun [Xun, Lu]", " lu xun "}) {
|
||||
EXPECT_EQ(authorKey(spelling), expected) << spelling;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(LibraryAuthorKey, InitialsAreIgnored) {
|
||||
EXPECT_EQ(authorKey("Herbert G Wells"), authorKey("Herbert Wells"));
|
||||
EXPECT_EQ(authorKey("Wells, Herbert G."), authorKey("Herbert Wells"));
|
||||
}
|
||||
|
||||
TEST(LibraryAuthorKey, SecondaryAuthorsAndBracketsDropped) {
|
||||
EXPECT_EQ(authorKey("Emile Erckmann; Alexandre Chatrian"), authorKey("Emile Erckmann"));
|
||||
EXPECT_EQ(authorKey("George Sand [Sand, George]"), authorKey("George Sand"));
|
||||
}
|
||||
|
||||
TEST(LibraryAuthorKey, DistinctPeopleDoNotCollide) {
|
||||
EXPECT_NE(authorKey("Mary Wollstonecraft"), authorKey("Charlotte Bronte"));
|
||||
EXPECT_NE(authorKey("Victor Hugo"), authorKey("Jules Verne"));
|
||||
}
|
||||
|
||||
TEST(LibraryAuthorKey, FitsTheRecordFieldWithoutCollapsingToAForename) {
|
||||
const std::string key = authorKey("Bartholomew Fitzgerald Wellington");
|
||||
ASSERT_FALSE(key.empty());
|
||||
EXPECT_LE(key.size(), library::AUTHOR_KEY_MAX_BYTES);
|
||||
EXPECT_NE(key.back(), ' ');
|
||||
|
||||
// Sorting puts a short forename first, so cutting on a token boundary would
|
||||
// reduce this to "mary" and merge every Alex in the library. The byte cut must
|
||||
// keep enough of the surname to discriminate.
|
||||
const std::string mary = authorKey("Wollstonecraft, Mary");
|
||||
EXPECT_GT(mary.size(), 5u);
|
||||
EXPECT_NE(mary, "mary");
|
||||
EXPECT_NE(mary, authorKey("Mary Trevelyan"));
|
||||
|
||||
// A truncated key stays a prefix of the untruncated one, so grouping is stable
|
||||
// however long the name is.
|
||||
EXPECT_EQ(authorKey("Wollstonecraft, Maryse").rfind("mary", 0), 0u);
|
||||
|
||||
EXPECT_FALSE(authorKey("Nebuchadnezzarson").empty());
|
||||
EXPECT_TRUE(authorKey("").empty());
|
||||
EXPECT_TRUE(authorKey("Q. X. Z.").empty()); // initials only: no identity
|
||||
}
|
||||
|
||||
// --- matchesQuery ------------------------------------------------------------
|
||||
//
|
||||
// Cases taken from the shape of the accented and
|
||||
// apostrophised titles real cards hold — what a naive matcher gets wrong.
|
||||
|
||||
TEST(MatchesQuery, EmptyQueryMatchesEverything) {
|
||||
EXPECT_TRUE(library::matchesQuery(library::fold("Wuthering Heights"), ""));
|
||||
}
|
||||
|
||||
TEST(MatchesQuery, WholeWordMatches) {
|
||||
EXPECT_TRUE(library::matchesQuery(library::fold("Wuthering Heights"), library::fold("heights")));
|
||||
}
|
||||
|
||||
TEST(MatchesQuery, PrefixOfOneWordIsEnough) {
|
||||
EXPECT_TRUE(library::matchesQuery(library::fold("Wuthering Heights"), library::fold("hei")));
|
||||
}
|
||||
|
||||
// The point of the whole design: six keypresses instead of ten, on a panel where
|
||||
// each one costs a full repaint.
|
||||
TEST(MatchesQuery, EveryWordMayBeAbbreviated) {
|
||||
EXPECT_TRUE(library::matchesQuery(library::fold("Wuthering Heights"), library::fold("wut hei")));
|
||||
}
|
||||
|
||||
TEST(MatchesQuery, WordsNeedNotBeInOrder) {
|
||||
EXPECT_TRUE(library::matchesQuery(library::fold("Wuthering Heights"), library::fold("heights wuthering")));
|
||||
}
|
||||
|
||||
TEST(MatchesQuery, EveryWordMustHit) {
|
||||
EXPECT_FALSE(library::matchesQuery(library::fold("Wuthering Heights"), library::fold("wuthering blue")));
|
||||
}
|
||||
|
||||
// A prefix, not a substring: "eights" is inside "heights" but starts no word.
|
||||
TEST(MatchesQuery, MidWordDoesNotMatch) {
|
||||
EXPECT_FALSE(library::matchesQuery(library::fold("Wuthering Heights"), library::fold("eights")));
|
||||
}
|
||||
|
||||
TEST(MatchesQuery, AccentsAreIgnoredOnBothSides) {
|
||||
EXPECT_TRUE(library::matchesQuery(library::fold("L'Énéide"), library::fold("eneide")));
|
||||
EXPECT_TRUE(library::matchesQuery(library::fold("L'Eneide"), library::fold("énéide")));
|
||||
EXPECT_TRUE(library::matchesQuery(library::fold("Éluard"), library::fold("eluard")));
|
||||
}
|
||||
|
||||
TEST(MatchesQuery, ApostropheSplitsWords) {
|
||||
EXPECT_TRUE(library::matchesQuery(library::fold("Le bureau d'à côté"), library::fold("cote")));
|
||||
}
|
||||
|
||||
TEST(MatchesQuery, CaseIsIgnored) {
|
||||
EXPECT_TRUE(library::matchesQuery(library::fold("Wuthering Heights"), library::fold("HEIGHTS")));
|
||||
}
|
||||
|
||||
// The stored fold is capped at 96 bytes, so a query word beyond that cannot be
|
||||
// found. Asserted rather than left implicit: it is the one place a search can
|
||||
// honestly fail to find a book that is really there.
|
||||
TEST(MatchesQuery, LongTitlesAreOnlySearchableWithinTheStoredFold) {
|
||||
const std::string longTitle(120, 'a');
|
||||
const std::string folded = library::fold(longTitle + " needle").substr(0, 96);
|
||||
EXPECT_FALSE(library::matchesQuery(folded, library::fold("needle")));
|
||||
}
|
||||
|
||||
// --- inverted author names ---------------------------------------------------
|
||||
|
||||
TEST(CleanPersonName, InvertedNameIsTurnedRound) {
|
||||
EXPECT_EQ(library::cleanPersonName("Austen, Jane"), "Jane Austen");
|
||||
EXPECT_EQ(library::cleanPersonName("Wollstonecraft, Mary"), "Mary Wollstonecraft");
|
||||
}
|
||||
|
||||
TEST(CleanPersonName, PlainNameIsUntouched) { EXPECT_EQ(library::cleanPersonName("Emily Bronte"), "Emily Bronte"); }
|
||||
|
||||
// Two commas mean a suffix or a list, not an inversion — leave it alone rather
|
||||
// than scramble it.
|
||||
TEST(CleanPersonName, MultipleCommasAreLeftAlone) {
|
||||
// The trailing full stop is stripped by the existing noise rules.
|
||||
EXPECT_EQ(library::cleanPersonName("Smith, John, Jr."), "Smith, John, Jr");
|
||||
}
|
||||
|
||||
TEST(CleanPersonName, DanglingCommaIsNotAnInversion) { EXPECT_EQ(library::cleanPersonName("Austen,"), "Austen"); }
|
||||
|
||||
// --- surnameKey --------------------------------------------------------------
|
||||
|
||||
TEST(SurnameKey, SurnameLeadsThenGivenNames) {
|
||||
EXPECT_EQ(library::surnameKey("Herman Melville"), "melville herman");
|
||||
EXPECT_EQ(library::surnameKey("Mary Wollstonecraft"), "wollstonecraft mary");
|
||||
}
|
||||
|
||||
TEST(SurnameKey, SingleWordKeysOnItself) { EXPECT_EQ(library::surnameKey("Voltaire"), "voltaire"); }
|
||||
|
||||
TEST(SurnameKey, AccentsAreFolded) { EXPECT_EQ(library::surnameKey("Paul Éluard"), "eluard paul"); }
|
||||
|
||||
TEST(SurnameKey, ThreeWordNamesTakeTheLast) { EXPECT_EQ(library::surnameKey("Herbert G. Wells"), "wells herbert g"); }
|
||||
|
||||
TEST(SurnameKey, EmptyStaysEmpty) { EXPECT_EQ(library::surnameKey(""), ""); }
|
||||
|
||||
// The whole point of keying off the DISPLAY name: the spelling vote has already
|
||||
// made every book by one author show one name, so a group cannot land in two
|
||||
// places even though "Victor Hugo" and "Hugo Victor" both exist in the wild.
|
||||
TEST(SurnameKey, HarmonisedDisplayNameKeepsAGroupTogether) {
|
||||
EXPECT_NE(library::surnameKey("Victor Hugo"), library::surnameKey("Hugo Victor"));
|
||||
}
|
||||
Reference in new issue
Block a user