shoko 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.bundle/config +4 -0
- data/.bundle/config.bak +3 -0
- data/.rspec_status +42 -0
- data/.rubocop.yml +124 -0
- data/Gemfile +19 -0
- data/LICENSE +21 -0
- data/README.md +82 -0
- data/Rakefile +29 -0
- data/bin/start +15 -0
- data/lib/shoko/adapters/book_sources/document_service.rb +201 -0
- data/lib/shoko/adapters/book_sources/download_service.rb +95 -0
- data/lib/shoko/adapters/book_sources/epub/epub_resource_loader.rb +137 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/html_processor.rb +151 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/metadata_extractor.rb +53 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/entry_reader.rb +77 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/metadata_extractor.rb +67 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_context.rb +86 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_document_index.rb +75 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_document_scanner.rb +47 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_extractor.rb +46 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_label_resolver.rb +83 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_list_item.rb +55 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_result.rb +8 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_selector.rb +100 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_source_locator.rb +93 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_traversal.rb +103 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_walker.rb +56 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/opf_processor.rb +102 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/xhtml_content_parser.rb +661 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/xml_text_normalizer.rb +41 -0
- data/lib/shoko/adapters/book_sources/epub_document.rb +253 -0
- data/lib/shoko/adapters/book_sources/epub_finder/directory_scanner.rb +134 -0
- data/lib/shoko/adapters/book_sources/epub_finder/scanner_context.rb +28 -0
- data/lib/shoko/adapters/book_sources/epub_finder.rb +161 -0
- data/lib/shoko/adapters/book_sources/epub_importer.rb +268 -0
- data/lib/shoko/adapters/book_sources/gutendex_client.rb +150 -0
- data/lib/shoko/adapters/book_sources/library_scanner.rb +93 -0
- data/lib/shoko/adapters/book_sources/source_fingerprint.rb +57 -0
- data/lib/shoko/adapters/input/annotations/mouse_handler.rb +84 -0
- data/lib/shoko/adapters/input/command_bridge.rb +148 -0
- data/lib/shoko/adapters/input/command_factory.rb +255 -0
- data/lib/shoko/adapters/input/commands.rb +60 -0
- data/lib/shoko/adapters/input/dispatcher.rb +69 -0
- data/lib/shoko/adapters/input/input_controller.rb +250 -0
- data/lib/shoko/adapters/input/key_definitions.rb +108 -0
- data/lib/shoko/adapters/input/validators/file_path_validator.rb +81 -0
- data/lib/shoko/adapters/input/validators/terminal_size_validator.rb +76 -0
- data/lib/shoko/adapters/monitoring/logger.rb +150 -0
- data/lib/shoko/adapters/monitoring/perf_tracer.rb +183 -0
- data/lib/shoko/adapters/monitoring/performance_monitor.rb +110 -0
- data/lib/shoko/adapters/output/clipboard/clipboard_service.rb +125 -0
- data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler/image_builder.rb +149 -0
- data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler/text_wrapper.rb +149 -0
- data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler/tokenizer.rb +91 -0
- data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler.rb +144 -0
- data/lib/shoko/adapters/output/formatting/formatting_service/plain_lines_builder.rb +54 -0
- data/lib/shoko/adapters/output/formatting/formatting_service.rb +247 -0
- data/lib/shoko/adapters/output/formatting/wrapping_service.rb +228 -0
- data/lib/shoko/adapters/output/instrumentation_service.rb +52 -0
- data/lib/shoko/adapters/output/kitty/image_transcoder.rb +71 -0
- data/lib/shoko/adapters/output/kitty/kitty_graphics.rb +114 -0
- data/lib/shoko/adapters/output/kitty/kitty_image_renderer.rb +239 -0
- data/lib/shoko/adapters/output/kitty/kitty_unicode_placeholders.rb +139 -0
- data/lib/shoko/adapters/output/kitty/kitty_unicode_placeholders_diacritic_codepoints.txt +26 -0
- data/lib/shoko/adapters/output/notification_service.rb +58 -0
- data/lib/shoko/adapters/output/render_registry.rb +45 -0
- data/lib/shoko/adapters/output/rendering/models/line_geometry.rb +60 -0
- data/lib/shoko/adapters/output/rendering/models/page_rendering_context.rb +22 -0
- data/lib/shoko/adapters/output/rendering/models/render_params.rb +28 -0
- data/lib/shoko/adapters/output/rendering/models/rendering_context.rb +58 -0
- data/lib/shoko/adapters/output/terminal/buffer.rb +275 -0
- data/lib/shoko/adapters/output/terminal/constants/terminal_defaults.rb +11 -0
- data/lib/shoko/adapters/output/terminal/input/decoder.rb +347 -0
- data/lib/shoko/adapters/output/terminal/input.rb +161 -0
- data/lib/shoko/adapters/output/terminal/output.rb +105 -0
- data/lib/shoko/adapters/output/terminal/terminal.rb +167 -0
- data/lib/shoko/adapters/output/terminal/terminal_sanitizer.rb +243 -0
- data/lib/shoko/adapters/output/terminal/terminal_service.rb +138 -0
- data/lib/shoko/adapters/output/terminal/text_metrics.rb +273 -0
- data/lib/shoko/adapters/output/ui/builders/page_setup_builder.rb +47 -0
- data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay/footer_renderer.rb +80 -0
- data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay/geometry.rb +61 -0
- data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay/note_renderer.rb +86 -0
- data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay_component.rb +234 -0
- data/lib/shoko/adapters/output/ui/components/annotations_overlay/list_renderer.rb +142 -0
- data/lib/shoko/adapters/output/ui/components/annotations_overlay_component.rb +185 -0
- data/lib/shoko/adapters/output/ui/components/base_component.rb +110 -0
- data/lib/shoko/adapters/output/ui/components/component_interface.rb +80 -0
- data/lib/shoko/adapters/output/ui/components/content_component.rb +61 -0
- data/lib/shoko/adapters/output/ui/components/enhanced_popup_menu.rb +191 -0
- data/lib/shoko/adapters/output/ui/components/footer_component.rb +120 -0
- data/lib/shoko/adapters/output/ui/components/header_component.rb +46 -0
- data/lib/shoko/adapters/output/ui/components/layouts/horizontal.rb +63 -0
- data/lib/shoko/adapters/output/ui/components/layouts/vertical.rb +73 -0
- data/lib/shoko/adapters/output/ui/components/main_menu_component.rb +103 -0
- data/lib/shoko/adapters/output/ui/components/reading/base_view_renderer.rb +199 -0
- data/lib/shoko/adapters/output/ui/components/reading/config_helpers.rb +42 -0
- data/lib/shoko/adapters/output/ui/components/reading/help_renderer.rb +62 -0
- data/lib/shoko/adapters/output/ui/components/reading/inline_segment_highlighter.rb +144 -0
- data/lib/shoko/adapters/output/ui/components/reading/kitty_image_line_renderer.rb +262 -0
- data/lib/shoko/adapters/output/ui/components/reading/line_content_composer.rb +114 -0
- data/lib/shoko/adapters/output/ui/components/reading/line_drawer.rb +87 -0
- data/lib/shoko/adapters/output/ui/components/reading/line_geometry_builder.rb +41 -0
- data/lib/shoko/adapters/output/ui/components/reading/rendered_lines_recorder.rb +64 -0
- data/lib/shoko/adapters/output/ui/components/reading/single_view_renderer.rb +156 -0
- data/lib/shoko/adapters/output/ui/components/reading/split_view_renderer.rb +221 -0
- data/lib/shoko/adapters/output/ui/components/reading/view_renderer_factory.rb +20 -0
- data/lib/shoko/adapters/output/ui/components/reading/wrapped_lines_fetcher.rb +139 -0
- data/lib/shoko/adapters/output/ui/components/rect.rb +15 -0
- data/lib/shoko/adapters/output/ui/components/render_style.rb +84 -0
- data/lib/shoko/adapters/output/ui/components/screen_component.rb +24 -0
- data/lib/shoko/adapters/output/ui/components/screens/annotation_detail_screen_component.rb +175 -0
- data/lib/shoko/adapters/output/ui/components/screens/annotation_edit_screen_component.rb +221 -0
- data/lib/shoko/adapters/output/ui/components/screens/annotation_editor_screen_component.rb +205 -0
- data/lib/shoko/adapters/output/ui/components/screens/annotation_rendering_helpers.rb +190 -0
- data/lib/shoko/adapters/output/ui/components/screens/annotations_screen_component.rb +266 -0
- data/lib/shoko/adapters/output/ui/components/screens/base_screen_component.rb +49 -0
- data/lib/shoko/adapters/output/ui/components/screens/browse_screen_component.rb +319 -0
- data/lib/shoko/adapters/output/ui/components/screens/download_books_screen_component.rb +340 -0
- data/lib/shoko/adapters/output/ui/components/screens/library_screen_component.rb +205 -0
- data/lib/shoko/adapters/output/ui/components/screens/loading_overlay_component.rb +49 -0
- data/lib/shoko/adapters/output/ui/components/screens/menu_screen_component.rb +107 -0
- data/lib/shoko/adapters/output/ui/components/screens/settings_screen_component.rb +238 -0
- data/lib/shoko/adapters/output/ui/components/sidebar/annotations_tab_renderer.rb +159 -0
- data/lib/shoko/adapters/output/ui/components/sidebar/bookmarks_tab_renderer.rb +139 -0
- data/lib/shoko/adapters/output/ui/components/sidebar/tab_header_component.rb +157 -0
- data/lib/shoko/adapters/output/ui/components/sidebar/toc_tab_renderer.rb +111 -0
- data/lib/shoko/adapters/output/ui/components/sidebar/toc_tab_support.rb +1606 -0
- data/lib/shoko/adapters/output/ui/components/sidebar_panel_component.rb +217 -0
- data/lib/shoko/adapters/output/ui/components/surface.rb +88 -0
- data/lib/shoko/adapters/output/ui/components/tooltip_overlay_component.rb +224 -0
- data/lib/shoko/adapters/output/ui/components/ui/box_drawer.rb +32 -0
- data/lib/shoko/adapters/output/ui/components/ui/list_helpers.rb +33 -0
- data/lib/shoko/adapters/output/ui/components/ui/overlay_layout.rb +79 -0
- data/lib/shoko/adapters/output/ui/components/ui/text_utils.rb +46 -0
- data/lib/shoko/adapters/output/ui/constants/highlighting.rb +21 -0
- data/lib/shoko/adapters/output/ui/constants/messages.rb +12 -0
- data/lib/shoko/adapters/output/ui/constants/themes.rb +79 -0
- data/lib/shoko/adapters/output/ui/constants/ui_constants.rb +85 -0
- data/lib/shoko/adapters/output/ui/rendering/frame_coordinator.rb +42 -0
- data/lib/shoko/adapters/output/ui/rendering/reader_render_coordinator.rb +169 -0
- data/lib/shoko/adapters/output/ui/rendering/render_pipeline.rb +55 -0
- data/lib/shoko/adapters/storage/atomic_file_writer.rb +43 -0
- data/lib/shoko/adapters/storage/background_worker.rb +66 -0
- data/lib/shoko/adapters/storage/book_cache_pipeline.rb +653 -0
- data/lib/shoko/adapters/storage/cache/epub/memory_cache.rb +99 -0
- data/lib/shoko/adapters/storage/cache/epub/persistence.rb +131 -0
- data/lib/shoko/adapters/storage/cache/epub/serializer/deserialize.rb +225 -0
- data/lib/shoko/adapters/storage/cache/epub/serializer/helpers.rb +63 -0
- data/lib/shoko/adapters/storage/cache/epub/serializer/serialize.rb +83 -0
- data/lib/shoko/adapters/storage/cache/epub/serializer.rb +5 -0
- data/lib/shoko/adapters/storage/cache/epub/source_reference.rb +58 -0
- data/lib/shoko/adapters/storage/cache_paths.rb +21 -0
- data/lib/shoko/adapters/storage/cache_pointer_manager.rb +60 -0
- data/lib/shoko/adapters/storage/config_paths.rb +30 -0
- data/lib/shoko/adapters/storage/epub_cache.rb +195 -0
- data/lib/shoko/adapters/storage/file_writer_service.rb +47 -0
- data/lib/shoko/adapters/storage/json_cache_store/chapters.rb +141 -0
- data/lib/shoko/adapters/storage/json_cache_store/layouts.rb +67 -0
- data/lib/shoko/adapters/storage/json_cache_store/manifest.rb +42 -0
- data/lib/shoko/adapters/storage/json_cache_store/payload_helpers.rb +113 -0
- data/lib/shoko/adapters/storage/json_cache_store/resources.rb +84 -0
- data/lib/shoko/adapters/storage/json_cache_store.rb +167 -0
- data/lib/shoko/adapters/storage/lazy_file_string.rb +65 -0
- data/lib/shoko/adapters/storage/pagination_cache.rb +127 -0
- data/lib/shoko/adapters/storage/recent_files.rb +78 -0
- data/lib/shoko/adapters/storage/repositories/annotation_repository.rb +182 -0
- data/lib/shoko/adapters/storage/repositories/base_repository.rb +81 -0
- data/lib/shoko/adapters/storage/repositories/bookmark_repository.rb +132 -0
- data/lib/shoko/adapters/storage/repositories/cached_library_repository.rb +129 -0
- data/lib/shoko/adapters/storage/repositories/config_repository.rb +262 -0
- data/lib/shoko/adapters/storage/repositories/progress_repository.rb +166 -0
- data/lib/shoko/adapters/storage/repositories/storage/annotation_file_store.rb +128 -0
- data/lib/shoko/adapters/storage/repositories/storage/bookmark_file_store.rb +109 -0
- data/lib/shoko/adapters/storage/repositories/storage/file_store_utils.rb +20 -0
- data/lib/shoko/adapters/storage/repositories/storage/progress_file_store.rb +59 -0
- data/lib/shoko/application/annotation_editor_overlay_session.rb +138 -0
- data/lib/shoko/application/cli.rb +134 -0
- data/lib/shoko/application/controllers/menu/input_controller.rb +189 -0
- data/lib/shoko/application/controllers/menu/state_controller.rb +642 -0
- data/lib/shoko/application/controllers/menu_controller.rb +469 -0
- data/lib/shoko/application/controllers/mouseable_reader.rb +377 -0
- data/lib/shoko/application/controllers/reader_controller.rb +449 -0
- data/lib/shoko/application/controllers/state_controller.rb +410 -0
- data/lib/shoko/application/controllers/ui_controller.rb +782 -0
- data/lib/shoko/application/dependency_container.rb +301 -0
- data/lib/shoko/application/infrastructure/event_bus.rb +80 -0
- data/lib/shoko/application/infrastructure/observer_state_store.rb +136 -0
- data/lib/shoko/application/infrastructure/state_store.rb +413 -0
- data/lib/shoko/application/main_menu/menu_progress_presenter.rb +83 -0
- data/lib/shoko/application/pending_jump_handler.rb +122 -0
- data/lib/shoko/application/reader_lifecycle.rb +65 -0
- data/lib/shoko/application/reader_startup_orchestrator.rb +113 -0
- data/lib/shoko/application/selectors/config_selectors.rb +62 -0
- data/lib/shoko/application/selectors/menu_selectors.rb +62 -0
- data/lib/shoko/application/selectors/reader_selectors.rb +186 -0
- data/lib/shoko/application/state/actions/base_action.rb +24 -0
- data/lib/shoko/application/state/actions/quit_to_menu_action.rb +16 -0
- data/lib/shoko/application/state/actions/switch_reader_mode_action.rb +22 -0
- data/lib/shoko/application/state/actions/toggle_view_mode_action.rb +31 -0
- data/lib/shoko/application/state/actions/update_annotation_editor_overlay_action.rb +27 -0
- data/lib/shoko/application/state/actions/update_annotations_action.rb +20 -0
- data/lib/shoko/application/state/actions/update_annotations_overlay_action.rb +27 -0
- data/lib/shoko/application/state/actions/update_bookmarks_action.rb +20 -0
- data/lib/shoko/application/state/actions/update_chapter_action.rb +24 -0
- data/lib/shoko/application/state/actions/update_config_action.rb +22 -0
- data/lib/shoko/application/state/actions/update_field_helpers.rb +26 -0
- data/lib/shoko/application/state/actions/update_menu_action.rb +21 -0
- data/lib/shoko/application/state/actions/update_message_action.rb +35 -0
- data/lib/shoko/application/state/actions/update_page_action.rb +21 -0
- data/lib/shoko/application/state/actions/update_pagination_state_action.rb +21 -0
- data/lib/shoko/application/state/actions/update_popup_menu_action.rb +27 -0
- data/lib/shoko/application/state/actions/update_reader_meta_action.rb +21 -0
- data/lib/shoko/application/state/actions/update_reader_mode_action.rb +20 -0
- data/lib/shoko/application/state/actions/update_rendered_lines_action.rb +40 -0
- data/lib/shoko/application/state/actions/update_selection_action.rb +27 -0
- data/lib/shoko/application/state/actions/update_selections_action.rb +21 -0
- data/lib/shoko/application/state/actions/update_sidebar_action.rb +34 -0
- data/lib/shoko/application/state/actions/update_ui_loading_action.rb +23 -0
- data/lib/shoko/application/ui/reader_view_model_builder.rb +74 -0
- data/lib/shoko/application/ui/view_models/reader_view_model.rb +177 -0
- data/lib/shoko/application/unified_application.rb +48 -0
- data/lib/shoko/application/use_cases/catalog_service.rb +117 -0
- data/lib/shoko/application/use_cases/commands/annotation_editor_commands.rb +105 -0
- data/lib/shoko/application/use_cases/commands/application_commands.rb +208 -0
- data/lib/shoko/application/use_cases/commands/base_command.rb +166 -0
- data/lib/shoko/application/use_cases/commands/bookmark_commands.rb +114 -0
- data/lib/shoko/application/use_cases/commands/conditional_navigation_commands.rb +57 -0
- data/lib/shoko/application/use_cases/commands/menu_commands.rb +170 -0
- data/lib/shoko/application/use_cases/commands/navigation_commands.rb +183 -0
- data/lib/shoko/application/use_cases/commands/reader_commands.rb +46 -0
- data/lib/shoko/application/use_cases/commands/sidebar_commands.rb +55 -0
- data/lib/shoko/application/use_cases/settings_service.rb +123 -0
- data/lib/shoko/core/events/annotation_events.rb +94 -0
- data/lib/shoko/core/events/base_domain_event.rb +169 -0
- data/lib/shoko/core/events/bookmark_events.rb +41 -0
- data/lib/shoko/core/events/domain_event_bus.rb +163 -0
- data/lib/shoko/core/events/progress_events.rb +108 -0
- data/lib/shoko/core/models/bookmark.rb +36 -0
- data/lib/shoko/core/models/bookmark_data.rb +10 -0
- data/lib/shoko/core/models/chapter.rb +25 -0
- data/lib/shoko/core/models/content_block.rb +44 -0
- data/lib/shoko/core/models/reader_settings.rb +20 -0
- data/lib/shoko/core/models/selection_anchor.rb +73 -0
- data/lib/shoko/core/models/toc_entry.rb +14 -0
- data/lib/shoko/core/ports/annotation_repository.rb +0 -0
- data/lib/shoko/core/ports/book_repository.rb +0 -0
- data/lib/shoko/core/ports/book_source.rb +0 -0
- data/lib/shoko/core/ports/bookmark_repository.rb +0 -0
- data/lib/shoko/core/ports/cache.rb +0 -0
- data/lib/shoko/core/ports/input_handler.rb +0 -0
- data/lib/shoko/core/ports/renderer.rb +0 -0
- data/lib/shoko/core/ports/storage.rb +0 -0
- data/lib/shoko/core/services/annotation_service.rb +102 -0
- data/lib/shoko/core/services/base_service.rb +60 -0
- data/lib/shoko/core/services/bookmark_service.rb +267 -0
- data/lib/shoko/core/services/coordinate_service.rb +265 -0
- data/lib/shoko/core/services/layout_service.rb +95 -0
- data/lib/shoko/core/services/navigation/absolute_change_applier.rb +96 -0
- data/lib/shoko/core/services/navigation/absolute_layout.rb +101 -0
- data/lib/shoko/core/services/navigation/absolute_strategy.rb +179 -0
- data/lib/shoko/core/services/navigation/context_builder.rb +52 -0
- data/lib/shoko/core/services/navigation/context_helpers.rb +63 -0
- data/lib/shoko/core/services/navigation/dynamic_change_applier.rb +50 -0
- data/lib/shoko/core/services/navigation/dynamic_strategy.rb +51 -0
- data/lib/shoko/core/services/navigation/image_offset_snapper.rb +150 -0
- data/lib/shoko/core/services/navigation/nav_context.rb +27 -0
- data/lib/shoko/core/services/navigation/state_updater.rb +29 -0
- data/lib/shoko/core/services/navigation/strategy_factory.rb +20 -0
- data/lib/shoko/core/services/navigation_service.rb +150 -0
- data/lib/shoko/core/services/page_calculator_service.rb +242 -0
- data/lib/shoko/core/services/pagination/internal/absolute_page_map_builder.rb +28 -0
- data/lib/shoko/core/services/pagination/internal/chapter_cache.rb +60 -0
- data/lib/shoko/core/services/pagination/internal/dynamic_page_map_builder.rb +157 -0
- data/lib/shoko/core/services/pagination/internal/layout_metrics_calculator.rb +73 -0
- data/lib/shoko/core/services/pagination/internal/page_hydrator.rb +145 -0
- data/lib/shoko/core/services/pagination/internal/pagination_workflow.rb +152 -0
- data/lib/shoko/core/services/pagination/page_info_calculator.rb +247 -0
- data/lib/shoko/core/services/pagination/pagination_cache_preloader.rb +173 -0
- data/lib/shoko/core/services/pagination/pagination_coordinator.rb +202 -0
- data/lib/shoko/core/services/pagination/pagination_orchestrator.rb +291 -0
- data/lib/shoko/core/services/pagination.rb +10 -0
- data/lib/shoko/core/services/progress_helper.rb +22 -0
- data/lib/shoko/core/services/selection_service.rb +126 -0
- data/lib/shoko/core/validator.rb +76 -0
- data/lib/shoko/shared/errors.rb +97 -0
- data/lib/shoko/shared/version.rb +5 -0
- data/lib/shoko/test_support/terminal_double.rb +175 -0
- data/lib/shoko/test_support/test_mode.rb +78 -0
- data/lib/shoko.rb +279 -0
- data/lib/zip.rb +732 -0
- data/zip.rb +5 -0
- metadata +370 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'io/console'
|
|
4
|
+
require_relative 'constants/terminal_defaults'
|
|
5
|
+
require_relative 'input/decoder'
|
|
6
|
+
|
|
7
|
+
module Shoko
|
|
8
|
+
module Adapters::Output::Terminal
|
|
9
|
+
# TerminalInput encapsulates input reading, console modes, and size queries.
|
|
10
|
+
class TerminalInput
|
|
11
|
+
SIZE_CACHE_INTERVAL = 0.5
|
|
12
|
+
READ_CHUNK_BYTES = 4096
|
|
13
|
+
|
|
14
|
+
def initialize(input: $stdin, output: $stdout, esc_timeout: Decoder::DEFAULT_ESC_TIMEOUT,
|
|
15
|
+
sequence_timeout: Decoder::DEFAULT_SEQUENCE_TIMEOUT)
|
|
16
|
+
@console = nil
|
|
17
|
+
@size_cache = { width: nil, height: nil, checked_at: nil }
|
|
18
|
+
@input = input
|
|
19
|
+
@output = output
|
|
20
|
+
@decoder = Decoder.new(esc_timeout: esc_timeout, sequence_timeout: sequence_timeout)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def size
|
|
24
|
+
update_size_cache if cache_expired?
|
|
25
|
+
[@size_cache[:height], @size_cache[:width]]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def setup_console
|
|
29
|
+
$stdout.sync = true
|
|
30
|
+
@console = resolve_console
|
|
31
|
+
@console.raw! if @console.respond_to?(:raw!)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def cleanup_console
|
|
35
|
+
@console.cooked! if @console.respond_to?(:cooked!)
|
|
36
|
+
@console = nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def with_raw_console(&)
|
|
40
|
+
console = resolve_console
|
|
41
|
+
console.raw(&)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def read_key
|
|
45
|
+
with_raw_console do
|
|
46
|
+
pump_input
|
|
47
|
+
@decoder.next_token(now: monotonic_now)
|
|
48
|
+
end
|
|
49
|
+
rescue IO::WaitReadable, EOFError
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def read_key_blocking(timeout: nil)
|
|
54
|
+
deadline = timeout ? monotonic_now + timeout.to_f : nil
|
|
55
|
+
loop do
|
|
56
|
+
key = read_key
|
|
57
|
+
return key if key
|
|
58
|
+
|
|
59
|
+
now = monotonic_now
|
|
60
|
+
remaining = deadline ? (deadline - now) : nil
|
|
61
|
+
return nil if remaining && remaining <= 0
|
|
62
|
+
|
|
63
|
+
pending = @decoder.pending_timeout(now: now)
|
|
64
|
+
wait = if pending && remaining
|
|
65
|
+
[pending, remaining].min
|
|
66
|
+
else
|
|
67
|
+
pending || remaining
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
if wait
|
|
71
|
+
next if wait <= 0
|
|
72
|
+
|
|
73
|
+
@input.wait_readable(wait)
|
|
74
|
+
else
|
|
75
|
+
@input.wait_readable
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Mouse support
|
|
81
|
+
def enable_mouse
|
|
82
|
+
@output.print "\e[?1002h\e[?1006h"
|
|
83
|
+
@output.flush
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def disable_mouse
|
|
87
|
+
@output.print "\e[?1002l\e[?1006l"
|
|
88
|
+
@output.flush
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def read_input_with_mouse(timeout: nil)
|
|
92
|
+
read_key_blocking(timeout: timeout)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def setup_signal_handlers(&cleanup_callback)
|
|
96
|
+
%w[INT TERM].each do |signal|
|
|
97
|
+
trap(signal) do
|
|
98
|
+
cleanup_callback&.call
|
|
99
|
+
exit(0)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def cache_expired?
|
|
107
|
+
now = Time.now
|
|
108
|
+
checked = @size_cache[:checked_at]
|
|
109
|
+
checked.nil? || now - checked > SIZE_CACHE_INTERVAL
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def update_size_cache
|
|
113
|
+
h, w = fetch_terminal_size
|
|
114
|
+
@size_cache = { width: w, height: h, checked_at: Time.now }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def fetch_terminal_size
|
|
118
|
+
IO.console.winsize
|
|
119
|
+
rescue StandardError
|
|
120
|
+
default_dimensions
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def default_dimensions
|
|
124
|
+
[Adapters::Output::Terminal::TerminalDefaults::DEFAULT_ROWS,
|
|
125
|
+
Adapters::Output::Terminal::TerminalDefaults::DEFAULT_COLUMNS]
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def resolve_console
|
|
129
|
+
return @console if @console
|
|
130
|
+
|
|
131
|
+
console = IO.console
|
|
132
|
+
if console
|
|
133
|
+
@console = console
|
|
134
|
+
return console
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
if @input.respond_to?(:tty?) && @input.tty? && @input.respond_to?(:raw)
|
|
138
|
+
@console = @input
|
|
139
|
+
return @input
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
raise Shoko::TerminalUnavailableError
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def pump_input
|
|
146
|
+
loop do
|
|
147
|
+
chunk = @input.read_nonblock(READ_CHUNK_BYTES)
|
|
148
|
+
@decoder.feed(chunk)
|
|
149
|
+
end
|
|
150
|
+
rescue IO::WaitReadable, EOFError
|
|
151
|
+
nil
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def monotonic_now
|
|
155
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
156
|
+
rescue StandardError
|
|
157
|
+
Time.now.to_f
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'io/console'
|
|
4
|
+
|
|
5
|
+
module Shoko
|
|
6
|
+
module Adapters::Output::Terminal
|
|
7
|
+
# TerminalOutput handles ANSI sequences and direct writes to an IO stream.
|
|
8
|
+
class TerminalOutput
|
|
9
|
+
attr_reader :io
|
|
10
|
+
|
|
11
|
+
def initialize(io = $stdout)
|
|
12
|
+
@io = io
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# A collection of ANSI escape codes and helpers
|
|
16
|
+
module ANSI
|
|
17
|
+
RESET = "\e[0m"
|
|
18
|
+
BOLD = "\e[1m"
|
|
19
|
+
DIM = "\e[2m"
|
|
20
|
+
ITALIC = "\e[3m"
|
|
21
|
+
REVERSE = "\e[7m"
|
|
22
|
+
|
|
23
|
+
BLACK = "\e[30m"
|
|
24
|
+
RED = "\e[31m"
|
|
25
|
+
GREEN = "\e[32m"
|
|
26
|
+
YELLOW = "\e[33m"
|
|
27
|
+
BLUE = "\e[34m"
|
|
28
|
+
MAGENTA = "\e[35m"
|
|
29
|
+
CYAN = "\e[36m"
|
|
30
|
+
WHITE = "\e[37m"
|
|
31
|
+
GRAY = "\e[90m"
|
|
32
|
+
LIGHT_GREY = "\e[37;1m"
|
|
33
|
+
|
|
34
|
+
BRIGHT_RED = "\e[91m"
|
|
35
|
+
BRIGHT_GREEN = "\e[92m"
|
|
36
|
+
BRIGHT_YELLOW = "\e[93m"
|
|
37
|
+
BRIGHT_BLUE = "\e[94m"
|
|
38
|
+
BRIGHT_MAGENTA = "\e[95m"
|
|
39
|
+
BRIGHT_CYAN = "\e[96m"
|
|
40
|
+
BRIGHT_WHITE = "\e[97m"
|
|
41
|
+
|
|
42
|
+
BG_DARK = "\e[48;5;236m"
|
|
43
|
+
BG_BLACK = "\e[40m"
|
|
44
|
+
BG_BLUE = "\e[44m"
|
|
45
|
+
BG_CYAN = "\e[46m"
|
|
46
|
+
BG_GREY = "\e[48;5;240m"
|
|
47
|
+
BG_SLATE = "\e[48;5;238m"
|
|
48
|
+
BG_SOFT_GREEN = "\e[48;5;65m"
|
|
49
|
+
BG_BRIGHT_GREEN = "\e[102m"
|
|
50
|
+
BG_BRIGHT_YELLOW = "\e[103m"
|
|
51
|
+
BG_BRIGHT_WHITE = "\e[107m"
|
|
52
|
+
|
|
53
|
+
module Control
|
|
54
|
+
CLEAR = "\e[2J"
|
|
55
|
+
HOME = "\e[H"
|
|
56
|
+
HIDE_CURSOR = "\e[?25l"
|
|
57
|
+
SHOW_CURSOR = "\e[?25h"
|
|
58
|
+
SAVE_SCREEN = "\e[?1049h"
|
|
59
|
+
RESTORE_SCREEN = "\e[?1049l"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.move(row, col)
|
|
63
|
+
"\e[#{row};#{col}H"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.clear_line
|
|
67
|
+
"\e[2K"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.clear_below
|
|
71
|
+
"\e[J"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def print(str)
|
|
76
|
+
io.print(str)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def flush
|
|
80
|
+
io.flush
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def clear
|
|
84
|
+
print(ANSI::Control::CLEAR + ANSI::Control::HOME)
|
|
85
|
+
flush
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def hide_cursor
|
|
89
|
+
print(ANSI::Control::HIDE_CURSOR)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def show_cursor
|
|
93
|
+
print(ANSI::Control::SHOW_CURSOR)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def save_screen
|
|
97
|
+
print(ANSI::Control::SAVE_SCREEN)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def restore_screen
|
|
101
|
+
print(ANSI::Control::RESTORE_SCREEN)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'io/console'
|
|
4
|
+
require_relative 'constants/terminal_defaults'
|
|
5
|
+
require_relative 'output'
|
|
6
|
+
require_relative 'buffer'
|
|
7
|
+
require_relative 'input'
|
|
8
|
+
|
|
9
|
+
module Shoko
|
|
10
|
+
module Adapters::Output::Terminal
|
|
11
|
+
# Facade that preserves the historical Terminal API
|
|
12
|
+
# while delegating to composable, testable components:
|
|
13
|
+
# - TerminalOutput
|
|
14
|
+
# - TerminalBuffer
|
|
15
|
+
# - TerminalInput
|
|
16
|
+
class Terminal
|
|
17
|
+
# Keep ANSI nested under Terminal for compatibility
|
|
18
|
+
ANSI = TerminalOutput::ANSI
|
|
19
|
+
|
|
20
|
+
# Defines constants for special keyboard keys to abstract away different
|
|
21
|
+
# terminal escape codes.
|
|
22
|
+
module Keys
|
|
23
|
+
UP = ["\e[A", "\eOA", 'k'].freeze
|
|
24
|
+
DOWN = ["\e[B", "\eOB", 'j'].freeze
|
|
25
|
+
ENTER = ["\r", "\n"].freeze
|
|
26
|
+
ESCAPE = ["\e", "\x1B", 'q'].freeze
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
@output = TerminalOutput.new($stdout)
|
|
30
|
+
@buffer_manager = TerminalBuffer.new(@output)
|
|
31
|
+
@input = TerminalInput.new
|
|
32
|
+
@buffer = @buffer_manager.buffer
|
|
33
|
+
|
|
34
|
+
class << self
|
|
35
|
+
# Expose a print wrapper for backward-compatible expectations in tests
|
|
36
|
+
def print(str)
|
|
37
|
+
@output.print(str)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def size
|
|
41
|
+
@input.size
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def clear
|
|
45
|
+
print [ANSI::Control::CLEAR, ANSI::Control::HOME].join
|
|
46
|
+
clear_buffer_cache
|
|
47
|
+
$stdout.flush
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def move(row, col)
|
|
51
|
+
# Historically this only queued the move; keep parity
|
|
52
|
+
@buffer << ANSI.move(row, col)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def write(row, col, text)
|
|
56
|
+
@buffer_manager.write(row, col, text)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def write_differential(row, col, text)
|
|
60
|
+
@buffer_manager.write_differential(row, col, text)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def clear_buffer_cache
|
|
64
|
+
@buffer_manager.clear_buffer_cache
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def batch_write(&)
|
|
68
|
+
@buffer_manager.batch_write(&)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def start_frame(width: nil, height: nil)
|
|
72
|
+
if width && height
|
|
73
|
+
w = width.to_i
|
|
74
|
+
h = height.to_i
|
|
75
|
+
else
|
|
76
|
+
h, w = size
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
@buffer_manager.start_frame(width: w, height: h)
|
|
80
|
+
@buffer = @buffer_manager.buffer
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def end_frame
|
|
84
|
+
@buffer_manager.end_frame
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Queue raw control sequences (e.g., Kitty graphics) for the current frame.
|
|
88
|
+
# These are emitted before any row diffs.
|
|
89
|
+
def raw(text)
|
|
90
|
+
@buffer_manager.raw(text)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def setup
|
|
94
|
+
@input.setup_console
|
|
95
|
+
print ANSI::Control::SAVE_SCREEN
|
|
96
|
+
print ANSI::Control::HIDE_CURSOR
|
|
97
|
+
print ANSI::BG_DARK
|
|
98
|
+
clear
|
|
99
|
+
@input.setup_signal_handlers { cleanup }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def cleanup
|
|
103
|
+
print([
|
|
104
|
+
ANSI::Control::CLEAR,
|
|
105
|
+
ANSI::Control::HOME,
|
|
106
|
+
ANSI::Control::SHOW_CURSOR,
|
|
107
|
+
ANSI::Control::RESTORE_SCREEN,
|
|
108
|
+
ANSI::RESET,
|
|
109
|
+
].join)
|
|
110
|
+
@output.flush
|
|
111
|
+
@input.cleanup_console
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def read_key
|
|
115
|
+
@input.read_key
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def read_key_blocking(timeout: nil)
|
|
119
|
+
@input.read_key_blocking(timeout: timeout)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def enable_mouse
|
|
123
|
+
@input.enable_mouse
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def disable_mouse
|
|
127
|
+
@input.disable_mouse
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def read_input_with_mouse(timeout: nil)
|
|
131
|
+
@input.read_input_with_mouse(timeout: timeout)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def setup_signal_handlers(&cleanup_callback)
|
|
135
|
+
@input.setup_signal_handlers(&cleanup_callback)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def buffer
|
|
139
|
+
@buffer
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def buffer_manager
|
|
143
|
+
@buffer_manager
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def output
|
|
147
|
+
@output
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def input
|
|
151
|
+
@input
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def reset!
|
|
155
|
+
@output = TerminalOutput.new($stdout)
|
|
156
|
+
@buffer_manager = TerminalBuffer.new(@output)
|
|
157
|
+
@input = TerminalInput.new
|
|
158
|
+
@buffer = @buffer_manager.buffer
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
module Shoko
|
|
166
|
+
Terminal = Adapters::Output::Terminal::Terminal
|
|
167
|
+
end
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shoko
|
|
4
|
+
module Adapters
|
|
5
|
+
module Output
|
|
6
|
+
module Terminal
|
|
7
|
+
# Sanitizes untrusted text before it is rendered in a terminal.
|
|
8
|
+
#
|
|
9
|
+
# Removes ANSI/VT control sequences (OSC/DCS/CSI/etc.) and drops C0/C1 control
|
|
10
|
+
# characters to prevent terminal escape injection and layout corruption.
|
|
11
|
+
module TerminalSanitizer
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# Pre-sanitizer for XML/XHTML sources before feeding them to an XML parser.
|
|
15
|
+
#
|
|
16
|
+
# Some EPUBs (and malicious inputs) include numeric character references to
|
|
17
|
+
# disallowed control characters (e.g. `` / ``), which can cause
|
|
18
|
+
# REXML to raise parse errors. To keep parsing resilient, this method:
|
|
19
|
+
# - Decodes *numeric* references for C0/C1/DEL into real codepoints so the
|
|
20
|
+
# control-sequence scanner can remove the entire escape sequence.
|
|
21
|
+
# - Drops numeric references to codepoints that are invalid in XML 1.0.
|
|
22
|
+
#
|
|
23
|
+
# It intentionally does not decode non-control references (e.g. `<`)
|
|
24
|
+
# because doing so can change document structure before parsing.
|
|
25
|
+
def sanitize_xml_source(text, preserve_newlines: true, preserve_tabs: true)
|
|
26
|
+
return '' if text.nil?
|
|
27
|
+
|
|
28
|
+
str = coerce_utf8(String(text))
|
|
29
|
+
return '' if str.empty?
|
|
30
|
+
|
|
31
|
+
pre = decode_control_numeric_references(str)
|
|
32
|
+
sanitize(pre, preserve_newlines: preserve_newlines, preserve_tabs: preserve_tabs)
|
|
33
|
+
rescue StandardError
|
|
34
|
+
sanitize(text.to_s, preserve_newlines: preserve_newlines, preserve_tabs: preserve_tabs)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @param text [String,nil]
|
|
38
|
+
# @param preserve_newlines [Boolean] keep `\n` (and normalize `\r` to `\n`)
|
|
39
|
+
# @param preserve_tabs [Boolean] keep `\t`
|
|
40
|
+
# @return [String] UTF-8 string safe for terminal rendering
|
|
41
|
+
def sanitize(text, preserve_newlines: false, preserve_tabs: false)
|
|
42
|
+
return '' if text.nil?
|
|
43
|
+
|
|
44
|
+
str = String(text)
|
|
45
|
+
return '' if str.empty?
|
|
46
|
+
|
|
47
|
+
str = coerce_utf8(str)
|
|
48
|
+
cps = str.codepoints
|
|
49
|
+
return '' if cps.empty?
|
|
50
|
+
|
|
51
|
+
out = +''
|
|
52
|
+
out.force_encoding(Encoding::UTF_8)
|
|
53
|
+
|
|
54
|
+
i = 0
|
|
55
|
+
while i < cps.length
|
|
56
|
+
cp = cps[i]
|
|
57
|
+
|
|
58
|
+
case cp
|
|
59
|
+
when 0x1B # ESC
|
|
60
|
+
i = skip_esc_sequence(cps, i + 1)
|
|
61
|
+
next
|
|
62
|
+
when 0x9B # CSI (8-bit)
|
|
63
|
+
i = skip_csi_sequence(cps, i + 1)
|
|
64
|
+
next
|
|
65
|
+
when 0x9D # OSC (8-bit)
|
|
66
|
+
i = skip_osc_sequence(cps, i + 1, c1_variant: true)
|
|
67
|
+
next
|
|
68
|
+
when 0x90, 0x98, 0x9E, 0x9F # DCS, SOS, PM, APC (8-bit)
|
|
69
|
+
i = skip_string_sequence(cps, i + 1, c1_variant: true)
|
|
70
|
+
next
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
if cp == 0x0A # \n
|
|
74
|
+
out << (preserve_newlines ? "\n" : ' ')
|
|
75
|
+
i += 1
|
|
76
|
+
next
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
if cp == 0x0D # \r
|
|
80
|
+
out << (preserve_newlines ? "\n" : ' ')
|
|
81
|
+
i += 1
|
|
82
|
+
next
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
if cp == 0x09 # \t
|
|
86
|
+
out << (preserve_tabs ? "\t" : ' ')
|
|
87
|
+
i += 1
|
|
88
|
+
next
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
if control_codepoint?(cp)
|
|
92
|
+
i += 1
|
|
93
|
+
next
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
out << cp
|
|
97
|
+
i += 1
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
out
|
|
101
|
+
rescue StandardError
|
|
102
|
+
String(text).encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "\uFFFD")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Input filter for single-character text entry (search fields, editors).
|
|
106
|
+
# Prevents inserting C0/C1 control characters and DEL.
|
|
107
|
+
def printable_char?(key)
|
|
108
|
+
return false unless key.is_a?(String)
|
|
109
|
+
return false unless key.length == 1
|
|
110
|
+
|
|
111
|
+
cp = key.ord
|
|
112
|
+
return false if cp < 0x20
|
|
113
|
+
return false if cp == 0x7F
|
|
114
|
+
return false if cp.between?(0x80, 0x9F)
|
|
115
|
+
|
|
116
|
+
true
|
|
117
|
+
rescue StandardError
|
|
118
|
+
false
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def coerce_utf8(str)
|
|
122
|
+
return str if str.encoding == Encoding::UTF_8 && str.valid_encoding?
|
|
123
|
+
|
|
124
|
+
str.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "\uFFFD")
|
|
125
|
+
rescue StandardError
|
|
126
|
+
str.to_s.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "\uFFFD")
|
|
127
|
+
end
|
|
128
|
+
private_class_method :coerce_utf8
|
|
129
|
+
|
|
130
|
+
def control_codepoint?(codepoint)
|
|
131
|
+
return true if codepoint < 0x20
|
|
132
|
+
return true if codepoint == 0x7F
|
|
133
|
+
return true if codepoint.between?(0x80, 0x9F)
|
|
134
|
+
|
|
135
|
+
false
|
|
136
|
+
end
|
|
137
|
+
private_class_method :control_codepoint?
|
|
138
|
+
|
|
139
|
+
def skip_esc_sequence(codepoints, index)
|
|
140
|
+
return codepoints.length if index >= codepoints.length
|
|
141
|
+
|
|
142
|
+
lead = codepoints[index]
|
|
143
|
+
case lead
|
|
144
|
+
when 0x5B # '[' CSI
|
|
145
|
+
skip_csi_sequence(codepoints, index + 1)
|
|
146
|
+
when 0x5D # ']' OSC
|
|
147
|
+
skip_osc_sequence(codepoints, index + 1, c1_variant: false)
|
|
148
|
+
when 0x50, 0x58, 0x5E, 0x5F # 'P' DCS, 'X' SOS, '^' PM, '_' APC
|
|
149
|
+
skip_string_sequence(codepoints, index + 1, c1_variant: false)
|
|
150
|
+
else
|
|
151
|
+
# 2-byte escape sequence (ESC + final byte)
|
|
152
|
+
index + 1
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
private_class_method :skip_esc_sequence
|
|
156
|
+
|
|
157
|
+
def skip_csi_sequence(codepoints, index)
|
|
158
|
+
while index < codepoints.length
|
|
159
|
+
cp = codepoints[index]
|
|
160
|
+
index += 1
|
|
161
|
+
# Final byte is 0x40..0x7E
|
|
162
|
+
break if cp.between?(0x40, 0x7E)
|
|
163
|
+
end
|
|
164
|
+
index
|
|
165
|
+
end
|
|
166
|
+
private_class_method :skip_csi_sequence
|
|
167
|
+
|
|
168
|
+
def skip_osc_sequence(codepoints, index, c1_variant:)
|
|
169
|
+
while index < codepoints.length
|
|
170
|
+
cp = codepoints[index]
|
|
171
|
+
|
|
172
|
+
return index + 1 if cp == 0x07 # BEL
|
|
173
|
+
|
|
174
|
+
return index + 2 if !c1_variant && cp == 0x1B && codepoints[index + 1] == 0x5C # ESC \
|
|
175
|
+
|
|
176
|
+
return index + 1 if c1_variant && cp == 0x9C # ST (8-bit)
|
|
177
|
+
|
|
178
|
+
# Allow ESC \ as terminator even for 8-bit variants (robustness).
|
|
179
|
+
return index + 2 if cp == 0x1B && codepoints[index + 1] == 0x5C
|
|
180
|
+
|
|
181
|
+
index += 1
|
|
182
|
+
end
|
|
183
|
+
index
|
|
184
|
+
end
|
|
185
|
+
private_class_method :skip_osc_sequence
|
|
186
|
+
|
|
187
|
+
def skip_string_sequence(codepoints, index, c1_variant:)
|
|
188
|
+
while index < codepoints.length
|
|
189
|
+
cp = codepoints[index]
|
|
190
|
+
|
|
191
|
+
return index + 1 if c1_variant && cp == 0x9C # ST
|
|
192
|
+
|
|
193
|
+
return index + 2 if cp == 0x1B && codepoints[index + 1] == 0x5C # ESC \
|
|
194
|
+
|
|
195
|
+
index += 1
|
|
196
|
+
end
|
|
197
|
+
index
|
|
198
|
+
end
|
|
199
|
+
private_class_method :skip_string_sequence
|
|
200
|
+
|
|
201
|
+
def decode_control_numeric_references(str)
|
|
202
|
+
# Hex numeric references
|
|
203
|
+
out = str.gsub(/&#x([0-9A-Fa-f]+);/) do |match|
|
|
204
|
+
cp = Regexp.last_match(1).to_i(16)
|
|
205
|
+
replacement_for_xml_numeric_ref(cp, match)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Decimal numeric references
|
|
209
|
+
out.gsub(/&#(\d+);/) do |match|
|
|
210
|
+
cp = Regexp.last_match(1).to_i
|
|
211
|
+
replacement_for_xml_numeric_ref(cp, match)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
private_class_method :decode_control_numeric_references
|
|
215
|
+
|
|
216
|
+
def replacement_for_xml_numeric_ref(codepoint, original)
|
|
217
|
+
return '' unless codepoint.is_a?(Integer)
|
|
218
|
+
|
|
219
|
+
# Decode control characters so the escape-sequence scanner can remove the
|
|
220
|
+
# full sequence (e.g. ESC + '[' + ... + 'm').
|
|
221
|
+
return [codepoint].pack('U') if codepoint < 0x20 || codepoint == 0x7F || codepoint.between?(0x80, 0x9F)
|
|
222
|
+
|
|
223
|
+
# Drop codepoints that are not valid in XML 1.0.
|
|
224
|
+
xml_allowed_codepoint?(codepoint) ? original : ''
|
|
225
|
+
rescue StandardError
|
|
226
|
+
''
|
|
227
|
+
end
|
|
228
|
+
private_class_method :replacement_for_xml_numeric_ref
|
|
229
|
+
|
|
230
|
+
def xml_allowed_codepoint?(codepoint)
|
|
231
|
+
return true if [0x09, 0x0A, 0x0D].include?(codepoint)
|
|
232
|
+
return true if codepoint.between?(0x20, 0xD7FF)
|
|
233
|
+
return true if codepoint.between?(0xE000, 0xFFFD)
|
|
234
|
+
return true if codepoint.between?(0x10000, 0x10FFFF)
|
|
235
|
+
|
|
236
|
+
false
|
|
237
|
+
end
|
|
238
|
+
private_class_method :xml_allowed_codepoint?
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|