fb2rb 0.1.0

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