shoko 0.1.3 → 0.1.4
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 +4 -4
- data/.bundle/config +0 -1
- data/.rubocop.yml +35 -4
- data/Gemfile +1 -0
- data/Shoko.gemspec +34 -0
- data/bin/shoko +2 -2
- data/bin/start +2 -2
- data/lib/shoko/adapters/base_adapter.rb +30 -0
- data/lib/shoko/adapters/book_sources/document_service.rb +23 -6
- data/lib/shoko/adapters/book_sources/download_service.rb +67 -71
- data/lib/shoko/adapters/book_sources/epub/epub_resource_loader.rb +8 -7
- data/lib/shoko/adapters/book_sources/epub/parsers/html_processor.rb +2 -7
- data/lib/shoko/adapters/book_sources/epub/parsers/metadata_extractor.rb +2 -1
- data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_traversal.rb +3 -2
- data/lib/shoko/adapters/book_sources/epub/parsers/opf_processor.rb +14 -5
- data/lib/shoko/adapters/book_sources/epub/parsers/rexml_safe_parser.rb +50 -0
- data/lib/shoko/adapters/book_sources/epub/parsers/xhtml_content_parser.rb +630 -507
- data/lib/shoko/adapters/book_sources/epub/parsers/xml_text_normalizer.rb +1 -1
- data/lib/shoko/adapters/book_sources/epub_document.rb +206 -193
- data/lib/shoko/adapters/book_sources/epub_finder/directory_scanner.rb +4 -3
- data/lib/shoko/adapters/book_sources/epub_finder.rb +10 -12
- data/lib/shoko/adapters/book_sources/epub_importer.rb +27 -17
- data/lib/shoko/adapters/book_sources/gutendex_client.rb +43 -27
- data/lib/shoko/adapters/book_sources/library_scanner.rb +44 -7
- data/lib/shoko/adapters/book_sources/metadata_reader_adapter.rb +21 -0
- data/lib/shoko/adapters/input/annotations/mouse_handler.rb +1 -1
- data/lib/shoko/adapters/input/command_bridge.rb +60 -127
- data/lib/shoko/adapters/input/command_factory.rb +104 -9
- data/lib/shoko/adapters/input/commands.rb +7 -5
- data/lib/shoko/adapters/input/input_controller.rb +266 -186
- data/lib/shoko/adapters/input/input_system_factory_adapter.rb +28 -0
- data/lib/shoko/adapters/input/key_classifier_adapter.rb +66 -0
- data/lib/shoko/adapters/input/validators/file_path_validator.rb +2 -2
- data/lib/shoko/adapters/input/validators/terminal_size_validator.rb +1 -1
- data/lib/shoko/adapters/monitoring/logger_adapter.rb +119 -0
- data/lib/shoko/adapters/monitoring/perf_tracer.rb +16 -19
- data/lib/shoko/adapters/monitoring/performance_monitor.rb +90 -97
- data/lib/shoko/adapters/output/clipboard/clipboard_service.rb +80 -90
- data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler/image_builder.rb +118 -118
- data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler/table_renderer.rb +361 -0
- data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler/text_wrapper.rb +106 -106
- data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler/tokenizer.rb +75 -62
- data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler.rb +226 -102
- data/lib/shoko/adapters/output/formatting/formatting_service/plain_lines_builder.rb +39 -39
- data/lib/shoko/adapters/output/formatting/formatting_service.rb +221 -197
- data/lib/shoko/adapters/output/formatting/wrapping_service.rb +164 -182
- data/lib/shoko/adapters/output/instrumentation_service.rb +52 -34
- data/lib/shoko/adapters/output/kitty/display_capabilities.rb +21 -0
- data/lib/shoko/adapters/output/kitty/kitty_image_renderer.rb +10 -5
- data/lib/shoko/adapters/output/kitty/kitty_unicode_placeholders.rb +125 -125
- data/lib/shoko/adapters/output/notification_service.rb +54 -39
- data/lib/shoko/adapters/output/render_registry.rb +0 -5
- data/lib/shoko/adapters/output/rendering/models/line_geometry.rb +3 -3
- data/lib/shoko/adapters/output/rendering/models/rendering_context.rb +14 -9
- data/lib/shoko/adapters/output/terminal/buffer.rb +263 -206
- data/lib/shoko/adapters/output/terminal/input/decoder.rb +298 -298
- data/lib/shoko/adapters/output/terminal/input.rb +76 -0
- data/lib/shoko/adapters/output/terminal/output.rb +83 -81
- data/lib/shoko/adapters/output/terminal/terminal.rb +58 -16
- data/lib/shoko/adapters/output/terminal/terminal_sanitizer.rb +201 -201
- data/lib/shoko/adapters/output/terminal/terminal_service.rb +121 -106
- data/lib/shoko/adapters/output/terminal/text_metrics.rb +218 -219
- data/lib/shoko/adapters/output/terminal/text_sanitizer_adapter.rb +19 -0
- data/lib/shoko/adapters/output/terminal_capabilities_adapter.rb +23 -0
- data/lib/shoko/adapters/output/ui/component_factory.rb +84 -0
- data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay/footer_renderer.rb +2 -2
- data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay/note_renderer.rb +5 -4
- data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay_component.rb +280 -107
- data/lib/shoko/adapters/output/ui/components/annotations_overlay/list_renderer.rb +9 -7
- data/lib/shoko/adapters/output/ui/components/annotations_overlay_component.rb +13 -3
- data/lib/shoko/adapters/output/ui/components/base_component.rb +9 -5
- data/lib/shoko/adapters/output/ui/components/content_component.rb +11 -3
- data/lib/shoko/adapters/output/ui/components/dictionary/entry_formatter.rb +228 -0
- data/lib/shoko/adapters/output/ui/components/dictionary_panel_component.rb +266 -0
- data/lib/shoko/adapters/output/ui/components/dictionary_popup_component.rb +251 -0
- data/lib/shoko/adapters/output/ui/components/enhanced_popup_menu.rb +19 -7
- data/lib/shoko/adapters/output/ui/components/footer_component.rb +5 -3
- data/lib/shoko/adapters/output/ui/components/header_component.rb +1 -1
- data/lib/shoko/adapters/output/ui/components/layouts/horizontal_three.rb +90 -0
- data/lib/shoko/adapters/output/ui/components/main_menu_component.rb +9 -6
- data/lib/shoko/adapters/output/ui/components/reading/base_view_renderer.rb +72 -21
- data/lib/shoko/adapters/output/ui/components/reading/config_helpers.rb +19 -11
- data/lib/shoko/adapters/output/ui/components/reading/inline_segment_highlighter.rb +25 -4
- data/lib/shoko/adapters/output/ui/components/reading/kitty_image_line_renderer.rb +18 -6
- data/lib/shoko/adapters/output/ui/components/reading/line_content_composer.rb +2 -2
- data/lib/shoko/adapters/output/ui/components/reading/line_drawer.rb +16 -8
- data/lib/shoko/adapters/output/ui/components/reading/line_geometry_builder.rb +2 -2
- data/lib/shoko/adapters/output/ui/components/reading/single_view_renderer.rb +5 -5
- data/lib/shoko/adapters/output/ui/components/reading/split_view_renderer.rb +7 -9
- data/lib/shoko/adapters/output/ui/components/reading/view_renderer_factory.rb +13 -4
- data/lib/shoko/adapters/output/ui/components/screens/annotation_detail_screen_component.rb +25 -97
- data/lib/shoko/adapters/output/ui/components/screens/annotation_edit_screen_component.rb +58 -100
- data/lib/shoko/adapters/output/ui/components/screens/annotation_editor_screen_component.rb +40 -6
- data/lib/shoko/adapters/output/ui/components/screens/annotation_rendering_helpers.rb +297 -21
- data/lib/shoko/adapters/output/ui/components/screens/annotations_screen_component.rb +93 -158
- data/lib/shoko/adapters/output/ui/components/screens/base_screen_component.rb +1 -1
- data/lib/shoko/adapters/output/ui/components/screens/browse_screen_component.rb +49 -36
- data/lib/shoko/adapters/output/ui/components/screens/dictionary_settings_screen_component.rb +457 -0
- data/lib/shoko/adapters/output/ui/components/screens/download_books_screen_component.rb +53 -36
- data/lib/shoko/adapters/output/ui/components/screens/library_screen_component.rb +81 -53
- data/lib/shoko/adapters/output/ui/components/screens/loading_overlay_component.rb +13 -4
- data/lib/shoko/adapters/output/ui/components/screens/menu_screen_component.rb +11 -3
- data/lib/shoko/adapters/output/ui/components/screens/settings_screen_component.rb +96 -13
- data/lib/shoko/adapters/output/ui/components/sidebar/annotations_tab_renderer.rb +13 -3
- data/lib/shoko/adapters/output/ui/components/sidebar/bookmarks_tab_renderer.rb +19 -2
- data/lib/shoko/adapters/output/ui/components/sidebar/tab_header_component.rb +13 -3
- data/lib/shoko/adapters/output/ui/components/sidebar/toc_tab_renderer.rb +22 -11
- data/lib/shoko/adapters/output/ui/components/sidebar/toc_tab_support.rb +18 -21
- data/lib/shoko/adapters/output/ui/components/sidebar_panel_component.rb +19 -16
- data/lib/shoko/adapters/output/ui/components/surface.rb +2 -2
- data/lib/shoko/adapters/output/ui/components/tooltip_overlay_component.rb +38 -12
- data/lib/shoko/adapters/output/ui/components/ui/annotation_list_input.rb +98 -0
- data/lib/shoko/adapters/output/ui/components/ui/annotation_markup.rb +452 -0
- data/lib/shoko/adapters/output/ui/components/ui/box_drawer.rb +3 -2
- data/lib/shoko/adapters/output/ui/components/ui/cursor_blink.rb +46 -0
- data/lib/shoko/adapters/output/ui/components/ui/overlay_layout.rb +10 -1
- data/lib/shoko/adapters/output/ui/components/ui/text_utils.rb +1 -1
- data/lib/shoko/adapters/output/ui/constants/themes.rb +6 -6
- data/lib/shoko/adapters/output/ui/constants/ui_constants.rb +32 -13
- data/lib/shoko/adapters/output/ui/rendering/frame_coordinator.rb +2 -0
- data/lib/shoko/adapters/output/ui/rendering/reader_render_coordinator.rb +101 -15
- data/lib/shoko/adapters/output/ui/rendering/render_pipeline.rb +45 -13
- data/lib/shoko/adapters/output/ui/rendering_factory_adapter.rb +34 -0
- data/lib/shoko/adapters/storage/background_worker.rb +8 -5
- data/lib/shoko/adapters/storage/book_cache_pipeline.rb +26 -18
- data/lib/shoko/adapters/storage/cache/epub/memory_cache.rb +1 -1
- data/lib/shoko/adapters/storage/cache/epub/serializer/helpers.rb +4 -2
- data/lib/shoko/adapters/storage/cache_availability_adapter.rb +100 -0
- data/lib/shoko/adapters/storage/cache_manager_adapter.rb +26 -0
- data/lib/shoko/adapters/storage/cache_pointer_manager.rb +4 -4
- data/lib/shoko/adapters/storage/cache_pointer_resolver.rb +25 -0
- data/lib/shoko/adapters/storage/config_storage_adapter.rb +67 -0
- data/lib/shoko/adapters/storage/dictionary_availability_adapter.rb +33 -0
- data/lib/shoko/adapters/storage/dictionary_catalog_service.rb +140 -0
- data/lib/shoko/adapters/storage/epub_cache.rb +12 -10
- data/lib/shoko/adapters/storage/file_writer_service.rb +22 -26
- data/lib/shoko/adapters/storage/json_cache_store/layouts.rb +2 -1
- data/lib/shoko/adapters/storage/json_cache_store/manifest.rb +1 -1
- data/lib/shoko/adapters/storage/json_cache_store.rb +15 -12
- data/lib/shoko/adapters/storage/lazy_file_string.rb +1 -1
- data/lib/shoko/adapters/storage/pagination_cache.rb +1 -2
- data/lib/shoko/adapters/storage/recent_files.rb +5 -3
- data/lib/shoko/adapters/storage/recent_files_repository.rb +27 -0
- data/lib/shoko/adapters/storage/repositories/annotation_repository.rb +156 -155
- data/lib/shoko/adapters/storage/repositories/base_repository.rb +57 -64
- data/lib/shoko/adapters/storage/repositories/bookmark_repository.rb +107 -106
- data/lib/shoko/adapters/storage/repositories/cached_library_repository.rb +94 -93
- data/lib/shoko/adapters/storage/repositories/config_repository.rb +225 -221
- data/lib/shoko/adapters/storage/repositories/progress_repository.rb +137 -138
- data/lib/shoko/adapters/storage/repositories/storage/annotation_file_store.rb +5 -28
- data/lib/shoko/adapters/storage/repositories/storage/base_file_store.rb +34 -0
- data/lib/shoko/adapters/storage/repositories/storage/bookmark_file_store.rb +7 -30
- data/lib/shoko/adapters/storage/repositories/storage/progress_file_store.rb +4 -26
- data/lib/shoko/adapters/storage/sqlite_dictionary_adapter.rb +307 -0
- data/lib/shoko/application/adapters/command_port_adapter.rb +133 -0
- data/lib/shoko/application/adapters/config_reader_adapter.rb +80 -0
- data/lib/shoko/application/adapters/layout_metrics_adapter.rb +66 -0
- data/lib/shoko/application/adapters/menu_state_reader_adapter.rb +204 -0
- data/lib/shoko/application/adapters/menu_state_writer_adapter.rb +134 -0
- data/lib/shoko/application/adapters/notification_writer_adapter.rb +37 -0
- data/lib/shoko/application/adapters/progress_state_reader_adapter.rb +35 -0
- data/lib/shoko/application/adapters/reader_state_reader_adapter.rb +150 -0
- data/lib/shoko/application/adapters/render_state_writer_adapter.rb +51 -0
- data/lib/shoko/application/adapters/rendered_content_reader_adapter.rb +27 -0
- data/lib/shoko/application/adapters/sidebar_state_reader_adapter.rb +65 -0
- data/lib/shoko/application/adapters/state_writer_adapter.rb +98 -0
- data/lib/shoko/application/adapters/ui_state_reader_adapter.rb +39 -0
- data/lib/shoko/application/adapters/wrapped_lines_provider_adapter.rb +35 -0
- data/lib/shoko/application/annotation_editor_overlay_session.rb +28 -8
- data/lib/shoko/application/cli.rb +15 -15
- data/lib/shoko/application/cli_progress_renderer.rb +151 -0
- data/lib/shoko/application/controllers/annotation_overlay_controller.rb +259 -0
- data/lib/shoko/application/controllers/dictionary_controller.rb +437 -0
- data/lib/shoko/application/controllers/document_path_resolver.rb +69 -0
- data/lib/shoko/application/controllers/menu/input_controller.rb +59 -30
- data/lib/shoko/application/controllers/menu/state_controller.rb +220 -85
- data/lib/shoko/application/controllers/menu_controller.rb +260 -61
- data/lib/shoko/application/controllers/mouseable_reader.rb +174 -374
- data/lib/shoko/application/controllers/reader_controller.rb +521 -386
- data/lib/shoko/application/controllers/selection_mouse_handler.rb +172 -0
- data/lib/shoko/application/controllers/sidebar_controller.rb +491 -0
- data/lib/shoko/application/controllers/sidebar_mouse_handler.rb +184 -0
- data/lib/shoko/application/controllers/state_controller.rb +154 -120
- data/lib/shoko/application/controllers/ui_controller.rb +263 -602
- data/lib/shoko/application/dependency_container.rb +790 -148
- data/lib/shoko/application/infrastructure/event_bus.rb +4 -2
- data/lib/shoko/application/infrastructure/observer_state_store.rb +7 -4
- data/lib/shoko/application/infrastructure/state_store.rb +202 -31
- data/lib/shoko/application/main_menu/menu_progress_presenter.rb +56 -56
- data/lib/shoko/application/pending_jump_handler.rb +21 -19
- data/lib/shoko/application/reader_lifecycle.rb +17 -24
- data/lib/shoko/application/reader_startup_orchestrator.rb +18 -57
- data/lib/shoko/application/selectors/config_selectors.rb +16 -0
- data/lib/shoko/application/selectors/menu_selectors.rb +20 -0
- data/lib/shoko/application/selectors/reader_selectors.rb +48 -55
- data/lib/shoko/application/state/actions/toggle_view_mode_action.rb +1 -1
- data/lib/shoko/application/state/actions/update_config_action.rb +4 -0
- data/lib/shoko/application/state/actions/update_menu_action.rb +6 -10
- data/lib/shoko/application/state/actions/update_message_action.rb +1 -14
- data/lib/shoko/application/state/actions/update_page_action.rb +4 -11
- data/lib/shoko/application/state/actions/update_pagination_state_action.rb +4 -6
- data/lib/shoko/application/state/actions/update_reader_meta_action.rb +5 -7
- data/lib/shoko/application/state/actions/update_rendered_lines_action.rb +3 -13
- data/lib/shoko/application/state/actions/update_selection_action.rb +4 -7
- data/lib/shoko/application/state/actions/update_selections_action.rb +4 -11
- data/lib/shoko/application/state/actions/update_sidebar_action.rb +4 -0
- data/lib/shoko/application/state/actions/update_state_action.rb +72 -0
- data/lib/shoko/application/state/actions/update_ui_loading_action.rb +6 -10
- data/lib/shoko/application/ui/reader_view_model_builder.rb +46 -46
- data/lib/shoko/application/ui/view_models/reader_view_model.rb +2 -2
- data/lib/shoko/application/unified_application.rb +94 -18
- data/lib/shoko/application/use_cases/catalog_service.rb +81 -87
- data/lib/shoko/application/use_cases/commands/annotation_editor_commands.rb +104 -44
- data/lib/shoko/application/use_cases/commands/application_commands.rb +34 -61
- data/lib/shoko/application/use_cases/commands/base_command.rb +1 -6
- data/lib/shoko/application/use_cases/commands/bookmark_commands.rb +2 -4
- data/lib/shoko/application/use_cases/commands/conditional_navigation_commands.rb +14 -2
- data/lib/shoko/application/use_cases/commands/menu_commands.rb +215 -119
- data/lib/shoko/application/use_cases/commands/navigation_commands.rb +8 -21
- data/lib/shoko/application/use_cases/commands/reader_commands.rb +2 -9
- data/lib/shoko/application/use_cases/commands/sidebar_commands.rb +3 -2
- data/lib/shoko/application/use_cases/settings_service.rb +275 -93
- data/lib/shoko/core/events/base_domain_event.rb +9 -6
- data/lib/shoko/core/events/domain_event_bus.rb +6 -9
- data/lib/shoko/core/models/dictionary_entry.rb +163 -0
- data/lib/shoko/core/models/reader_settings.rb +8 -8
- data/lib/shoko/core/models/selection_anchor.rb +58 -53
- data/lib/shoko/core/ports/annotation_repository.rb +94 -0
- data/lib/shoko/core/ports/async_executor.rb +24 -0
- data/lib/shoko/core/ports/bookmark_repository.rb +77 -0
- data/lib/shoko/core/ports/cache_availability.rb +18 -0
- data/lib/shoko/core/ports/cache_manager.rb +26 -0
- data/lib/shoko/core/ports/cache_pointer_resolver.rb +27 -0
- data/lib/shoko/core/ports/command_port.rb +55 -0
- data/lib/shoko/core/ports/config_reader.rb +109 -0
- data/lib/shoko/core/ports/config_storage.rb +61 -0
- data/lib/shoko/core/ports/dictionary_availability.rb +41 -0
- data/lib/shoko/core/ports/dictionary_repository.rb +69 -0
- data/lib/shoko/core/ports/display_capabilities.rb +18 -0
- data/lib/shoko/core/ports/input_system_factory.rb +36 -0
- data/lib/shoko/core/ports/instrumentation.rb +31 -0
- data/lib/shoko/core/ports/key_classifier.rb +107 -0
- data/lib/shoko/core/ports/layout_metrics.rb +78 -0
- data/lib/shoko/core/ports/logging.rb +65 -0
- data/lib/shoko/core/ports/menu_state_reader.rb +291 -0
- data/lib/shoko/core/ports/menu_state_writer.rb +136 -0
- data/lib/shoko/core/ports/metadata_reader.rb +20 -0
- data/lib/shoko/core/ports/notification_writer.rb +40 -0
- data/lib/shoko/core/ports/progress_state_reader.rb +47 -0
- data/lib/shoko/core/ports/reader_state_reader.rb +207 -0
- data/lib/shoko/core/ports/recent_files_repository.rb +25 -0
- data/lib/shoko/core/ports/render_state_writer.rb +31 -0
- data/lib/shoko/core/ports/rendered_content_reader.rb +32 -0
- data/lib/shoko/core/ports/rendering_factory.rb +38 -0
- data/lib/shoko/core/ports/sidebar_state_reader.rb +88 -0
- data/lib/shoko/core/ports/state_writer.rb +152 -0
- data/lib/shoko/core/ports/terminal_capabilities.rb +33 -0
- data/lib/shoko/core/ports/text_metrics.rb +19 -0
- data/lib/shoko/core/ports/text_sanitizer.rb +22 -0
- data/lib/shoko/core/ports/ui_component_factory.rb +61 -0
- data/lib/shoko/core/ports/ui_state_reader.rb +53 -0
- data/lib/shoko/core/ports/wrapped_lines_provider.rb +23 -0
- data/lib/shoko/core/services/annotation_service.rb +14 -15
- data/lib/shoko/core/services/base_service.rb +7 -46
- data/lib/shoko/core/services/bookmark_service.rb +118 -93
- data/lib/shoko/core/services/config_bridge.rb +27 -0
- data/lib/shoko/core/services/coordinate_service.rb +8 -10
- data/lib/shoko/core/services/default_display_capabilities.rb +18 -0
- data/lib/shoko/core/services/default_layout_metrics.rb +65 -0
- data/lib/shoko/core/services/default_terminal_capabilities.rb +23 -0
- data/lib/shoko/core/services/default_text_metrics.rb +44 -0
- data/lib/shoko/core/services/dictionary_service.rb +220 -0
- data/lib/shoko/core/services/inline_executor.rb +24 -0
- data/lib/shoko/core/services/layout_service.rb +1 -8
- data/lib/shoko/core/services/navigation/absolute_layout.rb +39 -9
- data/lib/shoko/core/services/navigation/context_builder.rb +32 -7
- data/lib/shoko/core/services/navigation/context_helpers.rb +24 -9
- data/lib/shoko/core/services/navigation/dynamic_change_applier.rb +5 -8
- data/lib/shoko/core/services/navigation/image_offset_snapper.rb +35 -16
- data/lib/shoko/core/services/navigation/state_updater.rb +13 -10
- data/lib/shoko/core/services/navigation_service.rb +80 -69
- data/lib/shoko/core/services/null_instrumentation.rb +24 -0
- data/lib/shoko/core/services/null_logger.rb +39 -0
- data/lib/shoko/core/services/page_calculator_service.rb +95 -88
- data/lib/shoko/core/services/pagination/internal/absolute_page_map_builder.rb +27 -19
- data/lib/shoko/core/services/pagination/internal/chapter_cache.rb +56 -45
- data/lib/shoko/core/services/pagination/internal/dynamic_page_map_builder.rb +163 -110
- data/lib/shoko/core/services/pagination/internal/layout_metrics_calculator.rb +59 -57
- data/lib/shoko/core/services/pagination/internal/page_hydrator.rb +109 -120
- data/lib/shoko/core/services/pagination/internal/pagination_workflow.rb +125 -128
- data/lib/shoko/core/services/pagination/page_info_calculator.rb +238 -240
- data/lib/shoko/core/services/pagination/pagination_cache_preloader.rb +197 -166
- data/lib/shoko/core/services/pagination/pagination_coordinator.rb +188 -159
- data/lib/shoko/core/services/pagination/pagination_orchestrator.rb +280 -239
- data/lib/shoko/core/services/progress_helper.rb +10 -10
- data/lib/shoko/core/services/selection_service.rb +70 -48
- data/lib/shoko/shared/errors.rb +53 -0
- data/lib/shoko/shared/optional_dependency.rb +50 -0
- data/lib/shoko/shared/unicode_display_width/display_width.marshal.gz +0 -0
- data/lib/shoko/shared/unicode_display_width.rb +147 -0
- data/lib/shoko/shared/version.rb +1 -1
- data/lib/shoko/test_support/test_mode.rb +1 -1
- data/lib/shoko.rb +142 -147
- metadata +102 -66
- data/lib/shoko/adapters/output/ui/components/component_interface.rb +0 -80
- data/lib/shoko/application/state/actions/update_annotation_editor_overlay_action.rb +0 -27
- data/lib/shoko/application/state/actions/update_annotations_action.rb +0 -20
- data/lib/shoko/application/state/actions/update_annotations_overlay_action.rb +0 -27
- data/lib/shoko/application/state/actions/update_bookmarks_action.rb +0 -20
- data/lib/shoko/application/state/actions/update_chapter_action.rb +0 -24
- data/lib/shoko/application/state/actions/update_popup_menu_action.rb +0 -27
- data/lib/shoko/application/state/actions/update_reader_mode_action.rb +0 -20
- 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/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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a7a36fd324fdba1eed316344cfab543f5fef1bfa863c1b25fc3e644502978054
|
|
4
|
+
data.tar.gz: 5fc5820176929bd037c545901cdc0c611e98b593a90fc8da7a54d4a01b0cc359
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e29b072b4f0dc64eb6a18f36d3fcc1bf37fa9efa904b1827cccbe85c4c37860073e04c3eddc87fdab827b9d02e03958f54bd1714d58644cdff026caaacf93bfe
|
|
7
|
+
data.tar.gz: 0ffb950539fe1440f26149b1d75755811f2aaf541f900f6ab89051dc9e4e2550c1a07a9de32d83a3d6bac5a5f25a1184f7830cf3b39866eed458b71081fe9248
|
data/.bundle/config
CHANGED
data/.rubocop.yml
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
plugins:
|
|
2
2
|
- rubocop-performance
|
|
3
|
-
- rubocop-rails
|
|
4
3
|
|
|
5
4
|
AllCops:
|
|
6
5
|
TargetRubyVersion: 3.3
|
|
@@ -36,24 +35,45 @@ Metrics/MethodLength:
|
|
|
36
35
|
CountComments: false
|
|
37
36
|
AllowedMethods:
|
|
38
37
|
- 'initialize'
|
|
38
|
+
# Data schema definition - just a hash literal
|
|
39
|
+
- 'build_initial_state'
|
|
40
|
+
# Character-by-character algorithms where splitting obscures logic
|
|
41
|
+
- 'sanitize'
|
|
42
|
+
- 'wrap_cells'
|
|
43
|
+
- 'truncate_to'
|
|
39
44
|
Exclude:
|
|
40
45
|
- 'spec/**/*'
|
|
41
46
|
- 'test/**/*'
|
|
42
47
|
|
|
43
48
|
Metrics/AbcSize:
|
|
44
49
|
Max: 20
|
|
50
|
+
AllowedMethods:
|
|
51
|
+
# Character-by-character algorithms
|
|
52
|
+
- 'wrap_cells'
|
|
53
|
+
- 'sanitize'
|
|
54
|
+
- 'truncate_to'
|
|
45
55
|
Exclude:
|
|
46
56
|
- 'spec/**/*'
|
|
47
57
|
- 'test/**/*'
|
|
48
58
|
|
|
49
59
|
Metrics/CyclomaticComplexity:
|
|
50
60
|
Max: 8
|
|
61
|
+
AllowedMethods:
|
|
62
|
+
# Character-by-character algorithms - inherent complexity
|
|
63
|
+
- 'sanitize'
|
|
64
|
+
- 'truncate_to'
|
|
65
|
+
- 'wrap_cells'
|
|
51
66
|
Exclude:
|
|
52
67
|
- 'spec/**/*'
|
|
53
68
|
- 'test/**/*'
|
|
54
69
|
|
|
55
70
|
Metrics/PerceivedComplexity:
|
|
56
71
|
Max: 9
|
|
72
|
+
AllowedMethods:
|
|
73
|
+
# Character-by-character algorithms - inherent complexity
|
|
74
|
+
- 'sanitize'
|
|
75
|
+
- 'truncate_to'
|
|
76
|
+
- 'wrap_cells'
|
|
57
77
|
Exclude:
|
|
58
78
|
- 'spec/**/*'
|
|
59
79
|
- 'test/**/*'
|
|
@@ -71,6 +91,20 @@ Metrics/BlockLength:
|
|
|
71
91
|
- 'spec/**/*'
|
|
72
92
|
- 'test/**/*'
|
|
73
93
|
|
|
94
|
+
# Naming
|
|
95
|
+
Naming/PredicateMethod:
|
|
96
|
+
# Allow handler methods that return boolean for control flow
|
|
97
|
+
AllowedMethods:
|
|
98
|
+
- handle_overlay_click
|
|
99
|
+
- handle_sidebar_interaction
|
|
100
|
+
- handle_sidebar_tab_click
|
|
101
|
+
- handle_sidebar_toc_click
|
|
102
|
+
- handle_sidebar_wheel
|
|
103
|
+
- handle_sidebar_scroll_drag
|
|
104
|
+
- start_sidebar_scroll_drag
|
|
105
|
+
- cancel_via_ui
|
|
106
|
+
- dispatch_to_mode
|
|
107
|
+
|
|
74
108
|
# Style
|
|
75
109
|
Style/Documentation:
|
|
76
110
|
Enabled: true
|
|
@@ -119,6 +153,3 @@ Lint/UselessAssignment:
|
|
|
119
153
|
|
|
120
154
|
Lint/DuplicateMethods:
|
|
121
155
|
Enabled: true
|
|
122
|
-
|
|
123
|
-
Rails:
|
|
124
|
-
Enabled: false
|
data/Gemfile
CHANGED
data/Shoko.gemspec
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/shoko/shared/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'shoko'
|
|
7
|
+
spec.version = Shoko::VERSION
|
|
8
|
+
spec.authors = ['Shoko']
|
|
9
|
+
spec.email = ['ruby.computer770@passinbox.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'Terminal EBook Reader'
|
|
12
|
+
spec.description = 'Terminal EBook Reader'
|
|
13
|
+
spec.homepage = 'https://sr.ht/~shayan/Shoko/'
|
|
14
|
+
spec.license = 'MIT'
|
|
15
|
+
spec.required_ruby_version = '>= 4.0.0'
|
|
16
|
+
|
|
17
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
18
|
+
spec.metadata['source_code_uri'] = spec.homepage
|
|
19
|
+
|
|
20
|
+
# Specify which files should be added to the gem
|
|
21
|
+
spec.files = Dir.chdir(__dir__) do
|
|
22
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
23
|
+
f.end_with?('.gem') ||
|
|
24
|
+
(f == __FILE__) ||
|
|
25
|
+
f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|circleci)|appveyor)})
|
|
26
|
+
end.select { |f| File.file?(f) }
|
|
27
|
+
end
|
|
28
|
+
spec.bindir = 'bin'
|
|
29
|
+
spec.executables = %w[shoko start]
|
|
30
|
+
spec.require_paths = ['lib']
|
|
31
|
+
|
|
32
|
+
# Development dependencies are managed in the Gemfile
|
|
33
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
34
|
+
end
|
data/bin/shoko
CHANGED
|
@@ -6,8 +6,8 @@ lib_dir = File.join(app_root, 'lib')
|
|
|
6
6
|
$LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir)
|
|
7
7
|
|
|
8
8
|
begin
|
|
9
|
-
|
|
10
|
-
if
|
|
9
|
+
# Only activate Bundler when explicitly running under it (e.g. bundle exec).
|
|
10
|
+
if ENV['BUNDLE_GEMFILE'] || ENV['BUNDLE_BIN_PATH']
|
|
11
11
|
require 'bundler/setup'
|
|
12
12
|
end
|
|
13
13
|
rescue LoadError
|
data/bin/start
CHANGED
|
@@ -6,8 +6,8 @@ lib_dir = File.join(app_root, 'lib')
|
|
|
6
6
|
$LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir)
|
|
7
7
|
|
|
8
8
|
begin
|
|
9
|
-
|
|
10
|
-
if
|
|
9
|
+
# Only activate Bundler when explicitly running under it (e.g. bundle exec).
|
|
10
|
+
if ENV['BUNDLE_GEMFILE'] || ENV['BUNDLE_BIN_PATH']
|
|
11
11
|
require 'bundler/setup'
|
|
12
12
|
end
|
|
13
13
|
rescue LoadError
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shoko
|
|
4
|
+
module Adapters
|
|
5
|
+
# Base class for adapters in the hexagonal architecture.
|
|
6
|
+
# Adapters connect ports to external infrastructure (UI, databases, etc.)
|
|
7
|
+
# and should NOT extend Core's BaseService.
|
|
8
|
+
#
|
|
9
|
+
# This class provides a simple dependency injection pattern via constructor kwargs
|
|
10
|
+
# without the `resolve()` semantics of BaseService.
|
|
11
|
+
class BaseAdapter
|
|
12
|
+
# @param logger [Object, nil] Optional logger for debugging
|
|
13
|
+
def initialize(logger: nil)
|
|
14
|
+
@logger = logger
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
protected
|
|
18
|
+
|
|
19
|
+
attr_reader :logger
|
|
20
|
+
|
|
21
|
+
def log_debug(message, **context)
|
|
22
|
+
logger&.debug(message, **context)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def log_error(message, **context)
|
|
26
|
+
logger&.error(message, **context)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative '../monitoring/performance_monitor.rb'
|
|
4
3
|
require_relative 'epub_document'
|
|
5
4
|
|
|
6
5
|
module Shoko
|
|
@@ -8,8 +7,14 @@ module Shoko
|
|
|
8
7
|
# Document service for loading and accessing EPUB content.
|
|
9
8
|
# Provides clean interface to document operations without coupling to controllers.
|
|
10
9
|
class DocumentService
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
# @param epub_path [String] Path to EPUB file
|
|
11
|
+
# @param wrapping_service [Object, nil] Wrapping service
|
|
12
|
+
# @param formatting_service [Object, nil] Formatting service
|
|
13
|
+
# @param background_worker [Object, nil] Background worker
|
|
14
|
+
# @param progress_reporter [Object, nil] Progress reporter
|
|
15
|
+
# @param logger [Core::Ports::Logging] Logger adapter (required)
|
|
16
|
+
def initialize(epub_path, wrapping_service = nil, logger:, formatting_service: nil, background_worker: nil,
|
|
17
|
+
progress_reporter: nil, instrumentation: nil)
|
|
13
18
|
@epub_path = epub_path
|
|
14
19
|
@document = nil
|
|
15
20
|
@content_cache = {}
|
|
@@ -17,20 +22,24 @@ module Shoko
|
|
|
17
22
|
@formatting_service = formatting_service
|
|
18
23
|
@background_worker = background_worker
|
|
19
24
|
@progress_reporter = progress_reporter
|
|
25
|
+
@logger = logger
|
|
26
|
+
@instrumentation = instrumentation
|
|
20
27
|
end
|
|
21
28
|
|
|
22
29
|
# Load the EPUB document
|
|
23
30
|
#
|
|
24
31
|
# @return [EPUBDocument] Loaded document
|
|
25
32
|
def load_document
|
|
26
|
-
@document ||=
|
|
33
|
+
@document ||= instrument('import.document.load') do
|
|
27
34
|
EPUBDocument.new(@epub_path,
|
|
28
35
|
formatting_service: @formatting_service,
|
|
29
36
|
background_worker: @background_worker,
|
|
30
|
-
progress_reporter: @progress_reporter
|
|
37
|
+
progress_reporter: @progress_reporter,
|
|
38
|
+
logger: @logger,
|
|
39
|
+
instrumentation: @instrumentation)
|
|
31
40
|
end
|
|
32
41
|
rescue StandardError => e
|
|
33
|
-
|
|
42
|
+
@logger.error('Failed to load document', path: @epub_path, error: e.message)
|
|
34
43
|
create_error_document(e.message)
|
|
35
44
|
end
|
|
36
45
|
|
|
@@ -155,6 +164,14 @@ module Shoko
|
|
|
155
164
|
|
|
156
165
|
yield chapter
|
|
157
166
|
end
|
|
167
|
+
|
|
168
|
+
def instrument(label, &)
|
|
169
|
+
if @instrumentation
|
|
170
|
+
@instrumentation.measure(label, &)
|
|
171
|
+
else
|
|
172
|
+
yield
|
|
173
|
+
end
|
|
174
|
+
end
|
|
158
175
|
end
|
|
159
176
|
|
|
160
177
|
# Simple error document for when EPUB loading fails
|
|
@@ -1,95 +1,91 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'fileutils'
|
|
4
|
-
require_relative '
|
|
4
|
+
require_relative '../base_adapter'
|
|
5
5
|
require_relative '../storage/config_paths'
|
|
6
6
|
|
|
7
7
|
module Shoko
|
|
8
8
|
module Adapters::BookSources
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
books: normalize_books(payload['results']),
|
|
20
|
-
}
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def download(book)
|
|
24
|
-
url = pick_download_url(book)
|
|
25
|
-
raise DownloadError, 'No EPUB format available' unless url
|
|
26
|
-
|
|
27
|
-
dest_dir = downloads_root
|
|
28
|
-
FileUtils.mkdir_p(dest_dir)
|
|
29
|
-
dest_path = File.join(dest_dir, filename_for(book))
|
|
30
|
-
return { path: dest_path, existing: true } if File.exist?(dest_path)
|
|
9
|
+
# Coordinates Gutendex search + download to the local library.
|
|
10
|
+
class DownloadService < Shoko::Adapters::BaseAdapter
|
|
11
|
+
class DownloadError < StandardError; end
|
|
12
|
+
|
|
13
|
+
# @param gutendex_client [Object] Client for Gutendex API
|
|
14
|
+
# @param logger [Object, nil] Optional logger
|
|
15
|
+
def initialize(gutendex_client:, logger: nil)
|
|
16
|
+
super(logger: logger)
|
|
17
|
+
@client = gutendex_client
|
|
18
|
+
end
|
|
31
19
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
20
|
+
def search(query:, page_url: nil)
|
|
21
|
+
payload = @client.search(query: query, page_url: page_url)
|
|
22
|
+
{
|
|
23
|
+
count: payload['count'].to_i,
|
|
24
|
+
next: payload['next'],
|
|
25
|
+
previous: payload['previous'],
|
|
26
|
+
books: normalize_books(payload['results']),
|
|
27
|
+
}
|
|
28
|
+
end
|
|
35
29
|
|
|
36
|
-
|
|
30
|
+
def download(book)
|
|
31
|
+
url = pick_download_url(book)
|
|
32
|
+
raise DownloadError, 'No EPUB format available' unless url
|
|
37
33
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
dest_dir = downloads_root
|
|
35
|
+
FileUtils.mkdir_p(dest_dir)
|
|
36
|
+
dest_path = File.join(dest_dir, filename_for(book))
|
|
37
|
+
return { path: dest_path, existing: true } if File.exist?(dest_path)
|
|
41
38
|
|
|
42
|
-
|
|
39
|
+
@client.download(url, dest_path) { |done, total| yield(done, total) if block_given? }
|
|
40
|
+
{ path: dest_path, existing: false }
|
|
41
|
+
end
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
@client ||= resolve(:gutendex_client)
|
|
46
|
-
end
|
|
43
|
+
private
|
|
47
44
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
def downloads_root
|
|
46
|
+
Adapters::Storage::ConfigPaths.downloads_root
|
|
47
|
+
end
|
|
51
48
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
end
|
|
49
|
+
def normalize_books(items)
|
|
50
|
+
Array(items).map do |raw|
|
|
51
|
+
{
|
|
52
|
+
id: raw['id'],
|
|
53
|
+
title: raw['title'],
|
|
54
|
+
authors: Array(raw['authors']).filter_map { |a| a['name'] },
|
|
55
|
+
languages: Array(raw['languages']).map(&:to_s),
|
|
56
|
+
download_count: raw['download_count'],
|
|
57
|
+
formats: raw['formats'] || {},
|
|
58
|
+
}
|
|
63
59
|
end
|
|
60
|
+
end
|
|
64
61
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
62
|
+
def pick_download_url(book)
|
|
63
|
+
formats = value_for(book, :formats, 'formats', {})
|
|
64
|
+
return nil unless formats.respond_to?(:each)
|
|
68
65
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
66
|
+
keys = formats.keys.map(&:to_s)
|
|
67
|
+
epub_key = keys.find { |k| k.start_with?('application/epub+zip') } ||
|
|
68
|
+
keys.find { |k| k.include?('application/epub') } ||
|
|
69
|
+
keys.find { |k| k.include?('epub') }
|
|
70
|
+
return nil unless epub_key
|
|
74
71
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def filename_for(book)
|
|
79
|
-
id = value_for(book, :id, 'id', 'book').to_s
|
|
80
|
-
title = value_for(book, :title, 'title', 'book').to_s
|
|
81
|
-
slug = title.downcase.gsub(/[^a-z0-9]+/, '-').gsub(/^-|-$/, '')
|
|
82
|
-
slug = "book-#{id}" if slug.empty?
|
|
83
|
-
"#{slug}-#{id}.epub"
|
|
84
|
-
end
|
|
72
|
+
formats[epub_key] || formats[epub_key.to_sym]
|
|
73
|
+
end
|
|
85
74
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
75
|
+
def filename_for(book)
|
|
76
|
+
id = value_for(book, :id, 'id', 'book').to_s
|
|
77
|
+
title = value_for(book, :title, 'title', 'book').to_s
|
|
78
|
+
slug = title.downcase.gsub(/[^a-z0-9]+/, '-').gsub(/^-|-$/, '')
|
|
79
|
+
slug = "book-#{id}" if slug.empty?
|
|
80
|
+
"#{slug}-#{id}.epub"
|
|
81
|
+
end
|
|
89
82
|
|
|
90
|
-
|
|
91
|
-
|
|
83
|
+
def value_for(book, key_sym, key_str, default)
|
|
84
|
+
return book[key_sym] if book.respond_to?(:key?) && book.key?(key_sym)
|
|
85
|
+
return book[key_str] if book.respond_to?(:key?) && book.key?(key_str)
|
|
92
86
|
|
|
87
|
+
default
|
|
93
88
|
end
|
|
89
|
+
end
|
|
94
90
|
end
|
|
95
91
|
end
|
|
@@ -3,9 +3,8 @@
|
|
|
3
3
|
require 'digest'
|
|
4
4
|
require 'zip'
|
|
5
5
|
|
|
6
|
-
require_relative '../../storage/atomic_file_writer
|
|
7
|
-
require_relative '../../storage/cache_paths
|
|
8
|
-
require_relative '../../monitoring/logger.rb'
|
|
6
|
+
require_relative '../../storage/atomic_file_writer'
|
|
7
|
+
require_relative '../../storage/cache_paths'
|
|
9
8
|
|
|
10
9
|
module Shoko
|
|
11
10
|
module Adapters::BookSources::Epub
|
|
@@ -14,8 +13,9 @@ module Shoko
|
|
|
14
13
|
class EpubResourceLoader
|
|
15
14
|
SHA256_HEX_PATTERN = /\A[0-9a-f]{64}\z/i
|
|
16
15
|
|
|
17
|
-
def initialize(cache_root: CachePaths.cache_root)
|
|
16
|
+
def initialize(cache_root: Shoko::Adapters::Storage::CachePaths.cache_root, logger: nil)
|
|
18
17
|
@cache_root = cache_root
|
|
18
|
+
@logger = logger
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
# Fetch an entry from the per-book blob cache or from the EPUB archive.
|
|
@@ -101,11 +101,12 @@ module Shoko
|
|
|
101
101
|
data
|
|
102
102
|
end
|
|
103
103
|
rescue Zip::Error => e
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
@logger&.debug('EpubResourceLoader: zip read failed', path: epub_path.to_s, entry: entry_path.to_s,
|
|
105
|
+
error: e.message)
|
|
106
106
|
nil
|
|
107
107
|
rescue StandardError => e
|
|
108
|
-
|
|
108
|
+
@logger&.debug('EpubResourceLoader: read failed', path: epub_path.to_s,
|
|
109
|
+
entry: entry_path.to_s, error: e.message)
|
|
109
110
|
nil
|
|
110
111
|
end
|
|
111
112
|
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'cgi'
|
|
4
4
|
|
|
5
|
-
require_relative '../../../
|
|
6
|
-
require_relative '../../../output/terminal/terminal_sanitizer.rb'
|
|
5
|
+
require_relative '../../../output/terminal/terminal_sanitizer'
|
|
7
6
|
|
|
8
7
|
module Shoko
|
|
9
8
|
module Adapters::BookSources::Epub::Parsers
|
|
@@ -16,11 +15,7 @@ module Shoko
|
|
|
16
15
|
end
|
|
17
16
|
|
|
18
17
|
def self.html_to_text(html)
|
|
19
|
-
|
|
20
|
-
Shoko::Adapters::Monitoring::PerfTracer.measure('xhtml.normalize') { normalize_html(html) }
|
|
21
|
-
else
|
|
22
|
-
normalize_html(html)
|
|
23
|
-
end
|
|
18
|
+
normalize_html(html)
|
|
24
19
|
end
|
|
25
20
|
|
|
26
21
|
BLOCK_REPLACEMENTS = {
|
|
@@ -4,6 +4,7 @@ require 'zip'
|
|
|
4
4
|
require 'rexml/document'
|
|
5
5
|
|
|
6
6
|
require_relative 'opf_processor'
|
|
7
|
+
require_relative 'rexml_safe_parser'
|
|
7
8
|
|
|
8
9
|
module Shoko
|
|
9
10
|
module Adapters::BookSources::Epub::Parsers
|
|
@@ -25,7 +26,7 @@ module Shoko
|
|
|
25
26
|
|
|
26
27
|
def self.find_opf_path(zip)
|
|
27
28
|
container_xml = zip.read('META-INF/container.xml')
|
|
28
|
-
container =
|
|
29
|
+
container = REXMLSafeParser.parse(container_xml)
|
|
29
30
|
rootfile = container.elements['//rootfile']
|
|
30
31
|
return nil unless rootfile
|
|
31
32
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'rexml/document'
|
|
4
4
|
|
|
5
5
|
require_relative 'navigation_context'
|
|
6
|
+
require_relative '../rexml_safe_parser'
|
|
6
7
|
require_relative 'navigation_walker'
|
|
7
8
|
require_relative 'navigation_result'
|
|
8
9
|
|
|
@@ -78,7 +79,7 @@ module Shoko
|
|
|
78
79
|
content = @entry_reader.safe_read_entry(path)
|
|
79
80
|
return nil unless content
|
|
80
81
|
|
|
81
|
-
doc =
|
|
82
|
+
doc = REXMLSafeParser.parse(content)
|
|
82
83
|
nav_list_from_document(doc)
|
|
83
84
|
rescue REXML::ParseException
|
|
84
85
|
nil
|
|
@@ -93,7 +94,7 @@ module Shoko
|
|
|
93
94
|
|
|
94
95
|
def nav_map_from_path(path)
|
|
95
96
|
ncx_content = @entry_reader.read_entry(path)
|
|
96
|
-
ncx =
|
|
97
|
+
ncx = REXMLSafeParser.parse(ncx_content)
|
|
97
98
|
ncx.elements['//navMap']
|
|
98
99
|
rescue StandardError
|
|
99
100
|
nil
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require 'cgi'
|
|
4
4
|
require 'rexml/document'
|
|
5
5
|
|
|
6
|
-
require_relative '
|
|
6
|
+
require_relative 'rexml_safe_parser'
|
|
7
7
|
require_relative 'opf/entry_reader'
|
|
8
8
|
require_relative 'opf/metadata_extractor'
|
|
9
9
|
require_relative 'opf/navigation_extractor'
|
|
@@ -18,12 +18,13 @@ module Shoko
|
|
|
18
18
|
|
|
19
19
|
attr_reader :toc_entries
|
|
20
20
|
|
|
21
|
-
def initialize(opf_path, zip: nil)
|
|
21
|
+
def initialize(opf_path, zip: nil, instrumentation: nil)
|
|
22
22
|
@opf_path = opf_path
|
|
23
|
+
@instrumentation = instrumentation
|
|
23
24
|
@entry_reader = OPFEntryReader.new(opf_path, zip: zip)
|
|
24
25
|
content = read_opf_content
|
|
25
|
-
@opf =
|
|
26
|
-
|
|
26
|
+
@opf = instrument('opf.parse') do
|
|
27
|
+
REXMLSafeParser.parse(content)
|
|
27
28
|
end
|
|
28
29
|
@toc_entries = []
|
|
29
30
|
@navigation_extractor = OPFNavigationExtractor.new(opf: @opf, entry_reader: @entry_reader)
|
|
@@ -66,7 +67,7 @@ module Shoko
|
|
|
66
67
|
|
|
67
68
|
def read_opf_content
|
|
68
69
|
raw = if @entry_reader.zip?
|
|
69
|
-
|
|
70
|
+
instrument('zip.read') do
|
|
70
71
|
@entry_reader.read_raw(@opf_path)
|
|
71
72
|
end
|
|
72
73
|
else
|
|
@@ -75,6 +76,14 @@ module Shoko
|
|
|
75
76
|
@entry_reader.normalize_xml_text(raw)
|
|
76
77
|
end
|
|
77
78
|
|
|
79
|
+
def instrument(label, &)
|
|
80
|
+
if @instrumentation
|
|
81
|
+
@instrumentation.measure(label, &)
|
|
82
|
+
else
|
|
83
|
+
yield
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
78
87
|
def manifest_item_id_href(item)
|
|
79
88
|
attrs = item.attributes
|
|
80
89
|
[attrs['id'], attrs['href']]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rexml/document'
|
|
4
|
+
require 'rexml/security'
|
|
5
|
+
|
|
6
|
+
module Shoko
|
|
7
|
+
module Adapters::BookSources::Epub::Parsers
|
|
8
|
+
# Centralized REXML parser with hardened entity expansion limits.
|
|
9
|
+
module REXMLSafeParser
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
DEFAULT_ENTITY_EXPANSION_LIMIT = 10_000
|
|
13
|
+
DEFAULT_ENTITY_EXPANSION_TEXT_LIMIT = 2_000_000
|
|
14
|
+
|
|
15
|
+
def parse(xml)
|
|
16
|
+
apply_security_limits
|
|
17
|
+
REXML::Document.new(xml)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def apply_security_limits
|
|
21
|
+
limit = integer_env('SHOKO_REXML_ENTITY_LIMIT', DEFAULT_ENTITY_EXPANSION_LIMIT)
|
|
22
|
+
text_limit = integer_env('SHOKO_REXML_TEXT_LIMIT', DEFAULT_ENTITY_EXPANSION_TEXT_LIMIT)
|
|
23
|
+
|
|
24
|
+
if defined?(REXML::Security) && REXML::Security.respond_to?(:entity_expansion_limit=)
|
|
25
|
+
REXML::Security.entity_expansion_limit = limit
|
|
26
|
+
end
|
|
27
|
+
if defined?(REXML::Security) && REXML::Security.respond_to?(:entity_expansion_text_limit=)
|
|
28
|
+
REXML::Security.entity_expansion_text_limit = text_limit
|
|
29
|
+
end
|
|
30
|
+
if defined?(REXML::Document) && REXML::Document.respond_to?(:entity_expansion_text_limit=)
|
|
31
|
+
REXML::Document.entity_expansion_text_limit = text_limit
|
|
32
|
+
end
|
|
33
|
+
rescue StandardError
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
private_class_method :apply_security_limits
|
|
37
|
+
|
|
38
|
+
def integer_env(key, fallback)
|
|
39
|
+
value = ENV.fetch(key, '').to_s.strip
|
|
40
|
+
return fallback if value.empty?
|
|
41
|
+
|
|
42
|
+
parsed = value.to_i
|
|
43
|
+
parsed.positive? ? parsed : fallback
|
|
44
|
+
rescue StandardError
|
|
45
|
+
fallback
|
|
46
|
+
end
|
|
47
|
+
private_class_method :integer_env
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|