epuber 0.8.0 → 0.9.1

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +9 -0
  3. data/LICENSE.txt +1 -1
  4. data/README.md +2 -2
  5. data/epuber.gemspec +3 -6
  6. data/lib/epuber/book/contributor.rb +10 -6
  7. data/lib/epuber/book/file_request.rb +2 -2
  8. data/lib/epuber/book/target.rb +10 -10
  9. data/lib/epuber/book.rb +2 -2
  10. data/lib/epuber/checker/text_checker.rb +14 -6
  11. data/lib/epuber/checker_transformer_base.rb +1 -1
  12. data/lib/epuber/command/build.rb +6 -1
  13. data/lib/epuber/command/from_file.rb +39 -0
  14. data/lib/epuber/command/init.rb +11 -9
  15. data/lib/epuber/command/server.rb +1 -1
  16. data/lib/epuber/command.rb +1 -0
  17. data/lib/epuber/compiler/file_database.rb +2 -2
  18. data/lib/epuber/compiler/file_finders/abstract.rb +3 -3
  19. data/lib/epuber/compiler/file_resolver.rb +3 -2
  20. data/lib/epuber/compiler/file_types/abstract_file.rb +1 -3
  21. data/lib/epuber/compiler/file_types/bade_file.rb +9 -9
  22. data/lib/epuber/compiler/file_types/css_file.rb +84 -0
  23. data/lib/epuber/compiler/file_types/source_file.rb +31 -0
  24. data/lib/epuber/compiler/file_types/stylus_file.rb +4 -3
  25. data/lib/epuber/compiler/nav_generator.rb +5 -5
  26. data/lib/epuber/compiler/opf_generator.rb +4 -4
  27. data/lib/epuber/compiler/xhtml_processor.rb +7 -25
  28. data/lib/epuber/compiler.rb +12 -7
  29. data/lib/epuber/config.rb +3 -3
  30. data/lib/epuber/dsl/attribute.rb +1 -1
  31. data/lib/epuber/dsl/attribute_support.rb +4 -4
  32. data/lib/epuber/dsl/object.rb +2 -2
  33. data/lib/epuber/from_file/bookspec_generator.rb +371 -0
  34. data/lib/epuber/from_file/encryption_handler.rb +146 -0
  35. data/lib/epuber/from_file/from_file_executor.rb +140 -0
  36. data/lib/epuber/from_file/nav_file.rb +163 -0
  37. data/lib/epuber/from_file/opf_file.rb +219 -0
  38. data/lib/epuber/plugin.rb +1 -1
  39. data/lib/epuber/server.rb +17 -17
  40. data/lib/epuber/transformer/book_transformer.rb +108 -0
  41. data/lib/epuber/transformer.rb +2 -0
  42. data/lib/epuber/user_interface.rb +2 -2
  43. data/lib/epuber/vendor/ruby_templater.rb +3 -3
  44. data/lib/epuber/vendor/version.rb +3 -3
  45. data/lib/epuber/version.rb +1 -1
  46. metadata +40 -59
@@ -5,9 +5,9 @@ require 'epuber-stylus'
5
5
  module Epuber
6
6
  class Compiler
7
7
  module FileTypes
8
- require_relative 'source_file'
8
+ require_relative 'css_file'
9
9
 
10
- class StylusFile < SourceFile
10
+ class StylusFile < CSSFile
11
11
  # @param [Compiler::CompilationContext] compilation_context
12
12
  #
13
13
  def process(compilation_context)
@@ -19,7 +19,8 @@ module Epuber
19
19
  Stylus.define('__book_title', compilation_context.book.title)
20
20
  Stylus.define('__const', compilation_context.target.constants)
21
21
 
22
- write_compiled(Stylus.compile(File.new(abs_source_path)))
22
+ css = Stylus.compile(File.new(abs_source_path))
23
+ write_compiled(process_css(css, compilation_context))
23
24
 
24
25
  update_metadata!
25
26
  end
@@ -94,7 +94,7 @@ module Epuber
94
94
  @xml.ncx(nav_namespaces, version: '2005-1') do
95
95
  # head
96
96
  @xml.head do
97
- @xml.meta(name: 'dtb:uid', content: (@target.identifier || "urn:isbn:#{@target.isbn}"))
97
+ @xml.meta(name: 'dtb:uid', content: @target.identifier || "urn:isbn:#{@target.isbn}")
98
98
  end
99
99
 
100
100
  # title
@@ -111,7 +111,7 @@ module Epuber
111
111
  end
112
112
  end
113
113
 
114
- # @param toc_items [Array<Epuber::Book::TocItem>]
114
+ # @param [Array<Epuber::Book::TocItem>] toc_items
115
115
  #
116
116
  def visit_toc_items(toc_items)
117
117
  iterate_lambda = lambda do
@@ -133,7 +133,7 @@ module Epuber
133
133
  toc_items.any? { |a| a.title || contains_item_with_title(a.sub_items) }
134
134
  end
135
135
 
136
- # @param toc_item [Epuber::Book::TocItem]
136
+ # @param [Epuber::Book::TocItem] toc_item
137
137
  #
138
138
  def visit_toc_item(toc_item)
139
139
  result_file_path = pretty_path_for_toc_item(toc_item)
@@ -165,7 +165,7 @@ module Epuber
165
165
 
166
166
  # --------------- landmarks -----------------------------
167
167
 
168
- # @param toc_items [Array<Epuber::Book::TocItem>]
168
+ # @param [Array<Epuber::Book::TocItem>] toc_items
169
169
  #
170
170
  def landmarks_visit_toc_items(toc_items)
171
171
  toc_items.each do |child_item|
@@ -173,7 +173,7 @@ module Epuber
173
173
  end
174
174
  end
175
175
 
176
- # @param toc_item [Epuber::Book::TocItem]
176
+ # @param [Epuber::Book::TocItem] toc_item
177
177
  #
178
178
  def landmarks_visit_toc_item(toc_item)
179
179
  landmarks = toc_item.landmarks
@@ -217,7 +217,7 @@ module Epuber
217
217
  end
218
218
  end
219
219
 
220
- # @param toc_items [Array<Epuber::Book::TocItem>]
220
+ # @param [Array<Epuber::Book::TocItem>] toc_items
221
221
  #
222
222
  # @return nil
223
223
  #
@@ -227,7 +227,7 @@ module Epuber
227
227
  end
228
228
  end
229
229
 
230
- # @param toc_item [Epuber::Book::TocItem]
230
+ # @param [Epuber::Book::TocItem] toc_item
231
231
  #
232
232
  # @return nil
233
233
  #
@@ -251,7 +251,7 @@ module Epuber
251
251
 
252
252
  # Creates id from file path
253
253
  #
254
- # @param path [String]
254
+ # @param [String] path
255
255
  #
256
256
  # @return [String]
257
257
  #
@@ -263,7 +263,7 @@ module Epuber
263
263
 
264
264
  # Creates proper mime-type for file
265
265
  #
266
- # @param file [Epuber::Compiler::FileTypes::AbstractFile | String]
266
+ # @param [Epuber::Compiler::FileTypes::AbstractFile | String] file
267
267
  #
268
268
  # @return [String]
269
269
  #
@@ -50,7 +50,8 @@ module Epuber
50
50
 
51
51
  parse_options = Nokogiri::XML::ParseOptions::DEFAULT_XML |
52
52
  Nokogiri::XML::ParseOptions::NOERROR | # to silence any errors or warnings printing into console
53
- Nokogiri::XML::ParseOptions::NOWARNING
53
+ Nokogiri::XML::ParseOptions::NOWARNING |
54
+ Nokogiri::XML::ParseOptions::NOENT
54
55
 
55
56
  doc = Nokogiri::XML("#{before}<root>#{text}</root>", file_path, nil, parse_options)
56
57
  text_for_errors = before + text
@@ -341,34 +342,15 @@ module Epuber
341
342
  end
342
343
 
343
344
  def self.resolve_resources_in(node_css_query, attribute_name, resource_group, xhtml_doc, file_path, file_resolver)
344
- dirname = File.dirname(file_path)
345
-
346
345
  xhtml_doc.css(node_css_query).each do |img|
347
346
  path = img[attribute_name]
348
347
  next if path.nil?
349
348
 
350
- begin
351
- new_path = file_resolver.dest_finder.find_file(path, groups: resource_group, context_path: dirname)
352
- rescue UnparseableLinkError, FileFinders::FileNotFoundError, FileFinders::MultipleFilesFoundError
353
- begin
354
- new_path = resolved_link_to_file(path, resource_group, dirname, file_resolver.source_finder).to_s
355
- pkg_abs_path = File.expand_path(new_path, dirname).unicode_normalize
356
- pkg_new_path = Pathname.new(pkg_abs_path).relative_path_from(Pathname.new(file_resolver.source_path)).to_s
357
-
358
- file_class = FileResolver.file_class_for(File.extname(new_path))
359
- file = file_class.new(pkg_new_path)
360
- file.path_type = :manifest
361
- file_resolver.add_file(file)
362
-
363
- new_path = FileResolver.renamed_file_with_path(new_path)
364
- rescue UnparseableLinkError, FileFinders::FileNotFoundError, FileFinders::MultipleFilesFoundError => e
365
- UI.warning(e.to_s, location: img)
366
-
367
- next
368
- end
369
- end
370
-
371
- img[attribute_name] = new_path
349
+ new_path = Compiler::FileTypes::SourceFile.resolve_relative_file(file_path,
350
+ path,
351
+ file_resolver,
352
+ group: resource_group)
353
+ img[attribute_name] = new_path if new_path
372
354
  end
373
355
  end
374
356
 
@@ -39,8 +39,8 @@ module Epuber
39
39
  #
40
40
  attr_reader :compilation_context
41
41
 
42
- # @param book [Epuber::Book::Book]
43
- # @param target [Epuber::Book::Target]
42
+ # @param [Epuber::Book::Book] book
43
+ # @param [Epuber::Book::Target] target
44
44
  #
45
45
  def initialize(book, target)
46
46
  @book = book
@@ -50,7 +50,7 @@ module Epuber
50
50
 
51
51
  # Compile target to build folder
52
52
  #
53
- # @param build_folder [String] path to folder, where will be stored all compiled files
53
+ # @param [String] build_folder path to folder, where will be stored all compiled files
54
54
  # @param [Bool] check should run non-release checkers
55
55
  # @param [Bool] write should perform transformations of source files and write them back
56
56
  # @param [Bool] release this is release build
@@ -88,6 +88,11 @@ module Epuber
88
88
 
89
89
  process_all_target_files
90
90
  generate_other_files
91
+
92
+ compilation_context.perform_plugin_things(Transformer, :after_all_text_files) do |transformer|
93
+ transformer.call(@book, compilation_context)
94
+ end
95
+
91
96
  process_global_ids
92
97
 
93
98
  # build folder cleanup
@@ -109,7 +114,7 @@ module Epuber
109
114
 
110
115
  # Archives current target files to epub
111
116
  #
112
- # @param path [String] path to created archive
117
+ # @param [String] path path to created archive
113
118
  #
114
119
  # @return [String] path
115
120
  #
@@ -285,7 +290,7 @@ module Epuber
285
290
  UI.processing_files_done
286
291
  end
287
292
 
288
- # @param toc_item [Epuber::Book::TocItem]
293
+ # @param [Epuber::Book::TocItem] toc_item
289
294
  #
290
295
  def parse_toc_item(toc_item)
291
296
  unless toc_item.file_request.nil?
@@ -310,7 +315,7 @@ module Epuber
310
315
 
311
316
  # Validates duplicity of global ids in all files + returns map of global ids to files
312
317
  #
313
- # @param xhtml_files [Array<FileTypes::XHTMLFile>]
318
+ # @param [Array<FileTypes::XHTMLFile>] xhtml_files
314
319
  # @return [Hash<String, FileTypes::XHTMLFile>]
315
320
  #
316
321
  def validate_global_ids(xhtml_files)
@@ -333,7 +338,7 @@ module Epuber
333
338
  map
334
339
  end
335
340
 
336
- # @param cmd [String]
341
+ # @param [String] cmd
337
342
  #
338
343
  # @return [void]
339
344
  #
data/lib/epuber/config.rb CHANGED
@@ -14,7 +14,7 @@ module Epuber
14
14
  @project_path ||= Dir.pwd.unicode_normalize
15
15
  end
16
16
 
17
- # @param of_file [String] absolute path to file
17
+ # @param [String] of_file absolute path to file
18
18
  #
19
19
  # @return [String] relative path to file from root of project
20
20
  #
@@ -84,7 +84,7 @@ module Epuber
84
84
  bookspec_lockfile.write_to_file
85
85
  end
86
86
 
87
- # @param target [Epuber::Book::Target]
87
+ # @param [Epuber::Book::Target] target
88
88
  #
89
89
  # @return [String]
90
90
  #
@@ -92,7 +92,7 @@ module Epuber
92
92
  File.join(working_path, 'build', target.name.to_s)
93
93
  end
94
94
 
95
- # @param target [Epuber::Book::Target]
95
+ # @param [Epuber::Book::Target] target
96
96
  #
97
97
  # @return [String]
98
98
  #
@@ -17,7 +17,7 @@ module Epuber
17
17
 
18
18
  # Returns a new attribute initialized with the given options.
19
19
  #
20
- # @param name [Symbol]
20
+ # @param [Symbol] name
21
21
  #
22
22
  # @see #name
23
23
  #
@@ -9,8 +9,8 @@ module Epuber
9
9
  # attribute :name
10
10
  # attribute :title, required: true, inherited: true
11
11
  #
12
- # @param name [Symbol] attribute name
13
- # @param options [Hash]
12
+ # @param [Symbol] name attribute name
13
+ # @param [Hash] options
14
14
  #
15
15
  # @see Attribute
16
16
  #
@@ -44,8 +44,8 @@ module Epuber
44
44
  end
45
45
  end
46
46
 
47
- # @param name [Symbol]
48
- # @param attr [Epuber::DSL::Attribute]
47
+ # @param [Symbol] name
48
+ # @param [Epuber::DSL::Attribute] attr
49
49
  #
50
50
  # @return nil
51
51
  #
@@ -58,7 +58,7 @@ module Epuber
58
58
 
59
59
  # Creates new instance by parsing ruby code from file
60
60
  #
61
- # @param file_path [String]
61
+ # @param [String] file_path
62
62
  #
63
63
  # @return [Self]
64
64
  #
@@ -68,7 +68,7 @@ module Epuber
68
68
 
69
69
  # Creates new instance by parsing ruby code from string
70
70
  #
71
- # @param string [String]
71
+ # @param [String] string
72
72
  #
73
73
  # @return [Self]
74
74
  #
@@ -0,0 +1,371 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module Epuber
6
+ class BookspecGenerator
7
+ class TocItem
8
+ # @return [String]
9
+ #
10
+ attr_accessor :href
11
+
12
+ # @return [String, nil]
13
+ #
14
+ attr_accessor :title
15
+
16
+ # @return [Array<TocItem>]
17
+ #
18
+ attr_accessor :children
19
+
20
+ # @return [Array<Symbol>]
21
+ #
22
+ attr_accessor :landmarks
23
+
24
+ def initialize(href, title = nil, landmarks = [], children = [])
25
+ @href = href
26
+ @title = title
27
+ @landmarks = landmarks
28
+ @children = children
29
+ end
30
+
31
+ def attribs
32
+ [href.inspect, title&.inspect, *landmarks.map(&:inspect)].compact.join(', ')
33
+ end
34
+
35
+ def ==(other)
36
+ other.is_a?(TocItem) &&
37
+ href == other.href &&
38
+ title == other.title &&
39
+ landmarks == other.landmarks &&
40
+ children == other.children
41
+ end
42
+
43
+ def to_s(level = 0)
44
+ indent = ' ' * level
45
+ children_str = children.map { |c| c.to_s(level + 2) }.join("\n")
46
+ %(#{indent}#{href.inspect} #{title.inspect} #{landmarks.inspect}\n#{children_str})
47
+ end
48
+
49
+ def inspect
50
+ %(#<#{self.class.name} #{self}>)
51
+ end
52
+ end
53
+
54
+ # @param [Epuber::OpfFile] opf
55
+ # @param [Epuber::NavFile, nil] nav
56
+ #
57
+ def initialize(opf, nav)
58
+ @opf = opf
59
+ @nav = nav
60
+ end
61
+
62
+ # @return [String]
63
+ #
64
+ def generate_bookspec
65
+ @indent = 0
66
+ @bookspec = []
67
+
68
+ add_code('Epuber::Book.new do |book|', after: 'end') do
69
+ generate_titles
70
+ generate_authors
71
+ add_empty_line
72
+ generate_id
73
+ generate_language
74
+ generate_published
75
+ generate_publisher
76
+ add_empty_line
77
+ generate_cover
78
+ add_empty_line
79
+ generate_toc
80
+ end
81
+ add_empty_line
82
+
83
+ @bookspec.join("\n")
84
+ end
85
+
86
+ # @return [void]
87
+ #
88
+ def generate_titles
89
+ titles = @opf.metadata.css('title')
90
+ titles.each do |title|
91
+ is_main = titles.count == 1
92
+
93
+ id = title['id']
94
+ is_main = @opf.find_refines(id, 'title-type') == 'main' if id && !is_main
95
+
96
+ add_comment('alternate title found from original EPUB file (Epuber supports only one title)') unless is_main
97
+ add_setting_property(:title, title.text.inspect, commented: !is_main)
98
+ end
99
+ end
100
+
101
+ # @return [void]
102
+ #
103
+ def generate_authors
104
+ authors = @opf.metadata.css('creator')
105
+ if authors.empty?
106
+ add_setting_property(:author, nil)
107
+ elsif authors.count == 1
108
+ add_setting_property(:author, format_author(authors.first))
109
+ else
110
+ add_setting_property(:authors, authors.map { |auth| format_author(auth) })
111
+ end
112
+ end
113
+
114
+ # @return [String]
115
+ #
116
+ def format_author(author_node)
117
+ name = author_node.text.strip
118
+ id = author_node['id']
119
+
120
+ if id
121
+ role = @opf.find_refines(id, 'role')
122
+ file_as = @opf.find_refines(id, 'file-as')
123
+ end
124
+
125
+ role ||= author_node['opf:role']
126
+ file_as ||= author_node['opf:file-as']
127
+
128
+ role_is_default = role.nil? || role == 'aut'
129
+ file_as_is_default = file_as.nil? || contributor_file_as_eq?(Book::Contributor.from_obj(name).file_as, file_as)
130
+
131
+ if role_is_default && file_as_is_default
132
+ name.inspect
133
+ elsif role_is_default && !file_as_is_default
134
+ %({ pretty_name: #{name.inspect}, file_as: #{file_as.inspect} })
135
+ elsif !role_is_default && file_as_is_default
136
+ %({ name: #{name.inspect}, role: #{role.inspect} })
137
+ else
138
+ %({ pretty_name: #{name.inspect}, file_as: #{file_as.inspect}, role: #{role.inspect} })
139
+ end
140
+ end
141
+
142
+ # @return [void]
143
+ #
144
+ def generate_id
145
+ nodes = @opf.metadata.css('identifier')
146
+
147
+ nodes.each do |id_node|
148
+ value = id_node.text.strip
149
+
150
+ is_main = @opf.package['unique-identifier'] == id_node['id']
151
+ is_isbn = value.start_with?('urn:isbn:')
152
+ value = value.sub(/^urn:isbn:/, '').strip if is_isbn
153
+ key = is_isbn ? :isbn : :identifier
154
+
155
+ if is_main
156
+ add_setting_property(key, value.inspect)
157
+ else
158
+ add_comment('alternate identifier found from original EPUB file (Epuber supports only one identifier)')
159
+ add_setting_property(key, value.inspect, commented: true)
160
+ end
161
+ end
162
+ end
163
+
164
+ # @return [void]
165
+ #
166
+ def generate_language
167
+ language = @opf.metadata.at_css('language')
168
+ add_setting_property(:language, language.text.strip.inspect) if language
169
+ end
170
+
171
+ # @return [void]
172
+ #
173
+ def generate_published
174
+ published = @opf.metadata.at_css('date')
175
+ add_setting_property(:published, published.text.strip.inspect) if published
176
+ end
177
+
178
+ # @return [void]
179
+ #
180
+ def generate_publisher
181
+ publisher = @opf.metadata.at_css('publisher')
182
+ add_setting_property(:publisher, publisher.text.strip.inspect) if publisher
183
+ end
184
+
185
+ # @return [void]
186
+ #
187
+ def generate_cover
188
+ cover_property = Compiler::OPFGenerator::PROPERTIES_MAP[:cover_image]
189
+
190
+ cover_id = @opf.manifest_items.find { |_, item| item.properties&.include?(cover_property) }&.last&.id
191
+ cover_id ||= @opf.metadata.at_css('meta[name="cover"]')&.[]('content')
192
+ cover = @opf.manifest_file_by_id(cover_id) if cover_id
193
+ return unless cover
194
+
195
+ href = cover.href.sub(/#{Regexp.escape(File.extname(cover.href))}$/, '')
196
+ add_setting_property(:cover_image, href.inspect)
197
+ end
198
+
199
+ # @return [void]
200
+ #
201
+ def generate_toc
202
+ items = calculate_toc_items
203
+
204
+ render_toc_item = lambda do |item|
205
+ if item.children.empty?
206
+ add_code(%(toc.file #{item.attribs}))
207
+ else
208
+ add_code(%(toc.file #{item.attribs} do), after: 'end') do
209
+ item.children.each do |child|
210
+ render_toc_item.call(child)
211
+ end
212
+ end
213
+ end
214
+ end
215
+
216
+ add_code('book.toc do |toc, target|', after: 'end') do
217
+ items.each do |toc_item|
218
+ render_toc_item.call(toc_item)
219
+ end
220
+ end
221
+ end
222
+
223
+ private
224
+
225
+ # Add code to final file. When block is given, it will be indented.
226
+ #
227
+ # @param [String] code code to print into final file
228
+ # @param [Boolean] commented should be code commented?
229
+ # @param [String, nil] after code to print after this code
230
+ #
231
+ # @return [void]
232
+ #
233
+ def add_code(code, commented: false, after: nil)
234
+ indent_str = ' ' * @indent
235
+ comment_prefix = commented ? '# ' : ''
236
+
237
+ @bookspec << %(#{indent_str}#{comment_prefix}#{code})
238
+
239
+ if block_given?
240
+ increase_indent
241
+ yield
242
+ decrease_indent
243
+ end
244
+
245
+ add_code(after, commented: commented) if after
246
+ end
247
+
248
+ def add_setting_property(name, value, commented: false)
249
+ if value.is_a?(Array)
250
+ add_code(%(book.#{name} = [), commented: commented, after: ']') do
251
+ value.each do |v|
252
+ add_code(%(#{v},), commented: commented)
253
+ end
254
+ end
255
+ else
256
+ add_code(%(book.#{name} = #{value}), commented: commented)
257
+ end
258
+ end
259
+
260
+ def add_comment(text)
261
+ add_code(text, commented: true)
262
+ end
263
+
264
+ def add_empty_line
265
+ return if @bookspec.last == ''
266
+
267
+ @bookspec << ''
268
+ end
269
+
270
+ def increase_indent
271
+ @indent += 2
272
+ end
273
+
274
+ def decrease_indent
275
+ @indent -= 2
276
+ @indent = 0 if @indent.negative?
277
+ end
278
+
279
+ # Compares two contributors by file_as, it will return true if file_as is same even when case is different
280
+ #
281
+ # @param [String] file_as_a
282
+ # @param [String] file_as_b
283
+ #
284
+ # @return [Boolean]
285
+ #
286
+ def contributor_file_as_eq?(file_as_a, file_as_b)
287
+ file_as_a == file_as_b || file_as_a.mb_chars.downcase == file_as_b.mb_chars.downcase
288
+ end
289
+
290
+ # @param [String] href
291
+ #
292
+ # @return [Array<Symbol>]
293
+ #
294
+ def landmark_for_href(href)
295
+ landmarks = @nav&.landmarks
296
+ landmark_map = NavFile::LANDMARKS_MAP unless landmarks.nil?
297
+
298
+ if landmarks.nil?
299
+ landmarks = @opf.guide_items
300
+ landmark_map = OpfFile::LANDMARKS_MAP
301
+ end
302
+
303
+ found = landmarks.select { |l| l.href == href }
304
+ .map(&:type)
305
+ .compact
306
+ .map { |t| landmark_map[t] }
307
+ .compact
308
+
309
+ return found unless found.empty?
310
+
311
+ # try to find by href without fragment
312
+ if href.include?('#')
313
+ href = href.split('#').first
314
+ return landmark_for_href(href)
315
+ end
316
+
317
+ []
318
+ end
319
+
320
+ # @return [Array<Epuber::BookspecGenerator::TocItem>]
321
+ #
322
+ def calculate_toc_items
323
+ spine_items = @opf.spine_items
324
+ idrefs = spine_items.map(&:idref)
325
+ landmarks = Set.new
326
+
327
+ # @param [ManifestItem, nil] manifest_item
328
+ # @param [NavItem, nil] toc_item
329
+ #
330
+ # @return [Array<Epuber::BookspecGenerator::TocItem>]
331
+ #
332
+ render_toc_item = lambda do |manifest_item, toc_item|
333
+ # ignore this item when it was already rendered
334
+ next nil if manifest_item && !idrefs.include?(manifest_item.id)
335
+
336
+ manifest_item ||= @opf.manifest_file_by_href(toc_item.href)
337
+
338
+ href = toc_item&.href || manifest_item.href
339
+ toc_item ||= @nav&.find_by_href(href) || @nav&.find_by_href(href, ignore_fragment: true)
340
+ href = toc_item&.href || manifest_item.href
341
+
342
+ href_output = href.sub(/\.x?html$/, '')
343
+ .sub(/\.x?html#/, '#')
344
+
345
+ landmarks_for_this = landmark_for_href(href)
346
+ unless landmarks_for_this.empty?
347
+ diff = Set.new(landmarks_for_this) - landmarks
348
+ landmarks += diff
349
+
350
+ landmarks_for_this = diff.to_a
351
+ end
352
+
353
+ item = TocItem.new(href_output, toc_item&.title, landmarks_for_this)
354
+ item.children = toc_item&.children&.map do |child|
355
+ render_toc_item.call(nil, child)
356
+ end || []
357
+
358
+ idrefs.delete(manifest_item.id) if manifest_item
359
+
360
+ item
361
+ end
362
+
363
+ spine_items.map do |itemref|
364
+ idref = itemref.idref
365
+ next unless idref
366
+
367
+ render_toc_item.call(@opf.manifest_file_by_id(idref), nil)
368
+ end.compact
369
+ end
370
+ end
371
+ end