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.
Files changed (318) hide show
  1. checksums.yaml +4 -4
  2. data/.bundle/config +0 -1
  3. data/.rubocop.yml +35 -4
  4. data/Gemfile +1 -0
  5. data/Shoko.gemspec +34 -0
  6. data/bin/shoko +2 -2
  7. data/bin/start +2 -2
  8. data/lib/shoko/adapters/base_adapter.rb +30 -0
  9. data/lib/shoko/adapters/book_sources/document_service.rb +23 -6
  10. data/lib/shoko/adapters/book_sources/download_service.rb +67 -71
  11. data/lib/shoko/adapters/book_sources/epub/epub_resource_loader.rb +8 -7
  12. data/lib/shoko/adapters/book_sources/epub/parsers/html_processor.rb +2 -7
  13. data/lib/shoko/adapters/book_sources/epub/parsers/metadata_extractor.rb +2 -1
  14. data/lib/shoko/adapters/book_sources/epub/parsers/opf/navigation_traversal.rb +3 -2
  15. data/lib/shoko/adapters/book_sources/epub/parsers/opf_processor.rb +14 -5
  16. data/lib/shoko/adapters/book_sources/epub/parsers/rexml_safe_parser.rb +50 -0
  17. data/lib/shoko/adapters/book_sources/epub/parsers/xhtml_content_parser.rb +630 -507
  18. data/lib/shoko/adapters/book_sources/epub/parsers/xml_text_normalizer.rb +1 -1
  19. data/lib/shoko/adapters/book_sources/epub_document.rb +206 -193
  20. data/lib/shoko/adapters/book_sources/epub_finder/directory_scanner.rb +4 -3
  21. data/lib/shoko/adapters/book_sources/epub_finder.rb +10 -12
  22. data/lib/shoko/adapters/book_sources/epub_importer.rb +27 -17
  23. data/lib/shoko/adapters/book_sources/gutendex_client.rb +43 -27
  24. data/lib/shoko/adapters/book_sources/library_scanner.rb +44 -7
  25. data/lib/shoko/adapters/book_sources/metadata_reader_adapter.rb +21 -0
  26. data/lib/shoko/adapters/input/annotations/mouse_handler.rb +1 -1
  27. data/lib/shoko/adapters/input/command_bridge.rb +60 -127
  28. data/lib/shoko/adapters/input/command_factory.rb +104 -9
  29. data/lib/shoko/adapters/input/commands.rb +7 -5
  30. data/lib/shoko/adapters/input/input_controller.rb +266 -186
  31. data/lib/shoko/adapters/input/input_system_factory_adapter.rb +28 -0
  32. data/lib/shoko/adapters/input/key_classifier_adapter.rb +66 -0
  33. data/lib/shoko/adapters/input/validators/file_path_validator.rb +2 -2
  34. data/lib/shoko/adapters/input/validators/terminal_size_validator.rb +1 -1
  35. data/lib/shoko/adapters/monitoring/logger_adapter.rb +119 -0
  36. data/lib/shoko/adapters/monitoring/perf_tracer.rb +16 -19
  37. data/lib/shoko/adapters/monitoring/performance_monitor.rb +90 -97
  38. data/lib/shoko/adapters/output/clipboard/clipboard_service.rb +80 -90
  39. data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler/image_builder.rb +118 -118
  40. data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler/table_renderer.rb +361 -0
  41. data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler/text_wrapper.rb +106 -106
  42. data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler/tokenizer.rb +75 -62
  43. data/lib/shoko/adapters/output/formatting/formatting_service/line_assembler.rb +226 -102
  44. data/lib/shoko/adapters/output/formatting/formatting_service/plain_lines_builder.rb +39 -39
  45. data/lib/shoko/adapters/output/formatting/formatting_service.rb +221 -197
  46. data/lib/shoko/adapters/output/formatting/wrapping_service.rb +164 -182
  47. data/lib/shoko/adapters/output/instrumentation_service.rb +52 -34
  48. data/lib/shoko/adapters/output/kitty/display_capabilities.rb +21 -0
  49. data/lib/shoko/adapters/output/kitty/kitty_image_renderer.rb +10 -5
  50. data/lib/shoko/adapters/output/kitty/kitty_unicode_placeholders.rb +125 -125
  51. data/lib/shoko/adapters/output/notification_service.rb +54 -39
  52. data/lib/shoko/adapters/output/render_registry.rb +0 -5
  53. data/lib/shoko/adapters/output/rendering/models/line_geometry.rb +3 -3
  54. data/lib/shoko/adapters/output/rendering/models/rendering_context.rb +14 -9
  55. data/lib/shoko/adapters/output/terminal/buffer.rb +263 -206
  56. data/lib/shoko/adapters/output/terminal/input/decoder.rb +298 -298
  57. data/lib/shoko/adapters/output/terminal/input.rb +76 -0
  58. data/lib/shoko/adapters/output/terminal/output.rb +83 -81
  59. data/lib/shoko/adapters/output/terminal/terminal.rb +58 -16
  60. data/lib/shoko/adapters/output/terminal/terminal_sanitizer.rb +201 -201
  61. data/lib/shoko/adapters/output/terminal/terminal_service.rb +121 -106
  62. data/lib/shoko/adapters/output/terminal/text_metrics.rb +218 -219
  63. data/lib/shoko/adapters/output/terminal/text_sanitizer_adapter.rb +19 -0
  64. data/lib/shoko/adapters/output/terminal_capabilities_adapter.rb +23 -0
  65. data/lib/shoko/adapters/output/ui/component_factory.rb +84 -0
  66. data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay/footer_renderer.rb +2 -2
  67. data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay/note_renderer.rb +5 -4
  68. data/lib/shoko/adapters/output/ui/components/annotation_editor_overlay_component.rb +280 -107
  69. data/lib/shoko/adapters/output/ui/components/annotations_overlay/list_renderer.rb +9 -7
  70. data/lib/shoko/adapters/output/ui/components/annotations_overlay_component.rb +13 -3
  71. data/lib/shoko/adapters/output/ui/components/base_component.rb +9 -5
  72. data/lib/shoko/adapters/output/ui/components/content_component.rb +11 -3
  73. data/lib/shoko/adapters/output/ui/components/dictionary/entry_formatter.rb +228 -0
  74. data/lib/shoko/adapters/output/ui/components/dictionary_panel_component.rb +266 -0
  75. data/lib/shoko/adapters/output/ui/components/dictionary_popup_component.rb +251 -0
  76. data/lib/shoko/adapters/output/ui/components/enhanced_popup_menu.rb +19 -7
  77. data/lib/shoko/adapters/output/ui/components/footer_component.rb +5 -3
  78. data/lib/shoko/adapters/output/ui/components/header_component.rb +1 -1
  79. data/lib/shoko/adapters/output/ui/components/layouts/horizontal_three.rb +90 -0
  80. data/lib/shoko/adapters/output/ui/components/main_menu_component.rb +9 -6
  81. data/lib/shoko/adapters/output/ui/components/reading/base_view_renderer.rb +72 -21
  82. data/lib/shoko/adapters/output/ui/components/reading/config_helpers.rb +19 -11
  83. data/lib/shoko/adapters/output/ui/components/reading/inline_segment_highlighter.rb +25 -4
  84. data/lib/shoko/adapters/output/ui/components/reading/kitty_image_line_renderer.rb +18 -6
  85. data/lib/shoko/adapters/output/ui/components/reading/line_content_composer.rb +2 -2
  86. data/lib/shoko/adapters/output/ui/components/reading/line_drawer.rb +16 -8
  87. data/lib/shoko/adapters/output/ui/components/reading/line_geometry_builder.rb +2 -2
  88. data/lib/shoko/adapters/output/ui/components/reading/single_view_renderer.rb +5 -5
  89. data/lib/shoko/adapters/output/ui/components/reading/split_view_renderer.rb +7 -9
  90. data/lib/shoko/adapters/output/ui/components/reading/view_renderer_factory.rb +13 -4
  91. data/lib/shoko/adapters/output/ui/components/screens/annotation_detail_screen_component.rb +25 -97
  92. data/lib/shoko/adapters/output/ui/components/screens/annotation_edit_screen_component.rb +58 -100
  93. data/lib/shoko/adapters/output/ui/components/screens/annotation_editor_screen_component.rb +40 -6
  94. data/lib/shoko/adapters/output/ui/components/screens/annotation_rendering_helpers.rb +297 -21
  95. data/lib/shoko/adapters/output/ui/components/screens/annotations_screen_component.rb +93 -158
  96. data/lib/shoko/adapters/output/ui/components/screens/base_screen_component.rb +1 -1
  97. data/lib/shoko/adapters/output/ui/components/screens/browse_screen_component.rb +49 -36
  98. data/lib/shoko/adapters/output/ui/components/screens/dictionary_settings_screen_component.rb +457 -0
  99. data/lib/shoko/adapters/output/ui/components/screens/download_books_screen_component.rb +53 -36
  100. data/lib/shoko/adapters/output/ui/components/screens/library_screen_component.rb +81 -53
  101. data/lib/shoko/adapters/output/ui/components/screens/loading_overlay_component.rb +13 -4
  102. data/lib/shoko/adapters/output/ui/components/screens/menu_screen_component.rb +11 -3
  103. data/lib/shoko/adapters/output/ui/components/screens/settings_screen_component.rb +96 -13
  104. data/lib/shoko/adapters/output/ui/components/sidebar/annotations_tab_renderer.rb +13 -3
  105. data/lib/shoko/adapters/output/ui/components/sidebar/bookmarks_tab_renderer.rb +19 -2
  106. data/lib/shoko/adapters/output/ui/components/sidebar/tab_header_component.rb +13 -3
  107. data/lib/shoko/adapters/output/ui/components/sidebar/toc_tab_renderer.rb +22 -11
  108. data/lib/shoko/adapters/output/ui/components/sidebar/toc_tab_support.rb +18 -21
  109. data/lib/shoko/adapters/output/ui/components/sidebar_panel_component.rb +19 -16
  110. data/lib/shoko/adapters/output/ui/components/surface.rb +2 -2
  111. data/lib/shoko/adapters/output/ui/components/tooltip_overlay_component.rb +38 -12
  112. data/lib/shoko/adapters/output/ui/components/ui/annotation_list_input.rb +98 -0
  113. data/lib/shoko/adapters/output/ui/components/ui/annotation_markup.rb +452 -0
  114. data/lib/shoko/adapters/output/ui/components/ui/box_drawer.rb +3 -2
  115. data/lib/shoko/adapters/output/ui/components/ui/cursor_blink.rb +46 -0
  116. data/lib/shoko/adapters/output/ui/components/ui/overlay_layout.rb +10 -1
  117. data/lib/shoko/adapters/output/ui/components/ui/text_utils.rb +1 -1
  118. data/lib/shoko/adapters/output/ui/constants/themes.rb +6 -6
  119. data/lib/shoko/adapters/output/ui/constants/ui_constants.rb +32 -13
  120. data/lib/shoko/adapters/output/ui/rendering/frame_coordinator.rb +2 -0
  121. data/lib/shoko/adapters/output/ui/rendering/reader_render_coordinator.rb +101 -15
  122. data/lib/shoko/adapters/output/ui/rendering/render_pipeline.rb +45 -13
  123. data/lib/shoko/adapters/output/ui/rendering_factory_adapter.rb +34 -0
  124. data/lib/shoko/adapters/storage/background_worker.rb +8 -5
  125. data/lib/shoko/adapters/storage/book_cache_pipeline.rb +26 -18
  126. data/lib/shoko/adapters/storage/cache/epub/memory_cache.rb +1 -1
  127. data/lib/shoko/adapters/storage/cache/epub/serializer/helpers.rb +4 -2
  128. data/lib/shoko/adapters/storage/cache_availability_adapter.rb +100 -0
  129. data/lib/shoko/adapters/storage/cache_manager_adapter.rb +26 -0
  130. data/lib/shoko/adapters/storage/cache_pointer_manager.rb +4 -4
  131. data/lib/shoko/adapters/storage/cache_pointer_resolver.rb +25 -0
  132. data/lib/shoko/adapters/storage/config_storage_adapter.rb +67 -0
  133. data/lib/shoko/adapters/storage/dictionary_availability_adapter.rb +33 -0
  134. data/lib/shoko/adapters/storage/dictionary_catalog_service.rb +140 -0
  135. data/lib/shoko/adapters/storage/epub_cache.rb +12 -10
  136. data/lib/shoko/adapters/storage/file_writer_service.rb +22 -26
  137. data/lib/shoko/adapters/storage/json_cache_store/layouts.rb +2 -1
  138. data/lib/shoko/adapters/storage/json_cache_store/manifest.rb +1 -1
  139. data/lib/shoko/adapters/storage/json_cache_store.rb +15 -12
  140. data/lib/shoko/adapters/storage/lazy_file_string.rb +1 -1
  141. data/lib/shoko/adapters/storage/pagination_cache.rb +1 -2
  142. data/lib/shoko/adapters/storage/recent_files.rb +5 -3
  143. data/lib/shoko/adapters/storage/recent_files_repository.rb +27 -0
  144. data/lib/shoko/adapters/storage/repositories/annotation_repository.rb +156 -155
  145. data/lib/shoko/adapters/storage/repositories/base_repository.rb +57 -64
  146. data/lib/shoko/adapters/storage/repositories/bookmark_repository.rb +107 -106
  147. data/lib/shoko/adapters/storage/repositories/cached_library_repository.rb +94 -93
  148. data/lib/shoko/adapters/storage/repositories/config_repository.rb +225 -221
  149. data/lib/shoko/adapters/storage/repositories/progress_repository.rb +137 -138
  150. data/lib/shoko/adapters/storage/repositories/storage/annotation_file_store.rb +5 -28
  151. data/lib/shoko/adapters/storage/repositories/storage/base_file_store.rb +34 -0
  152. data/lib/shoko/adapters/storage/repositories/storage/bookmark_file_store.rb +7 -30
  153. data/lib/shoko/adapters/storage/repositories/storage/progress_file_store.rb +4 -26
  154. data/lib/shoko/adapters/storage/sqlite_dictionary_adapter.rb +307 -0
  155. data/lib/shoko/application/adapters/command_port_adapter.rb +133 -0
  156. data/lib/shoko/application/adapters/config_reader_adapter.rb +80 -0
  157. data/lib/shoko/application/adapters/layout_metrics_adapter.rb +66 -0
  158. data/lib/shoko/application/adapters/menu_state_reader_adapter.rb +204 -0
  159. data/lib/shoko/application/adapters/menu_state_writer_adapter.rb +134 -0
  160. data/lib/shoko/application/adapters/notification_writer_adapter.rb +37 -0
  161. data/lib/shoko/application/adapters/progress_state_reader_adapter.rb +35 -0
  162. data/lib/shoko/application/adapters/reader_state_reader_adapter.rb +150 -0
  163. data/lib/shoko/application/adapters/render_state_writer_adapter.rb +51 -0
  164. data/lib/shoko/application/adapters/rendered_content_reader_adapter.rb +27 -0
  165. data/lib/shoko/application/adapters/sidebar_state_reader_adapter.rb +65 -0
  166. data/lib/shoko/application/adapters/state_writer_adapter.rb +98 -0
  167. data/lib/shoko/application/adapters/ui_state_reader_adapter.rb +39 -0
  168. data/lib/shoko/application/adapters/wrapped_lines_provider_adapter.rb +35 -0
  169. data/lib/shoko/application/annotation_editor_overlay_session.rb +28 -8
  170. data/lib/shoko/application/cli.rb +15 -15
  171. data/lib/shoko/application/cli_progress_renderer.rb +151 -0
  172. data/lib/shoko/application/controllers/annotation_overlay_controller.rb +259 -0
  173. data/lib/shoko/application/controllers/dictionary_controller.rb +437 -0
  174. data/lib/shoko/application/controllers/document_path_resolver.rb +69 -0
  175. data/lib/shoko/application/controllers/menu/input_controller.rb +59 -30
  176. data/lib/shoko/application/controllers/menu/state_controller.rb +220 -85
  177. data/lib/shoko/application/controllers/menu_controller.rb +260 -61
  178. data/lib/shoko/application/controllers/mouseable_reader.rb +174 -374
  179. data/lib/shoko/application/controllers/reader_controller.rb +521 -386
  180. data/lib/shoko/application/controllers/selection_mouse_handler.rb +172 -0
  181. data/lib/shoko/application/controllers/sidebar_controller.rb +491 -0
  182. data/lib/shoko/application/controllers/sidebar_mouse_handler.rb +184 -0
  183. data/lib/shoko/application/controllers/state_controller.rb +154 -120
  184. data/lib/shoko/application/controllers/ui_controller.rb +263 -602
  185. data/lib/shoko/application/dependency_container.rb +790 -148
  186. data/lib/shoko/application/infrastructure/event_bus.rb +4 -2
  187. data/lib/shoko/application/infrastructure/observer_state_store.rb +7 -4
  188. data/lib/shoko/application/infrastructure/state_store.rb +202 -31
  189. data/lib/shoko/application/main_menu/menu_progress_presenter.rb +56 -56
  190. data/lib/shoko/application/pending_jump_handler.rb +21 -19
  191. data/lib/shoko/application/reader_lifecycle.rb +17 -24
  192. data/lib/shoko/application/reader_startup_orchestrator.rb +18 -57
  193. data/lib/shoko/application/selectors/config_selectors.rb +16 -0
  194. data/lib/shoko/application/selectors/menu_selectors.rb +20 -0
  195. data/lib/shoko/application/selectors/reader_selectors.rb +48 -55
  196. data/lib/shoko/application/state/actions/toggle_view_mode_action.rb +1 -1
  197. data/lib/shoko/application/state/actions/update_config_action.rb +4 -0
  198. data/lib/shoko/application/state/actions/update_menu_action.rb +6 -10
  199. data/lib/shoko/application/state/actions/update_message_action.rb +1 -14
  200. data/lib/shoko/application/state/actions/update_page_action.rb +4 -11
  201. data/lib/shoko/application/state/actions/update_pagination_state_action.rb +4 -6
  202. data/lib/shoko/application/state/actions/update_reader_meta_action.rb +5 -7
  203. data/lib/shoko/application/state/actions/update_rendered_lines_action.rb +3 -13
  204. data/lib/shoko/application/state/actions/update_selection_action.rb +4 -7
  205. data/lib/shoko/application/state/actions/update_selections_action.rb +4 -11
  206. data/lib/shoko/application/state/actions/update_sidebar_action.rb +4 -0
  207. data/lib/shoko/application/state/actions/update_state_action.rb +72 -0
  208. data/lib/shoko/application/state/actions/update_ui_loading_action.rb +6 -10
  209. data/lib/shoko/application/ui/reader_view_model_builder.rb +46 -46
  210. data/lib/shoko/application/ui/view_models/reader_view_model.rb +2 -2
  211. data/lib/shoko/application/unified_application.rb +94 -18
  212. data/lib/shoko/application/use_cases/catalog_service.rb +81 -87
  213. data/lib/shoko/application/use_cases/commands/annotation_editor_commands.rb +104 -44
  214. data/lib/shoko/application/use_cases/commands/application_commands.rb +34 -61
  215. data/lib/shoko/application/use_cases/commands/base_command.rb +1 -6
  216. data/lib/shoko/application/use_cases/commands/bookmark_commands.rb +2 -4
  217. data/lib/shoko/application/use_cases/commands/conditional_navigation_commands.rb +14 -2
  218. data/lib/shoko/application/use_cases/commands/menu_commands.rb +215 -119
  219. data/lib/shoko/application/use_cases/commands/navigation_commands.rb +8 -21
  220. data/lib/shoko/application/use_cases/commands/reader_commands.rb +2 -9
  221. data/lib/shoko/application/use_cases/commands/sidebar_commands.rb +3 -2
  222. data/lib/shoko/application/use_cases/settings_service.rb +275 -93
  223. data/lib/shoko/core/events/base_domain_event.rb +9 -6
  224. data/lib/shoko/core/events/domain_event_bus.rb +6 -9
  225. data/lib/shoko/core/models/dictionary_entry.rb +163 -0
  226. data/lib/shoko/core/models/reader_settings.rb +8 -8
  227. data/lib/shoko/core/models/selection_anchor.rb +58 -53
  228. data/lib/shoko/core/ports/annotation_repository.rb +94 -0
  229. data/lib/shoko/core/ports/async_executor.rb +24 -0
  230. data/lib/shoko/core/ports/bookmark_repository.rb +77 -0
  231. data/lib/shoko/core/ports/cache_availability.rb +18 -0
  232. data/lib/shoko/core/ports/cache_manager.rb +26 -0
  233. data/lib/shoko/core/ports/cache_pointer_resolver.rb +27 -0
  234. data/lib/shoko/core/ports/command_port.rb +55 -0
  235. data/lib/shoko/core/ports/config_reader.rb +109 -0
  236. data/lib/shoko/core/ports/config_storage.rb +61 -0
  237. data/lib/shoko/core/ports/dictionary_availability.rb +41 -0
  238. data/lib/shoko/core/ports/dictionary_repository.rb +69 -0
  239. data/lib/shoko/core/ports/display_capabilities.rb +18 -0
  240. data/lib/shoko/core/ports/input_system_factory.rb +36 -0
  241. data/lib/shoko/core/ports/instrumentation.rb +31 -0
  242. data/lib/shoko/core/ports/key_classifier.rb +107 -0
  243. data/lib/shoko/core/ports/layout_metrics.rb +78 -0
  244. data/lib/shoko/core/ports/logging.rb +65 -0
  245. data/lib/shoko/core/ports/menu_state_reader.rb +291 -0
  246. data/lib/shoko/core/ports/menu_state_writer.rb +136 -0
  247. data/lib/shoko/core/ports/metadata_reader.rb +20 -0
  248. data/lib/shoko/core/ports/notification_writer.rb +40 -0
  249. data/lib/shoko/core/ports/progress_state_reader.rb +47 -0
  250. data/lib/shoko/core/ports/reader_state_reader.rb +207 -0
  251. data/lib/shoko/core/ports/recent_files_repository.rb +25 -0
  252. data/lib/shoko/core/ports/render_state_writer.rb +31 -0
  253. data/lib/shoko/core/ports/rendered_content_reader.rb +32 -0
  254. data/lib/shoko/core/ports/rendering_factory.rb +38 -0
  255. data/lib/shoko/core/ports/sidebar_state_reader.rb +88 -0
  256. data/lib/shoko/core/ports/state_writer.rb +152 -0
  257. data/lib/shoko/core/ports/terminal_capabilities.rb +33 -0
  258. data/lib/shoko/core/ports/text_metrics.rb +19 -0
  259. data/lib/shoko/core/ports/text_sanitizer.rb +22 -0
  260. data/lib/shoko/core/ports/ui_component_factory.rb +61 -0
  261. data/lib/shoko/core/ports/ui_state_reader.rb +53 -0
  262. data/lib/shoko/core/ports/wrapped_lines_provider.rb +23 -0
  263. data/lib/shoko/core/services/annotation_service.rb +14 -15
  264. data/lib/shoko/core/services/base_service.rb +7 -46
  265. data/lib/shoko/core/services/bookmark_service.rb +118 -93
  266. data/lib/shoko/core/services/config_bridge.rb +27 -0
  267. data/lib/shoko/core/services/coordinate_service.rb +8 -10
  268. data/lib/shoko/core/services/default_display_capabilities.rb +18 -0
  269. data/lib/shoko/core/services/default_layout_metrics.rb +65 -0
  270. data/lib/shoko/core/services/default_terminal_capabilities.rb +23 -0
  271. data/lib/shoko/core/services/default_text_metrics.rb +44 -0
  272. data/lib/shoko/core/services/dictionary_service.rb +220 -0
  273. data/lib/shoko/core/services/inline_executor.rb +24 -0
  274. data/lib/shoko/core/services/layout_service.rb +1 -8
  275. data/lib/shoko/core/services/navigation/absolute_layout.rb +39 -9
  276. data/lib/shoko/core/services/navigation/context_builder.rb +32 -7
  277. data/lib/shoko/core/services/navigation/context_helpers.rb +24 -9
  278. data/lib/shoko/core/services/navigation/dynamic_change_applier.rb +5 -8
  279. data/lib/shoko/core/services/navigation/image_offset_snapper.rb +35 -16
  280. data/lib/shoko/core/services/navigation/state_updater.rb +13 -10
  281. data/lib/shoko/core/services/navigation_service.rb +80 -69
  282. data/lib/shoko/core/services/null_instrumentation.rb +24 -0
  283. data/lib/shoko/core/services/null_logger.rb +39 -0
  284. data/lib/shoko/core/services/page_calculator_service.rb +95 -88
  285. data/lib/shoko/core/services/pagination/internal/absolute_page_map_builder.rb +27 -19
  286. data/lib/shoko/core/services/pagination/internal/chapter_cache.rb +56 -45
  287. data/lib/shoko/core/services/pagination/internal/dynamic_page_map_builder.rb +163 -110
  288. data/lib/shoko/core/services/pagination/internal/layout_metrics_calculator.rb +59 -57
  289. data/lib/shoko/core/services/pagination/internal/page_hydrator.rb +109 -120
  290. data/lib/shoko/core/services/pagination/internal/pagination_workflow.rb +125 -128
  291. data/lib/shoko/core/services/pagination/page_info_calculator.rb +238 -240
  292. data/lib/shoko/core/services/pagination/pagination_cache_preloader.rb +197 -166
  293. data/lib/shoko/core/services/pagination/pagination_coordinator.rb +188 -159
  294. data/lib/shoko/core/services/pagination/pagination_orchestrator.rb +280 -239
  295. data/lib/shoko/core/services/progress_helper.rb +10 -10
  296. data/lib/shoko/core/services/selection_service.rb +70 -48
  297. data/lib/shoko/shared/errors.rb +53 -0
  298. data/lib/shoko/shared/optional_dependency.rb +50 -0
  299. data/lib/shoko/shared/unicode_display_width/display_width.marshal.gz +0 -0
  300. data/lib/shoko/shared/unicode_display_width.rb +147 -0
  301. data/lib/shoko/shared/version.rb +1 -1
  302. data/lib/shoko/test_support/test_mode.rb +1 -1
  303. data/lib/shoko.rb +142 -147
  304. metadata +102 -66
  305. data/lib/shoko/adapters/output/ui/components/component_interface.rb +0 -80
  306. data/lib/shoko/application/state/actions/update_annotation_editor_overlay_action.rb +0 -27
  307. data/lib/shoko/application/state/actions/update_annotations_action.rb +0 -20
  308. data/lib/shoko/application/state/actions/update_annotations_overlay_action.rb +0 -27
  309. data/lib/shoko/application/state/actions/update_bookmarks_action.rb +0 -20
  310. data/lib/shoko/application/state/actions/update_chapter_action.rb +0 -24
  311. data/lib/shoko/application/state/actions/update_popup_menu_action.rb +0 -27
  312. data/lib/shoko/application/state/actions/update_reader_mode_action.rb +0 -20
  313. data/lib/shoko/core/ports/book_repository.rb +0 -0
  314. data/lib/shoko/core/ports/book_source.rb +0 -0
  315. data/lib/shoko/core/ports/cache.rb +0 -0
  316. data/lib/shoko/core/ports/input_handler.rb +0 -0
  317. data/lib/shoko/core/ports/renderer.rb +0 -0
  318. data/lib/shoko/core/ports/storage.rb +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5060375164152dd31808e0548f1a525246af0d4708e037eb3590423730d4d6c6
4
- data.tar.gz: a71558d1f38594e40ffe44d80f4d05dcdd1d1c43a23bb003ab42f74c9ffffeec
3
+ metadata.gz: a7a36fd324fdba1eed316344cfab543f5fef1bfa863c1b25fc3e644502978054
4
+ data.tar.gz: 5fc5820176929bd037c545901cdc0c611e98b593a90fc8da7a54d4a01b0cc359
5
5
  SHA512:
6
- metadata.gz: 89e5372ed7369bb5631b4c190311303bbe8285602d5d7520060dc91f3c176c903f269a491d8b436f2c1d280717ddffd8b3498560aab076d7f7b7598ff76be0e8
7
- data.tar.gz: b6ca5f48134e099a7b112c517bfa7efeb683606e3d25e66cdc27c8e6c6711615f49a7d2d7c4c75b7ef801e2d4286b3be8f3911dd00d726598093ee583c562198
6
+ metadata.gz: e29b072b4f0dc64eb6a18f36d3fcc1bf37fa9efa904b1827cccbe85c4c37860073e04c3eddc87fdab827b9d02e03958f54bd1714d58644cdff026caaacf93bfe
7
+ data.tar.gz: 0ffb950539fe1440f26149b1d75755811f2aaf541f900f6ab89051dc9e4e2550c1a07a9de32d83a3d6bac5a5f25a1184f7830cf3b39866eed458b71081fe9248
data/.bundle/config CHANGED
@@ -1,3 +1,2 @@
1
1
  ---
2
2
  BUNDLE_PATH: "vendor/bundle"
3
- BUNDLE_WITHOUT: "development"
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
@@ -6,6 +6,7 @@ gemspec
6
6
 
7
7
  group :development do
8
8
  gem 'reek'
9
+ gem 'rake'
9
10
  gem 'rubocop', require: false
10
11
  gem 'rubocop-performance', require: false
11
12
  gem 'rubocop-rails', require: false
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
- gemspec = Dir[File.join(app_root, '*.gemspec')].first
10
- if gemspec && File.exist?(File.join(app_root, 'Gemfile'))
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
- gemspec = Dir[File.join(app_root, '*.gemspec')].first
10
- if gemspec && File.exist?(File.join(app_root, 'Gemfile'))
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
- def initialize(epub_path, wrapping_service = nil, formatting_service: nil, background_worker: nil,
12
- progress_reporter: nil)
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 ||= Adapters::Monitoring::PerformanceMonitor.time('import.document.load') do
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
- Adapters::Monitoring::Logger.error('Failed to load document', path: @epub_path, error: e.message)
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 '../../core/services/base_service.rb'
4
+ require_relative '../base_adapter'
5
5
  require_relative '../storage/config_paths'
6
6
 
7
7
  module Shoko
8
8
  module Adapters::BookSources
9
- # Coordinates Gutendex search + download to the local library.
10
- class DownloadService < BaseService
11
- class DownloadError < StandardError; end
12
-
13
- def search(query:, page_url: nil)
14
- payload = client.search(query: query, page_url: page_url)
15
- {
16
- count: payload['count'].to_i,
17
- next: payload['next'],
18
- previous: payload['previous'],
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
- client.download(url, dest_path) { |done, total| yield(done, total) if block_given? }
33
- { path: dest_path, existing: false }
34
- end
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
- protected
30
+ def download(book)
31
+ url = pick_download_url(book)
32
+ raise DownloadError, 'No EPUB format available' unless url
37
33
 
38
- def required_dependencies
39
- [:gutendex_client]
40
- end
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
- private
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
- def client
45
- @client ||= resolve(:gutendex_client)
46
- end
43
+ private
47
44
 
48
- def downloads_root
49
- Adapters::Storage::ConfigPaths.downloads_root
50
- end
45
+ def downloads_root
46
+ Adapters::Storage::ConfigPaths.downloads_root
47
+ end
51
48
 
52
- def normalize_books(items)
53
- Array(items).map do |raw|
54
- {
55
- id: raw['id'],
56
- title: raw['title'],
57
- authors: Array(raw['authors']).filter_map { |a| a['name'] },
58
- languages: Array(raw['languages']).map(&:to_s),
59
- download_count: raw['download_count'],
60
- formats: raw['formats'] || {},
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
- def pick_download_url(book)
66
- formats = value_for(book, :formats, 'formats', {})
67
- return nil unless formats.respond_to?(:each)
62
+ def pick_download_url(book)
63
+ formats = value_for(book, :formats, 'formats', {})
64
+ return nil unless formats.respond_to?(:each)
68
65
 
69
- keys = formats.keys.map(&:to_s)
70
- epub_key = keys.find { |k| k.start_with?('application/epub+zip') } ||
71
- keys.find { |k| k.include?('application/epub') } ||
72
- keys.find { |k| k.include?('epub') }
73
- return nil unless epub_key
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
- formats[epub_key] || formats[epub_key.to_sym]
76
- end
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
- def value_for(book, key_sym, key_str, default)
87
- return book[key_sym] if book.respond_to?(:key?) && book.key?(key_sym)
88
- return book[key_str] if book.respond_to?(:key?) && book.key?(key_str)
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
- default
91
- end
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.rb'
7
- require_relative '../../storage/cache_paths.rb'
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
- Shoko::Adapters::Monitoring::Logger.debug('EpubResourceLoader: zip read failed', path: epub_path.to_s, entry: entry_path.to_s,
105
- error: e.message)
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
- Shoko::Adapters::Monitoring::Logger.debug('EpubResourceLoader: read failed', path: epub_path.to_s, entry: entry_path.to_s, error: e.message)
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 '../../../monitoring/perf_tracer.rb'
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
- if Shoko::Adapters::Monitoring::PerfTracer.enabled?
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 = REXML::Document.new(container_xml)
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 = REXML::Document.new(content)
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 = REXML::Document.new(ncx_content)
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 '../../../monitoring/perf_tracer.rb'
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 = Shoko::Adapters::Monitoring::PerfTracer.measure('opf.parse') do
26
- REXML::Document.new(content)
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
- Shoko::Adapters::Monitoring::PerfTracer.measure('zip.read') do
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