fb2rb 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 34995961f6a6a691b554528f38d91db3dbe97a784111003430adf76d9a1b8843
4
+ data.tar.gz: 187174bf5bb0b8fc77dc93bec864adadc58e534ba2328ec63cef31cce7ab1ea5
5
+ SHA512:
6
+ metadata.gz: 1ea7ad32895bf83fbef11bde5d716c3b115eeeedfc951e0f168a37bf14011fc33cf9a017d5f82d998619af037ae80d2cf052248b0cffe991c2cc5836aaaf2dbb
7
+ data.tar.gz: db6c27a94eaa9152286baa144ab834c7abd207c7df50ac37a5b60dd89ea46bedf8828fe30f6c2d442bcc899ebec8c78a6bc1bfb4b0363d65574bb162374d673e
@@ -0,0 +1,9 @@
1
+ root = true
2
+
3
+ [*]
4
+ indent_style = space
5
+ indent_size = 2
6
+ end_of_line = lf
7
+ charset = utf-8
8
+ trim_trailing_whitespace = true
9
+ insert_final_newline = true
@@ -0,0 +1 @@
1
+ *.rb eol=lf
@@ -0,0 +1,8 @@
1
+ # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
2
+
3
+ version: 2
4
+ updates:
5
+ - package-ecosystem: "bundler"
6
+ directory: "/"
7
+ schedule:
8
+ interval: "daily"
@@ -0,0 +1,33 @@
1
+ name: CI
2
+ on: [push, pull_request]
3
+ jobs:
4
+ lint:
5
+ runs-on: ubuntu-latest
6
+ steps:
7
+ - name: Checkout
8
+ uses: actions/checkout@v2
9
+ - name: Set up Ruby
10
+ uses: ruby/setup-ruby@v1
11
+ with:
12
+ ruby-version: 2.7
13
+ - name: Build
14
+ run: bundle install --jobs 4 --retry 3
15
+ - name: Lint
16
+ run: bundle exec rake lint
17
+ test:
18
+ strategy:
19
+ matrix:
20
+ ruby: [jruby, 2.4, 2.5, 2.6, 2.7]
21
+ platform: [ubuntu-latest, macos-latest, windows-latest]
22
+ runs-on: ${{ matrix.platform }}
23
+ steps:
24
+ - name: Checkout
25
+ uses: actions/checkout@v2
26
+ - name: Set up Ruby
27
+ uses: ruby/setup-ruby@v1
28
+ with:
29
+ ruby-version: ${{ matrix.ruby }}
30
+ - name: Build
31
+ run: bundle install --jobs 4 --retry 3
32
+ - name: Test
33
+ run: bundle exec rake spec
@@ -0,0 +1,24 @@
1
+ name: Publish to RubyGems.org
2
+ on:
3
+ push:
4
+ tags:
5
+ - '*'
6
+ jobs:
7
+ build:
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - uses: actions/checkout@v2
11
+ - name: Set up Ruby
12
+ uses: ruby/setup-ruby@v1
13
+ with:
14
+ ruby-version: 2.7
15
+ - name: Publish to RubyGems.org
16
+ run: |
17
+ mkdir -p $HOME/.gem
18
+ touch $HOME/.gem/credentials
19
+ chmod 0600 $HOME/.gem/credentials
20
+ printf -- "---\n:rubygems_api_key: ${RUBYGEMS_API_KEY}\n" > $HOME/.gem/credentials
21
+ gem build *.gemspec
22
+ gem push *.gem
23
+ env:
24
+ RUBYGEMS_API_KEY: ${{secrets.RUBYGEMS_API_KEY}}
@@ -0,0 +1,3 @@
1
+ /.bundle/
2
+ /.idea/
3
+ /Gemfile.lock
@@ -0,0 +1,12 @@
1
+ = {project-name} Changelog
2
+ :project-name: FB2rb
3
+ :project-handle: fb2rb
4
+ :slug: slonopotamus/{project-handle}
5
+ :uri-project: https://github.com/{slug}
6
+
7
+ This document provides a high-level view of the changes to the {project-name} by release.
8
+ For a detailed view of what has changed, refer to the {uri-project}/commits/master[commit history] on GitHub.
9
+
10
+ == 0.1.0 (2020-07-23) - @slonopotamus
11
+
12
+ * Initial release
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Marat Radchenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,56 @@
1
+ = {project-name}
2
+ Marat Radchenko <marat@slonopotamus.org>
3
+ :project-name: FB2rb
4
+ :project-handle: fb2rb
5
+ :slug: slonopotamus/{project-handle}
6
+ :uri-project: https://github.com/{slug}
7
+ :uri-ci: {uri-project}/actions?query=branch%3Amaster
8
+ :uri-gem: https://rubygems.org/gems/{project-handle}
9
+
10
+ image:https://img.shields.io/gem/v/{project-handle}.svg[Latest Release,link={uri-gem}]
11
+ image:{uri-project}/workflows/CI/badge.svg?branch=master[Build Status,link={uri-ci}]
12
+
13
+ {project-handle} is a http://gribuser.ru/xml/fictionbook/index.html.en[Fiction Book 2] parser/generator library written in Ruby.
14
+
15
+ == Installation
16
+
17
+ Add this line to your application's Gemfile:
18
+
19
+ [source,ruby]
20
+ ----
21
+ gem 'fb2rb'
22
+ ----
23
+
24
+ And then execute:
25
+
26
+ [source,shell script]
27
+ ----
28
+ $ bundle install
29
+ ----
30
+
31
+ Or install it yourself as:
32
+
33
+ [source,shell script]
34
+ ----
35
+ $ gem install fb2rb
36
+ ----
37
+
38
+ == Usage
39
+
40
+ [source,ruby]
41
+ ----
42
+ require 'fb2rb'
43
+
44
+ book = FB2rb::Book.new
45
+ book.description.title_info.book_title = 'Book title'
46
+
47
+ body = FB2rb::Body.new(nil, '<p>Book text</p>')
48
+ book.bodies << body
49
+
50
+ book.write('/path/to/book.fb2.zip')
51
+ ----
52
+
53
+ == Development
54
+
55
+ After checking out the repo, run `bundle install` to install dependencies.
56
+ Then, run `bundle exec rake spec` to run the tests.
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ $default_tasks = [] # rubocop:disable Style/GlobalVars
4
+ Dir.glob('tasks/*.rake').each { |file| load file }
5
+ task default: $default_tasks unless $default_tasks.empty? # rubocop:disable Style/GlobalVars
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/fb2rb/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'fb2rb'
7
+ s.version = FB2rb::VERSION
8
+ s.authors = ['Marat Radchenko']
9
+ s.email = ['marat@slonopotamus.org']
10
+ s.summary = 'Fiction Book 2 parser/generator library'
11
+ s.homepage = 'https://github.com/slonopotamus/fb2rb'
12
+ s.license = 'MIT'
13
+ s.required_ruby_version = '>= 2.4.0'
14
+
15
+ s.files = `git ls-files`.split("\n").reject { |f| f.match(%r{^spec/}) }
16
+ s.executables = `git ls-files -- bin/*`.split("\n").map do |f|
17
+ File.basename(f)
18
+ end
19
+ s.require_paths = ['lib']
20
+
21
+ s.add_runtime_dependency 'nokogiri', '~> 1.10.0'
22
+ s.add_runtime_dependency 'rubyzip', '~> 2.3.0'
23
+
24
+ s.add_development_dependency 'rake', '~> 13.0.0'
25
+ s.add_development_dependency 'rspec', '~> 3.9.0'
26
+ s.add_development_dependency 'rubocop', '~> 0.88.0'
27
+ s.add_development_dependency 'rubocop-rspec', '~> 1.42.0'
28
+ end
@@ -0,0 +1,607 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'date'
5
+ require 'fb2rb/version'
6
+ require 'nokogiri'
7
+ require 'zip'
8
+
9
+ # Fiction Book 2 parser/generator library
10
+ module FB2rb
11
+ FB2_NAMESPACE = 'http://www.gribuser.ru/xml/fictionbook/2.0'
12
+ XLINK_NAMESPACE = 'http://www.w3.org/1999/xlink'
13
+
14
+ # Holds data of a single FB2 file
15
+ class Book
16
+ attr_accessor(:stylesheets)
17
+ attr_accessor(:description)
18
+ attr_accessor(:bodies)
19
+ attr_accessor(:binaries)
20
+
21
+ def initialize(description = Description.new, bodies = [], binaries = [], stylesheets = [])
22
+ @binaries = binaries
23
+ @bodies = bodies
24
+ @description = description
25
+ @stylesheets = stylesheets
26
+ end
27
+
28
+ # Reads existing FB2 file from an IO object, and creates new Book object.
29
+ def self.read(filename_or_io)
30
+ Zip::InputStream.open(filename_or_io) do |zis|
31
+ while (entry = zis.get_next_entry)
32
+ next if entry.directory?
33
+
34
+ xml = Nokogiri::XML::Document.parse(zis)
35
+ fb2_prefix = ns_prefix(FB2rb::FB2_NAMESPACE, xml.namespaces)
36
+ xlink_prefix = ns_prefix(FB2rb::XLINK_NAMESPACE, xml.namespaces)
37
+ return parse(xml, fb2_prefix, xlink_prefix)
38
+ end
39
+ end
40
+ end
41
+
42
+ def self.ns_prefix(namespace, namespaces)
43
+ prefix = namespaces.key(namespace)
44
+ prefix.nil? ? nil : prefix.sub(/^xmlns:/, '')
45
+ end
46
+
47
+ def self.parse(xml, fb2_prefix, xlink_prefix) # rubocop:disable Metrics/MethodLength
48
+ Book.new(
49
+ Description.parse(xml.xpath("/#{fb2_prefix}:FictionBook/#{fb2_prefix}:description"), fb2_prefix, xlink_prefix),
50
+ xml.xpath("/#{fb2_prefix}:FictionBook/#{fb2_prefix}:body").map do |node|
51
+ Body.parse(node)
52
+ end,
53
+ xml.xpath("#{fb2_prefix}:FictionBook/#{fb2_prefix}:binary").map do |node|
54
+ Binary.parse(node)
55
+ end,
56
+ xml.xpath("/#{fb2_prefix}:FictionBook/#{fb2_prefix}:stylesheet").map do |node|
57
+ Stylesheet.parse(node)
58
+ end
59
+ )
60
+ end
61
+
62
+ def to_xml(xml) # rubocop:disable Metrics/MethodLength
63
+ xml.FictionBook('xmlns' => FB2rb::FB2_NAMESPACE,
64
+ 'xmlns:l' => 'http://www.w3.org/1999/xlink') do
65
+ @stylesheets.each do |stylesheet|
66
+ stylesheet.to_xml(xml)
67
+ end
68
+ @description.to_xml(xml)
69
+ @bodies.each do |body|
70
+ body.to_xml(xml)
71
+ end
72
+ @binaries.each do |binary|
73
+ binary.to_xml(xml)
74
+ end
75
+ end
76
+ end
77
+
78
+ def add_binary(name, filename_or_io, content_type = nil)
79
+ if filename_or_io.respond_to?(:read)
80
+ add_binary_io name, filename_or_io, content_type
81
+ else
82
+ File.open(filename_or_io, 'r') do |io|
83
+ add_binary_io name, io, content_type
84
+ end
85
+ end
86
+ end
87
+
88
+ # Serializes and returns FB2 as StringIO.
89
+ def to_ios
90
+ Zip::OutputStream.write_buffer do |io|
91
+ write_to_stream(io)
92
+ end
93
+ end
94
+
95
+ # Writes FB2 to file or IO object. If file exists, it will be overwritten.
96
+ def write(filename_or_io)
97
+ if filename_or_io.respond_to?(:write)
98
+ Zip::OutputStream.write_buffer(filename_or_io) do |zos|
99
+ write_to_zip(zos)
100
+ end
101
+ else
102
+ Zip::OutputStream.open(filename_or_io) do |zos|
103
+ write_to_zip(zos)
104
+ end
105
+ end
106
+ end
107
+
108
+ private
109
+
110
+ def write_to_zip(zos)
111
+ # TODO: entry name
112
+ zos.put_next_entry('book.fb2')
113
+ write_to_stream(zos)
114
+ end
115
+
116
+ def add_binary_io(name, io, content_type = nil)
117
+ io.binmode
118
+ content = io.read
119
+ @binaries << Binary.new(name, content, content_type)
120
+ self
121
+ end
122
+
123
+ # Writes FB2 (uncompressed) to stream specified by the argument.
124
+ def write_to_stream(io)
125
+ builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
126
+ to_xml(xml)
127
+ end
128
+ xml = builder.to_xml
129
+ io.write(xml)
130
+ end
131
+ end
132
+
133
+ # Holds <description> data
134
+ class Description
135
+ attr_accessor(:title_info)
136
+ attr_accessor(:src_title_info)
137
+ attr_accessor(:document_info)
138
+ attr_accessor(:publish_info)
139
+ attr_accessor(:custom_infos)
140
+ # TODO: <output>
141
+
142
+ def initialize(title_info = TitleInfo.new,
143
+ document_info = DocumentInfo.new,
144
+ publish_info = nil,
145
+ src_title_info = nil,
146
+ custom_infos = [])
147
+ @title_info = title_info
148
+ @document_info = document_info
149
+ @publish_info = publish_info
150
+ @src_title_info = src_title_info
151
+ @custom_infos = custom_infos
152
+ end
153
+
154
+ def self.parse(xml, fb2_prefix, xlink_prefix) # rubocop:disable Metrics/MethodLength
155
+ publish_info_xml = xml.at("./#{fb2_prefix}:publish-info")
156
+ src_title_info_xml = xml.at("./#{fb2_prefix}:src-title-info")
157
+ Description.new(
158
+ TitleInfo.parse(xml.at("./#{fb2_prefix}:title-info"), fb2_prefix, xlink_prefix),
159
+ DocumentInfo.parse(xml.at("./#{fb2_prefix}:document-info"), fb2_prefix),
160
+ publish_info_xml.nil? ? nil : PublishInfo.parse(publish_info_xml, fb2_prefix),
161
+ src_title_info_xml.nil? ? nil : TitleInfo.parse(src_title_info_xml, fb2_prefix, xlink_prefix),
162
+ xml.xpath("./#{fb2_prefix}:custom-info").map do |node|
163
+ CustomInfo.parse(node)
164
+ end
165
+ )
166
+ end
167
+
168
+ def to_xml(xml)
169
+ xml.description do
170
+ @title_info.to_xml(xml, :'title-info')
171
+ @src_title_info&.to_xml(xml, :'src-title-info')
172
+ @document_info.to_xml(xml)
173
+ @publish_info&.to_xml(xml)
174
+ @custom_infos.each do |custom_info|
175
+ custom_info.to_xml(xml)
176
+ end
177
+ end
178
+ end
179
+ end
180
+
181
+ # Holds <stylesheet> data
182
+ class Stylesheet
183
+ attr_accessor(:type)
184
+ attr_accessor(:content)
185
+
186
+ def initialize(type = '', content = nil)
187
+ @type = type
188
+ @content = content
189
+ end
190
+
191
+ def self.parse(xml)
192
+ Stylesheet.new(xml['type'], xml.text)
193
+ end
194
+
195
+ def to_xml(xml)
196
+ return if @content.nil?
197
+
198
+ xml.send('stylesheet', @content, 'type' => @type)
199
+ end
200
+ end
201
+
202
+ # Holds <custom-info> data
203
+ class CustomInfo
204
+ attr_accessor(:info_type)
205
+ attr_accessor(:content)
206
+
207
+ def initialize(info_type = '', content = nil)
208
+ @info_type = info_type
209
+ @content = content
210
+ end
211
+
212
+ def self.parse(xml)
213
+ CustomInfo.new(xml['info-type'], xml.text)
214
+ end
215
+
216
+ def to_xml(xml)
217
+ return if @content.nil?
218
+
219
+ xml.send('custom-info', @content, 'info-type' => @info_type)
220
+ end
221
+ end
222
+
223
+ # Holds <publish-info> data
224
+ class PublishInfo
225
+ attr_accessor(:book_name)
226
+ attr_accessor(:publisher)
227
+ attr_accessor(:city)
228
+ attr_accessor(:year)
229
+ attr_accessor(:isbn)
230
+ attr_accessor(:sequences)
231
+
232
+ def initialize(book_name = nil, # rubocop:disable Metrics/ParameterLists
233
+ publisher = nil,
234
+ city = nil,
235
+ year = nil,
236
+ isbn = nil,
237
+ sequences = [])
238
+ @book_name = book_name
239
+ @publisher = publisher
240
+ @city = city
241
+ @year = year
242
+ @isbn = isbn
243
+ @sequences = sequences
244
+ end
245
+
246
+ def self.parse(xml, fb2_prefix)
247
+ PublishInfo.new(
248
+ xml.at("./#{fb2_prefix}:book-name/text()")&.text,
249
+ xml.at("./#{fb2_prefix}:publisher/text()")&.text,
250
+ xml.at("./#{fb2_prefix}:city/text()")&.text,
251
+ xml.at("./#{fb2_prefix}:year/text()")&.text,
252
+ xml.at("./#{fb2_prefix}:isbn/text()")&.text,
253
+ xml.xpath("./#{fb2_prefix}:sequence").map do |node|
254
+ Sequence.parse(node)
255
+ end
256
+ )
257
+ end
258
+
259
+ def to_xml(xml)
260
+ xml.send('publish-info') do
261
+ xml.send('book-name', @book_name) unless @book_name.nil?
262
+ xml.publisher(@publisher) unless @publisher.nil?
263
+ xml.city(@city) unless @city.nil?
264
+ xml.year(@year) unless @year.nil?
265
+ xml.isbn(@isbn) unless @isbn.nil?
266
+ @sequences.each do |sequence|
267
+ sequence.to_xml(xml)
268
+ end
269
+ end
270
+ end
271
+ end
272
+
273
+ # Holds <document-info> data
274
+ class DocumentInfo
275
+ attr_accessor(:authors)
276
+ attr_accessor(:program_used)
277
+ attr_accessor(:date)
278
+ attr_accessor(:src_urls)
279
+ attr_accessor(:src_ocr)
280
+ attr_accessor(:id)
281
+ attr_accessor(:version)
282
+ attr_accessor(:history)
283
+ attr_accessor(:publishers)
284
+
285
+ def initialize(authors = [], # rubocop:disable Metrics/ParameterLists
286
+ program_used = nil,
287
+ date = FB2Date.new,
288
+ src_urls = [],
289
+ src_ocr = nil,
290
+ id = '',
291
+ version = '',
292
+ history = nil,
293
+ publishers = [])
294
+ @authors = authors
295
+ @program_used = program_used
296
+ @date = date
297
+ @src_urls = src_urls
298
+ @src_ocr = src_ocr
299
+ @id = id
300
+ @version = version
301
+ @history = history
302
+ @publishers = publishers
303
+ end
304
+
305
+ def self.parse(xml, fb2_prefix) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
306
+ date = xml.at("./#{fb2_prefix}:date")
307
+ DocumentInfo.new(
308
+ xml.xpath("./#{fb2_prefix}:author").map do |node|
309
+ Author.parse(node, fb2_prefix)
310
+ end,
311
+ xml.at("./#{fb2_prefix}:program-used")&.text,
312
+ date.nil? ? FB2Date.new : FB2Date.parse(date),
313
+ xml.xpath("./#{fb2_prefix}:src-url").map(&:text),
314
+ xml.at("./#{fb2_prefix}:src-ocr")&.text,
315
+ xml.at("./#{fb2_prefix}:id").text,
316
+ xml.at("./#{fb2_prefix}:version")&.text,
317
+ xml.at("./#{fb2_prefix}:history")&.children&.to_s&.strip
318
+ )
319
+ end
320
+
321
+ def to_xml(xml) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
322
+ xml.send(:'document-info') do
323
+ @authors.each do |author|
324
+ author.to_xml(xml, 'author')
325
+ end
326
+ xml.send('program-used', @program_used) unless @program_used.nil?
327
+ @date.to_xml(xml)
328
+ @src_urls.each do |src_url|
329
+ xml.send('src-url', src_url)
330
+ end
331
+ xml.send('src-ocr', @src_ocr) unless @src_ocr.nil?
332
+ xml.id(@id)
333
+ xml.version(@version) unless @version.nil?
334
+ unless @history.nil?
335
+ xml.history do
336
+ xml << @history
337
+ end
338
+ end
339
+ end
340
+ end
341
+ end
342
+
343
+ # Holds <title-info>/<src-title-info> data
344
+ class TitleInfo
345
+ attr_accessor(:genres)
346
+ attr_accessor(:authors)
347
+ attr_accessor(:book_title)
348
+ attr_accessor(:annotation)
349
+ attr_accessor(:keywords)
350
+ attr_accessor(:date)
351
+ attr_accessor(:coverpage)
352
+ attr_accessor(:lang)
353
+ attr_accessor(:src_lang)
354
+ attr_accessor(:translators)
355
+ attr_accessor(:sequences)
356
+
357
+ def initialize(genres = [], # rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
358
+ authors = [],
359
+ book_title = '',
360
+ annotation = nil,
361
+ keywords = [],
362
+ date = nil,
363
+ coverpage = nil,
364
+ lang = 'en',
365
+ src_lang = nil,
366
+ translators = [],
367
+ sequences = [])
368
+ @genres = genres
369
+ @authors = authors
370
+ @book_title = book_title
371
+ @annotation = annotation
372
+ @keywords = keywords
373
+ @date = date
374
+ @coverpage = coverpage
375
+ @lang = lang
376
+ @src_lang = src_lang
377
+ @translators = translators
378
+ @sequences = sequences
379
+ end
380
+
381
+ def self.parse(xml, fb2_prefix, xlink_prefix) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
382
+ date = xml.at("./#{fb2_prefix}:date")
383
+ coverpage = xml.at("./#{fb2_prefix}:coverpage")
384
+ TitleInfo.new(
385
+ xml.xpath("./#{fb2_prefix}:genre/text()").map(&:text),
386
+ xml.xpath("./#{fb2_prefix}:author").map do |node|
387
+ Author.parse(node, fb2_prefix)
388
+ end,
389
+ xml.at("./#{fb2_prefix}:book-title/text()")&.text,
390
+ xml.at("./#{fb2_prefix}:annotation")&.children.to_s.strip,
391
+ xml.at("./#{fb2_prefix}:keywords/text()")&.text&.split(', ') || [],
392
+ date.nil? ? nil : FB2Date.parse(date),
393
+ coverpage.nil? ? nil : Coverpage.parse(coverpage, fb2_prefix, xlink_prefix),
394
+ xml.at("./#{fb2_prefix}:lang/text()").text,
395
+ xml.at("./#{fb2_prefix}:src-lang/text()")&.text,
396
+ xml.xpath("./#{fb2_prefix}:translator").map do |node|
397
+ Author.parse(node, fb2_prefix)
398
+ end,
399
+ xml.xpath("./#{fb2_prefix}:sequence").map do |node|
400
+ Sequence.parse(node)
401
+ end
402
+ )
403
+ end
404
+
405
+ def to_xml(xml, tag) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
406
+ xml.send(tag) do
407
+ genres.each do |genre|
408
+ xml.genre(genre)
409
+ end
410
+ authors.each do |author|
411
+ author.to_xml(xml, 'author')
412
+ end
413
+ xml.send('book-title', @book_title)
414
+ unless @annotation.nil?
415
+ xml.annotation do
416
+ xml << @annotation
417
+ end
418
+ end
419
+ xml.keywords(@keywords.join(', ')) unless keywords.nil?
420
+ @date.to_xml(xml) unless date.nil?
421
+ @coverpage.to_xml(xml) unless coverpage.nil?
422
+ xml.lang(@lang)
423
+ xml.send('src-lang') unless src_lang.nil?
424
+ @translators.each do |translator|
425
+ translator.to_xml(xml, 'translator')
426
+ end
427
+ @sequences.each do |sequence|
428
+ sequence.to_xml(xml)
429
+ end
430
+ end
431
+ end
432
+ end
433
+
434
+ # Holds <coverpage> data
435
+ class Coverpage
436
+ attr_accessor(:images)
437
+
438
+ def initialize(images = [])
439
+ @images = images
440
+ end
441
+
442
+ def self.parse(xml, fb2_prefix, xlink_prefix)
443
+ Coverpage.new(
444
+ xml.xpath("./#{fb2_prefix}:image/@#{xlink_prefix}:href").map(&:to_s)
445
+ )
446
+ end
447
+
448
+ def to_xml(xml)
449
+ xml.coverpage do
450
+ @images.each do |image|
451
+ xml.image('l:href' => image)
452
+ end
453
+ end
454
+ end
455
+ end
456
+
457
+ # Holds <date> data
458
+ class FB2Date
459
+ attr_accessor(:display_value)
460
+ attr_accessor(:value)
461
+
462
+ def initialize(display_value = '', value = nil)
463
+ @display_value = display_value
464
+ @value = value
465
+ end
466
+
467
+ def self.parse(xml)
468
+ value = xml['value']
469
+ FB2Date.new(
470
+ xml.at('./text()')&.text || '',
471
+ value.nil? ? nil : Date.parse(value)
472
+ )
473
+ end
474
+
475
+ def to_xml(xml)
476
+ xml.date(@display_value) do
477
+ xml.parent['value'] = @value.to_s unless value.nil?
478
+ end
479
+ end
480
+ end
481
+
482
+ # Holds <sequence> data
483
+ class Sequence
484
+ attr_accessor(:name)
485
+ attr_accessor(:number)
486
+
487
+ def initialize(name = '', number = nil)
488
+ @name = name
489
+ @number = number
490
+ end
491
+
492
+ def self.parse(xml)
493
+ Sequence.new(xml['name'], xml['number']&.to_i)
494
+ end
495
+
496
+ def to_xml(xml)
497
+ xml.send('sequence', 'name' => @name) do
498
+ xml.parent['number'] = @number unless @number.nil?
499
+ end
500
+ end
501
+ end
502
+
503
+ # Holds <author> data
504
+ class Author
505
+ attr_accessor(:first_name)
506
+ attr_accessor(:middle_name)
507
+ attr_accessor(:last_name)
508
+ attr_accessor(:nickname)
509
+ attr_accessor(:home_pages)
510
+ attr_accessor(:emails)
511
+ attr_accessor(:id)
512
+
513
+ def initialize(first_name = nil, # rubocop:disable Metrics/ParameterLists
514
+ middle_name = nil,
515
+ last_name = nil,
516
+ nickname = nil,
517
+ home_pages = [],
518
+ emails = [],
519
+ id = nil)
520
+ @first_name = first_name
521
+ @middle_name = middle_name
522
+ @last_name = last_name
523
+ @nickname = nickname
524
+ @home_pages = home_pages
525
+ @emails = emails
526
+ @id = id
527
+ end
528
+
529
+ def self.parse(xml, fb2_prefix) # rubocop:disable Metrics/CyclomaticComplexity
530
+ Author.new(
531
+ xml.at("./#{fb2_prefix}:first-name/text()")&.text,
532
+ xml.at("./#{fb2_prefix}:middle-name/text()")&.text,
533
+ xml.at("./#{fb2_prefix}:last-name/text()")&.text,
534
+ xml.at("./#{fb2_prefix}:nickname/text()")&.text,
535
+ xml.xpath("./#{fb2_prefix}:home-page/text()").map(&:text),
536
+ xml.xpath("./#{fb2_prefix}:email/text()").map(&:text),
537
+ xml.at("./#{fb2_prefix}:id/text()")&.text
538
+ )
539
+ end
540
+
541
+ def to_xml(xml, tag) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
542
+ xml.send(tag) do
543
+ xml.send('first-name', @first_name) unless @first_name.nil?
544
+ xml.send('middle-name', @middle_name) unless @middle_name.nil?
545
+ xml.send('last-name', @last_name) unless @last_name.nil?
546
+ xml.nickname(@nickname) unless @nickname.nil?
547
+ @home_pages.each do |home_page|
548
+ xml.send('home-page', home_page)
549
+ end
550
+ @emails.each do |email|
551
+ xml.email(email)
552
+ end
553
+ xml.id(@id) unless @id.nil?
554
+ end
555
+ end
556
+ end
557
+
558
+ # Holds <body> data
559
+ class Body
560
+ attr_accessor(:name)
561
+ attr_accessor(:content)
562
+
563
+ def initialize(name = nil, content = '')
564
+ @name = name
565
+ @content = content
566
+ end
567
+
568
+ def self.parse(xml)
569
+ Body.new(
570
+ xml['name'],
571
+ xml.children.to_s.strip
572
+ )
573
+ end
574
+
575
+ def to_xml(xml)
576
+ return if @content.nil?
577
+
578
+ xml.body do
579
+ xml.parent['name'] = @name unless @name.nil?
580
+ xml << @content
581
+ end
582
+ end
583
+ end
584
+
585
+ # Holds data of a single binary within FB2 file
586
+ class Binary
587
+ attr_accessor(:id)
588
+ attr_accessor(:content)
589
+ attr_accessor(:content_type)
590
+
591
+ def initialize(name, content, content_type = nil)
592
+ @id = name
593
+ @content = content
594
+ @content_type = content_type
595
+ end
596
+
597
+ def self.parse(xml)
598
+ decoded = Base64.decode64(xml.text)
599
+ Binary.new(xml['id'], decoded, xml['content-type'])
600
+ end
601
+
602
+ def to_xml(xml)
603
+ encoded = Base64.encode64(@content)
604
+ xml.binary(encoded, 'id' => @id, 'content-type' => @content_type)
605
+ end
606
+ end
607
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FB2rb
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+
5
+ $default_tasks << :build # rubocop:disable Style/GlobalVars
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rake/clean'
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ desc 'Open an irb session preloaded with this library'
4
+ task :console do
5
+ sh 'bundle console', verbose: false
6
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/core/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new :spec do |t|
6
+ t.verbose = true
7
+ opts = %w[-f progress]
8
+ opts.append '-t', '~visual', '-t', '~cli' if ENV['UNIT']
9
+ opts.unshift '-w' if $VERBOSE || ENV['COVERAGE']
10
+ t.rspec_opts = opts
11
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop/rake_task'
4
+
5
+ RuboCop::RakeTask.new :lint do |t|
6
+ t.patterns = Dir['lib/*.rb'] + %w[Rakefile Gemfile tasks/*.rake spec/*.rb]
7
+ end
metadata ADDED
@@ -0,0 +1,146 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fb2rb
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Marat Radchenko
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-07-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: nokogiri
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 1.10.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 1.10.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: rubyzip
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 2.3.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 2.3.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 13.0.0
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 13.0.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 3.9.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 3.9.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.88.0
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.88.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop-rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 1.42.0
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 1.42.0
97
+ description:
98
+ email:
99
+ - marat@slonopotamus.org
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - ".editorconfig"
105
+ - ".gitattributes"
106
+ - ".github/dependabot.yml"
107
+ - ".github/workflows/ci.yml"
108
+ - ".github/workflows/release.yml"
109
+ - ".gitignore"
110
+ - CHANGELOG.adoc
111
+ - Gemfile
112
+ - LICENSE.txt
113
+ - README.adoc
114
+ - Rakefile
115
+ - fb2rb.gemspec
116
+ - lib/fb2rb.rb
117
+ - lib/fb2rb/version.rb
118
+ - tasks/bundler.rake
119
+ - tasks/clean.rake
120
+ - tasks/console.rake
121
+ - tasks/rspec.rake
122
+ - tasks/rubocop.rake
123
+ homepage: https://github.com/slonopotamus/fb2rb
124
+ licenses:
125
+ - MIT
126
+ metadata: {}
127
+ post_install_message:
128
+ rdoc_options: []
129
+ require_paths:
130
+ - lib
131
+ required_ruby_version: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - ">="
134
+ - !ruby/object:Gem::Version
135
+ version: 2.4.0
136
+ required_rubygems_version: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - ">="
139
+ - !ruby/object:Gem::Version
140
+ version: '0'
141
+ requirements: []
142
+ rubygems_version: 3.1.2
143
+ signing_key:
144
+ specification_version: 4
145
+ summary: Fiction Book 2 parser/generator library
146
+ test_files: []