epuber 0.8.0 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
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