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,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base_service'
|
|
4
|
+
require_relative '../models/selection_anchor'
|
|
5
|
+
require_relative '../../application/selectors/reader_selectors'
|
|
6
|
+
|
|
7
|
+
module Shoko
|
|
8
|
+
module Core
|
|
9
|
+
module Services
|
|
10
|
+
# Service to normalize selection ranges and extract text from rendered_lines
|
|
11
|
+
# Centralizes logic used by UIController and MouseableReader
|
|
12
|
+
class SelectionService < BaseService
|
|
13
|
+
# Convenience helper: extract selection text directly from state.
|
|
14
|
+
# If selection_range is nil, uses state[:reader][:selection].
|
|
15
|
+
def extract_from_state(state, selection_range = nil)
|
|
16
|
+
return '' unless state
|
|
17
|
+
|
|
18
|
+
range = selection_range || state.get(%i[reader selection])
|
|
19
|
+
rendered = Shoko::Application::Selectors::ReaderSelectors.rendered_lines(state)
|
|
20
|
+
extract_text(range, rendered)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Extract selected text from selection_range using rendered_lines in state
|
|
24
|
+
# @param selection_range [Hash] {:start=>{x:,y:}, :end=>{x:,y:}}
|
|
25
|
+
# @param rendered_lines [Hash<Integer, Hash>] mapping of line_id => {row:, col:, col_end:, width:, text:}
|
|
26
|
+
# @return [String]
|
|
27
|
+
def extract_text(selection_range, rendered_lines)
|
|
28
|
+
return '' unless selection_range && rendered_lines && !rendered_lines.empty?
|
|
29
|
+
|
|
30
|
+
coordinate_service = resolve(:coordinate_service)
|
|
31
|
+
normalized = coordinate_service.normalize_selection_range(selection_range, rendered_lines)
|
|
32
|
+
return '' unless normalized
|
|
33
|
+
|
|
34
|
+
start_anchor = Shoko::Core::Models::SelectionAnchor.from(normalized[:start])
|
|
35
|
+
end_anchor = Shoko::Core::Models::SelectionAnchor.from(normalized[:end])
|
|
36
|
+
return '' unless start_anchor && end_anchor
|
|
37
|
+
|
|
38
|
+
geometry_index = build_geometry_index(rendered_lines)
|
|
39
|
+
return '' if geometry_index.empty?
|
|
40
|
+
|
|
41
|
+
ordered = order_geometry(geometry_index.values)
|
|
42
|
+
start_idx = ordered.find_index { |geo| geo.key == start_anchor.geometry_key }
|
|
43
|
+
end_idx = ordered.find_index { |geo| geo.key == end_anchor.geometry_key }
|
|
44
|
+
return '' unless start_idx && end_idx
|
|
45
|
+
|
|
46
|
+
text_lines = []
|
|
47
|
+
|
|
48
|
+
ordered[start_idx..end_idx].each do |geometry|
|
|
49
|
+
start_cell = geometry.key == start_anchor.geometry_key ? start_anchor.cell_index : 0
|
|
50
|
+
end_cell = geometry.key == end_anchor.geometry_key ? end_anchor.cell_index : geometry.cells.length
|
|
51
|
+
|
|
52
|
+
next if end_cell < start_cell
|
|
53
|
+
|
|
54
|
+
start_char = char_index_for_cell(geometry, start_cell)
|
|
55
|
+
end_char = char_index_for_cell(geometry, end_cell)
|
|
56
|
+
segment = geometry.plain_text[start_char...end_char]
|
|
57
|
+
next if segment.nil?
|
|
58
|
+
|
|
59
|
+
text_lines << segment
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
text_lines.join("\n")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Normalize a selection range using the coordinate service and rendered_lines
|
|
66
|
+
#
|
|
67
|
+
# @param state [Application::Infrastructure::ObserverStateStore]
|
|
68
|
+
# @param selection_range [Hash]
|
|
69
|
+
# @return [Hash, nil] normalized range or nil when normalization fails
|
|
70
|
+
def normalize_range(state, selection_range)
|
|
71
|
+
return nil unless selection_range
|
|
72
|
+
return selection_range if anchor_range?(selection_range)
|
|
73
|
+
|
|
74
|
+
coordinate_service = resolve(:coordinate_service)
|
|
75
|
+
rendered = Shoko::Application::Selectors::ReaderSelectors.rendered_lines(state)
|
|
76
|
+
coordinate_service.normalize_selection_range(selection_range, rendered)
|
|
77
|
+
rescue StandardError
|
|
78
|
+
nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
protected
|
|
82
|
+
|
|
83
|
+
def required_dependencies
|
|
84
|
+
[:coordinate_service]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def anchor_range?(range)
|
|
90
|
+
return false unless range.is_a?(Hash)
|
|
91
|
+
|
|
92
|
+
start_anchor = range[:start] || range['start']
|
|
93
|
+
start_anchor.is_a?(Hash) && (start_anchor.key?(:geometry_key) || start_anchor.key?('geometry_key'))
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def build_geometry_index(rendered_lines)
|
|
97
|
+
rendered_lines.each_with_object({}) do |(_key, info), acc|
|
|
98
|
+
geometry = info[:geometry]
|
|
99
|
+
next unless geometry
|
|
100
|
+
|
|
101
|
+
acc[geometry.key] = geometry
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def order_geometry(geometries)
|
|
106
|
+
geometries.sort_by do |geo|
|
|
107
|
+
[geo.page_id || 0, geo.line_offset || 0, geo.column_id || 0, geo.row || 0, geo.column_origin || 0]
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def char_index_for_cell(geometry, cell_index)
|
|
112
|
+
cells = geometry.cells
|
|
113
|
+
return 0 if cells.empty?
|
|
114
|
+
|
|
115
|
+
if cell_index <= 0
|
|
116
|
+
0
|
|
117
|
+
elsif cell_index >= cells.length
|
|
118
|
+
geometry.plain_text.length
|
|
119
|
+
else
|
|
120
|
+
cells[cell_index].char_start
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shoko
|
|
4
|
+
module Core
|
|
5
|
+
# Base validator class providing common validation patterns.
|
|
6
|
+
# Subclasses should implement specific validation logic.
|
|
7
|
+
#
|
|
8
|
+
# @abstract
|
|
9
|
+
class Validator
|
|
10
|
+
# Validation errors collection
|
|
11
|
+
attr_reader :errors
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@errors = []
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Check if validation passed
|
|
18
|
+
#
|
|
19
|
+
# @return [Boolean] true if no errors
|
|
20
|
+
def valid?
|
|
21
|
+
@errors.empty?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Add an error message
|
|
25
|
+
#
|
|
26
|
+
# @param field [Symbol] Field name
|
|
27
|
+
# @param message [String] Error message
|
|
28
|
+
def add_error(field, message)
|
|
29
|
+
@errors << { field:, message: }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Clear all errors
|
|
33
|
+
def clear_errors
|
|
34
|
+
@errors = []
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Validate presence of a value
|
|
38
|
+
#
|
|
39
|
+
# @param value [Object] Value to check
|
|
40
|
+
# @param field [Symbol] Field name for error reporting
|
|
41
|
+
# @param message [String] Custom error message
|
|
42
|
+
# @return [Boolean] Validation result
|
|
43
|
+
def presence_valid?(value, field, message = "can't be blank")
|
|
44
|
+
return true if value && !value.to_s.strip.empty?
|
|
45
|
+
|
|
46
|
+
add_error(field, message)
|
|
47
|
+
false
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
RangeValidationContext = Struct.new(:value, :range, :field, :message)
|
|
51
|
+
FormatValidationContext = Struct.new(:value, :pattern, :field, :message)
|
|
52
|
+
|
|
53
|
+
# Validate that a value falls within a range
|
|
54
|
+
#
|
|
55
|
+
# @param context [RangeValidationContext] Validation parameters
|
|
56
|
+
# @return [Boolean] Validation result
|
|
57
|
+
def range_valid?(context)
|
|
58
|
+
return true if context.range.include?(context.value)
|
|
59
|
+
|
|
60
|
+
add_error(context.field, context.message || 'must be between')
|
|
61
|
+
false
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Validate that a value matches a regular expression pattern
|
|
65
|
+
#
|
|
66
|
+
# @param context [FormatValidationContext] Validation parameters
|
|
67
|
+
# @return [Boolean] Validation result
|
|
68
|
+
def format_valid?(context)
|
|
69
|
+
return true if context.value&.match?(context.pattern)
|
|
70
|
+
|
|
71
|
+
add_error(context.field, context.message || 'has invalid format')
|
|
72
|
+
false
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shoko
|
|
4
|
+
# Base error class for Shoko
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Raised when EPUB file cannot be parsed
|
|
8
|
+
class EPUBParseError < Error
|
|
9
|
+
attr_reader :file_path
|
|
10
|
+
|
|
11
|
+
def initialize(message, file_path)
|
|
12
|
+
super("Failed to parse EPUB at #{file_path}: #{message}")
|
|
13
|
+
@file_path = file_path
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Raised when required file is not found
|
|
18
|
+
class FileNotFoundError < Error
|
|
19
|
+
attr_reader :file_path
|
|
20
|
+
|
|
21
|
+
def initialize(file_path)
|
|
22
|
+
super("File not found: #{file_path}")
|
|
23
|
+
@file_path = file_path
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Raised when configuration is invalid
|
|
28
|
+
class ConfigurationError < Error; end
|
|
29
|
+
|
|
30
|
+
# Raised when no interactive terminal is available
|
|
31
|
+
class TerminalUnavailableError < Error
|
|
32
|
+
def initialize
|
|
33
|
+
super('Interactive terminal not available')
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Raised when reader state is invalid
|
|
38
|
+
class InvalidStateError < Error
|
|
39
|
+
attr_reader :state
|
|
40
|
+
|
|
41
|
+
def initialize(message, state)
|
|
42
|
+
super("Invalid reader state: #{message}")
|
|
43
|
+
@state = state
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Raised when navigation is not possible
|
|
48
|
+
class NavigationError < Error
|
|
49
|
+
attr_reader :direction, :reason
|
|
50
|
+
|
|
51
|
+
def initialize(direction, reason)
|
|
52
|
+
super("Cannot navigate #{direction}: #{reason}")
|
|
53
|
+
@direction = direction
|
|
54
|
+
@reason = reason
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Raised when bookmark operation fails
|
|
59
|
+
class BookmarkError < Error
|
|
60
|
+
attr_reader :operation
|
|
61
|
+
|
|
62
|
+
def initialize(operation, message)
|
|
63
|
+
super("Bookmark #{operation} failed: #{message}")
|
|
64
|
+
@operation = operation
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Raised when rendering fails
|
|
69
|
+
class RenderError < Error
|
|
70
|
+
attr_reader :component
|
|
71
|
+
|
|
72
|
+
def initialize(component, message)
|
|
73
|
+
super("Rendering failed in #{component}: #{message}")
|
|
74
|
+
@component = component
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Raised when content normalization produces no semantic blocks
|
|
79
|
+
class FormattingError < Error
|
|
80
|
+
attr_reader :source
|
|
81
|
+
|
|
82
|
+
def initialize(source, message)
|
|
83
|
+
super("Formatting failed for #{source}: #{message}")
|
|
84
|
+
@source = source
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Raised when cached book data cannot be loaded or is incompatible.
|
|
89
|
+
class CacheLoadError < Error
|
|
90
|
+
attr_reader :path
|
|
91
|
+
|
|
92
|
+
def initialize(path, message = 'Cache is corrupt or incompatible')
|
|
93
|
+
super("Cache load failed for #{path}: #{message}")
|
|
94
|
+
@path = path
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shoko
|
|
4
|
+
# Test-only helpers and doubles for terminal interactions.
|
|
5
|
+
module TestSupport
|
|
6
|
+
# Lightweight replacement for the production Terminal facade that keeps
|
|
7
|
+
# tests deterministic and side-effect free. All terminal operations become
|
|
8
|
+
# in-memory recordings so specs can make assertions without touching an
|
|
9
|
+
# actual TTY.
|
|
10
|
+
class TerminalDouble
|
|
11
|
+
ANSI = Shoko::Adapters::Output::Terminal::TerminalOutput::ANSI
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
attr_accessor :default_height, :default_width
|
|
15
|
+
|
|
16
|
+
def reset!
|
|
17
|
+
@writes = []
|
|
18
|
+
@printed = []
|
|
19
|
+
@moves = []
|
|
20
|
+
@clears = []
|
|
21
|
+
@mouse_events = []
|
|
22
|
+
@input_queue = Queue.new
|
|
23
|
+
@default_height = 24
|
|
24
|
+
@default_width = 80
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def writes
|
|
28
|
+
@writes ||= []
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def printed
|
|
32
|
+
@printed ||= []
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def moves
|
|
36
|
+
@moves ||= []
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def clears
|
|
40
|
+
@clears ||= []
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def mouse_events
|
|
44
|
+
@mouse_events ||= []
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def push_input(*keys)
|
|
48
|
+
ensure_input_queue
|
|
49
|
+
keys.flatten.each { |k| @input_queue << k }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def drain_input
|
|
53
|
+
ensure_input_queue
|
|
54
|
+
drained = []
|
|
55
|
+
loop do
|
|
56
|
+
drained << @input_queue.pop(true)
|
|
57
|
+
rescue ThreadError
|
|
58
|
+
break
|
|
59
|
+
end
|
|
60
|
+
drained
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def size=(tuple)
|
|
64
|
+
@default_height, @default_width = tuple
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def size
|
|
68
|
+
[@default_height || 24, @default_width || 80]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def setup
|
|
72
|
+
@setup_calls = setup_calls + 1
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def cleanup
|
|
76
|
+
@cleanup_calls = cleanup_calls + 1
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def setup_calls
|
|
80
|
+
@setup_calls ||= 0
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def cleanup_calls
|
|
84
|
+
@cleanup_calls ||= 0
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def start_frame(**_kwargs)
|
|
88
|
+
# no-op, but record for assertions if needed
|
|
89
|
+
@frame_started = true
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def end_frame
|
|
93
|
+
@frame_started = false
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def frame_started?
|
|
97
|
+
!!@frame_started
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def clear
|
|
101
|
+
clears << :clear
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def move(row, col)
|
|
105
|
+
moves << [row, col]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def write(row, col, text)
|
|
109
|
+
writes << { row:, col:, text: text.to_s }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def write_differential(row, col, text)
|
|
113
|
+
write(row, col, text)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def clear_buffer_cache
|
|
117
|
+
# Nothing cached in the double; keep interface parity.
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def batch_write
|
|
121
|
+
yield if block_given?
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def print(str)
|
|
125
|
+
printed << str.to_s
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def flush
|
|
129
|
+
# no-op
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def enable_mouse
|
|
133
|
+
mouse_events << :enabled
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def disable_mouse
|
|
137
|
+
mouse_events << :disabled
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def read_key
|
|
141
|
+
pop_key(non_block: true)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def read_key_blocking(timeout: nil)
|
|
145
|
+
pop_key(timeout: timeout || 0.1)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def read_input_with_mouse(timeout: nil)
|
|
149
|
+
read_key_blocking(timeout: timeout)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
private
|
|
153
|
+
|
|
154
|
+
def ensure_input_queue
|
|
155
|
+
@ensure_input_queue ||= Queue.new
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def pop_key(non_block: false, timeout: nil)
|
|
159
|
+
ensure_input_queue
|
|
160
|
+
if non_block
|
|
161
|
+
@input_queue.pop(true)
|
|
162
|
+
elsif timeout
|
|
163
|
+
@input_queue.pop(timeout: timeout)
|
|
164
|
+
else
|
|
165
|
+
@input_queue.pop
|
|
166
|
+
end
|
|
167
|
+
rescue ThreadError
|
|
168
|
+
nil
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
TerminalDouble.reset!
|
|
174
|
+
end
|
|
175
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'terminal_double'
|
|
4
|
+
|
|
5
|
+
module Shoko
|
|
6
|
+
module TestSupport
|
|
7
|
+
# Central hook that enables deterministic test behaviour by swapping in
|
|
8
|
+
# lightweight adapters and updating the dependency container.
|
|
9
|
+
module TestMode
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def active?
|
|
13
|
+
primary = ENV.fetch('SHOKO_TEST_MODE', '').to_s.strip
|
|
14
|
+
primary == '1'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def activate!
|
|
18
|
+
return unless active?
|
|
19
|
+
|
|
20
|
+
install_terminal_double!
|
|
21
|
+
silence_logger!
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def configure_container(container)
|
|
25
|
+
return unless active?
|
|
26
|
+
|
|
27
|
+
container.register_singleton(:terminal_service) do |c|
|
|
28
|
+
TestTerminalService.new(c)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Test-specialised terminal service that exposes helpers for enqueuing
|
|
33
|
+
# deterministic input sequences while delegating behaviour to the base
|
|
34
|
+
# implementation (now backed by the TerminalDouble).
|
|
35
|
+
class TestTerminalService < Shoko::Adapters::Output::Terminal::TerminalService
|
|
36
|
+
def queue_input(*keys)
|
|
37
|
+
Shoko::TestSupport::TerminalDouble.push_input(*keys)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def drain_input
|
|
41
|
+
Shoko::TestSupport::TerminalDouble.drain_input
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def configure_size(height:, width:)
|
|
45
|
+
Shoko::TestSupport::TerminalDouble.size = [height, width]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def install_terminal_double!
|
|
50
|
+
return if @terminal_installed
|
|
51
|
+
|
|
52
|
+
real_terminal = (Shoko.const_get(:Terminal) if Shoko.const_defined?(:Terminal, false))
|
|
53
|
+
Shoko.const_set(:RealTerminal, real_terminal) unless Shoko.const_defined?(:RealTerminal)
|
|
54
|
+
Shoko.send(:remove_const, :Terminal) if Shoko.const_defined?(:Terminal, false)
|
|
55
|
+
Shoko.const_set(:Terminal, Shoko::TestSupport::TerminalDouble)
|
|
56
|
+
Shoko::TestSupport::TerminalDouble.reset!
|
|
57
|
+
@terminal_installed = true
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def silence_logger!
|
|
61
|
+
return unless defined?(Shoko::Adapters::Monitoring::Logger)
|
|
62
|
+
|
|
63
|
+
begin
|
|
64
|
+
Shoko::Adapters::Monitoring::Logger.output = logger_null_io
|
|
65
|
+
Shoko::Adapters::Monitoring::Logger.level = :fatal
|
|
66
|
+
rescue StandardError
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def logger_null_io
|
|
72
|
+
@null_logger_io ||= File.open(File::NULL, 'w')
|
|
73
|
+
rescue StandardError
|
|
74
|
+
nil
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|