rpub 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +8 -0
- data/.rspec +5 -0
- data/.travis.yml +5 -0
- data/.yardopts +6 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +51 -0
- data/Guardfile +5 -0
- data/HISTORY.md +5 -0
- data/LICENSE +0 -0
- data/README.md +182 -0
- data/Rakefile +11 -0
- data/bin/rpub +6 -0
- data/example/advanced/README +1 -0
- data/example/advanced/config.yml +11 -0
- data/example/advanced/layout.html +0 -0
- data/example/advanced/styles.css +0 -0
- data/example/simple/01-introduction.md +3 -0
- data/example/simple/02-foo.md +4 -0
- data/example/simple/03-bar.md +4 -0
- data/example/simple/config.yml +8 -0
- data/lib/rpub.rb +53 -0
- data/lib/rpub/book.rb +70 -0
- data/lib/rpub/chapter.rb +84 -0
- data/lib/rpub/commander.rb +14 -0
- data/lib/rpub/commands/base.rb +33 -0
- data/lib/rpub/commands/clean.rb +55 -0
- data/lib/rpub/commands/compile.rb +56 -0
- data/lib/rpub/commands/help.rb +37 -0
- data/lib/rpub/commands/main.rb +45 -0
- data/lib/rpub/commands/package.rb +45 -0
- data/lib/rpub/commands/preview.rb +56 -0
- data/lib/rpub/compilation_helpers.rb +43 -0
- data/lib/rpub/compressor.rb +46 -0
- data/lib/rpub/epub.rb +37 -0
- data/lib/rpub/epub/container.rb +14 -0
- data/lib/rpub/epub/content.rb +80 -0
- data/lib/rpub/epub/cover.rb +27 -0
- data/lib/rpub/epub/html_toc.rb +25 -0
- data/lib/rpub/epub/toc.rb +35 -0
- data/lib/rpub/hash_delegation.rb +23 -0
- data/lib/rpub/subclass_tracker.rb +53 -0
- data/lib/rpub/version.rb +3 -0
- data/lib/rpub/xml_file.rb +14 -0
- data/rpub.gemspec +52 -0
- data/spec/fixtures/clean/config.yml +2 -0
- data/spec/fixtures/clean/example.epub +0 -0
- data/spec/fixtures/clean/preview.html +0 -0
- data/spec/fixtures/no_files/config.yml +0 -0
- data/spec/fixtures/preview/a.md +1 -0
- data/spec/fixtures/preview/b.md +1 -0
- data/spec/fixtures/preview/config.yml +2 -0
- data/spec/rpub/book_spec.rb +62 -0
- data/spec/rpub/chapter_spec.rb +61 -0
- data/spec/rpub/commands/clean_spec.rb +46 -0
- data/spec/rpub/commands/main_spec.rb +26 -0
- data/spec/rpub/commands/preview_spec.rb +42 -0
- data/spec/rpub_spec.rb +7 -0
- data/spec/spec_helper.rb +21 -0
- data/support/layout.html +22 -0
- data/support/styles.css +66 -0
- metadata +310 -0
@@ -0,0 +1,46 @@
|
|
1
|
+
module Rpub
|
2
|
+
# Wrapper around a `ZipOutputStream` object provided by the `rubyzip` gem.
|
3
|
+
# This writes string contents straight into a zip file, without first saving
|
4
|
+
# them to disk.
|
5
|
+
class Compressor
|
6
|
+
|
7
|
+
# @return [ZipOutputStream]
|
8
|
+
attr_reader :zip
|
9
|
+
|
10
|
+
# Convenience method for opening a stream, allowing content to be written
|
11
|
+
# and finally closing the stream again.
|
12
|
+
def self.open(filename)
|
13
|
+
compressor = new(filename)
|
14
|
+
yield compressor
|
15
|
+
compressor.close
|
16
|
+
end
|
17
|
+
|
18
|
+
# @param [String] filename of the archive to write to disk
|
19
|
+
def initialize(filename)
|
20
|
+
@zip = Zip::ZipOutputStream.new(filename)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Close the zip stream and write the file to disk.
|
24
|
+
def close
|
25
|
+
zip.close
|
26
|
+
end
|
27
|
+
|
28
|
+
# Store a file in the archive without any compression.
|
29
|
+
#
|
30
|
+
# @param [String] filename under the which the data should be stored
|
31
|
+
# @param [#to_s] content to be compressed
|
32
|
+
def store_file(filename, content)
|
33
|
+
zip.put_next_entry filename, nil, nil, Zip::ZipEntry::STORED, Zlib::NO_COMPRESSION
|
34
|
+
zip.write content.to_s
|
35
|
+
end
|
36
|
+
|
37
|
+
# Store a file with maximum compression in the archive.
|
38
|
+
#
|
39
|
+
# @param [String] filename under the which the data should be stored
|
40
|
+
# @param [#to_s] content to be compressed
|
41
|
+
def compress_file(filename, content)
|
42
|
+
zip.put_next_entry filename, nil, nil, Zip::ZipEntry::DEFLATED, Zlib::BEST_COMPRESSION
|
43
|
+
zip.write content.to_s
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/rpub/epub.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
module Rpub
|
2
|
+
class Epub
|
3
|
+
attr_reader :book, :styles
|
4
|
+
|
5
|
+
def initialize(book, styles)
|
6
|
+
@book, @styles = book, styles
|
7
|
+
end
|
8
|
+
|
9
|
+
def manifest_in(target)
|
10
|
+
target.store_file 'mimetype', 'application/epub+zip'
|
11
|
+
target.compress_file 'META-INF/container.xml', Container.new
|
12
|
+
target.compress_file 'OEBPS/content.opf', Content.new(book)
|
13
|
+
target.compress_file 'OEBPS/toc.ncx', Toc.new(book)
|
14
|
+
target.compress_file 'OEBPS/styles.css', styles
|
15
|
+
if book.cover?
|
16
|
+
target.compress_file 'OEBPS/cover.html', Cover.new(book)
|
17
|
+
target.compress_file File.join('OEBPS', book.cover_image), File.read(book.cover_image)
|
18
|
+
end
|
19
|
+
if book.toc?
|
20
|
+
target.compress_file 'OEBPS/toc.html', toc { HtmlToc.new(book).render }
|
21
|
+
end
|
22
|
+
book.each do |chapter|
|
23
|
+
target.compress_file File.join('OEBPS', chapter.filename), chapter.to_html
|
24
|
+
end
|
25
|
+
book.images.each do |image|
|
26
|
+
target.compress_file File.join('OEBPS', image), File.read(image)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def toc
|
33
|
+
@body = yield
|
34
|
+
ERB.new(File.read(book.layout)).result(binding)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Rpub
|
2
|
+
class Epub
|
3
|
+
class Container < XmlFile
|
4
|
+
def render
|
5
|
+
xml.instruct!
|
6
|
+
xml.container :version => '1.0', :xmlns => 'urn:oasis:names:tc:opendocument:xmlns:container' do
|
7
|
+
xml.rootfiles do
|
8
|
+
xml.rootfile 'full-path' => 'OEBPS/content.opf', 'media-type' => 'application/oebps-package+xml'
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Rpub
|
2
|
+
class Epub
|
3
|
+
class Content < XmlFile
|
4
|
+
attr_reader :book
|
5
|
+
|
6
|
+
MEDIA_TYPES = {
|
7
|
+
'png' => 'image/png',
|
8
|
+
'gif' => 'image/gif',
|
9
|
+
'jpg' => 'image/jpeg',
|
10
|
+
'svg' => 'image/svg+xml'
|
11
|
+
}
|
12
|
+
|
13
|
+
def initialize(book)
|
14
|
+
@book = book
|
15
|
+
super()
|
16
|
+
end
|
17
|
+
|
18
|
+
def render
|
19
|
+
xml.instruct!
|
20
|
+
xml.declare! :DOCTYPE, :package, :PUBLIC, '-//W3C//DTD XHTML 1.1//EN', 'http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd'
|
21
|
+
xml.package 'xmlns' => 'http://www.idpf.org/2007/opf', 'unique-identifier' => 'BookId', 'version' => '2.0' do
|
22
|
+
xml.metadata 'xmlns:dc' => 'http://purl.org/dc/elements/1.1/', 'xmlns:opf' => 'http://www.idpf.org/2007/opf' do
|
23
|
+
xml.dc :language, book.language
|
24
|
+
xml.dc :title, book.title
|
25
|
+
xml.dc :creator, book.creator, 'opf:role' => 'aut'
|
26
|
+
xml.dc :publisher, book.publisher
|
27
|
+
xml.dc :subject, book.subject
|
28
|
+
xml.dc :identifier, book.uid, :id => 'BookId'
|
29
|
+
xml.dc :rights, book.rights
|
30
|
+
xml.dc :description, book.description
|
31
|
+
|
32
|
+
if book.cover?
|
33
|
+
xml.meta :name => 'cover', :content => 'cover-image'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
xml.manifest do
|
38
|
+
xml.item 'id' => 'ncx', 'href' => 'toc.ncx', 'media-type' => 'application/x-dtbncx+xml'
|
39
|
+
xml.item 'id' => 'css', 'href' => 'styles.css', 'media-type' => 'text/css'
|
40
|
+
|
41
|
+
if book.cover?
|
42
|
+
xml.item 'id' => 'cover', 'href' => 'cover.html', 'media-type' => 'application/xhtml+xml'
|
43
|
+
xml.item 'id' => 'cover-image', 'href' => book.cover_image, 'media-type' => guess_media_type(book.cover_image)
|
44
|
+
end
|
45
|
+
|
46
|
+
book.images.each do |image|
|
47
|
+
xml.item 'id' => File.basename(image), 'href' => image, 'media-type' => guess_media_type(image)
|
48
|
+
end
|
49
|
+
xml.item 'id' => 'toc', 'href' => 'toc.html', 'media-type' => 'application/xhtml+xml'
|
50
|
+
book.chapters.each do |chapter|
|
51
|
+
xml.item 'id' => chapter.id, 'href' => chapter.filename, 'media-type' => 'application/xhtml+xml'
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
xml.spine 'toc' => 'ncx' do
|
56
|
+
if book.cover?
|
57
|
+
xml.itemref 'idref' => 'cover', 'linear' => 'no'
|
58
|
+
end
|
59
|
+
book.chapters.each do |chapter|
|
60
|
+
xml.itemref 'idref' => chapter.id
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
if book.cover?
|
65
|
+
xml.guide do
|
66
|
+
xml.reference :type => 'cover', :title => 'Cover', :href => 'cover.html'
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def guess_media_type(filename)
|
75
|
+
MEDIA_TYPES.fetch(filename[/\.(gif|png|jpe?g|svg)$/, 1]) { 'image/png' }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Rpub
|
2
|
+
class Epub
|
3
|
+
class Cover < XmlFile
|
4
|
+
attr_reader :book
|
5
|
+
|
6
|
+
def initialize(book)
|
7
|
+
@book = book
|
8
|
+
super()
|
9
|
+
end
|
10
|
+
|
11
|
+
def render
|
12
|
+
xml.declare! :DOCTYPE, :html, :PUBLIC, '-//W3C//DTD XHTML 1.0 Strict//EN', 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'
|
13
|
+
xml.html :xmlns => 'http://www.w3.org/1999/xhtml' do
|
14
|
+
xml.head do
|
15
|
+
xml.title 'Cover'
|
16
|
+
xml.style 'img { max-width: 100%; }', :type => 'text/css'
|
17
|
+
end
|
18
|
+
xml.body do
|
19
|
+
xml.div :id => 'cover-image' do
|
20
|
+
xml.img :src => book.cover_image, :alt => book.title
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Rpub
|
2
|
+
class Epub
|
3
|
+
class HtmlToc < XmlFile
|
4
|
+
attr_reader :book
|
5
|
+
|
6
|
+
def initialize(book)
|
7
|
+
@book = book
|
8
|
+
super()
|
9
|
+
end
|
10
|
+
|
11
|
+
def render
|
12
|
+
xml.h1 'Table of Contents'
|
13
|
+
xml.div :class => 'toc' do
|
14
|
+
book.outline.each do |(filename, headings)|
|
15
|
+
headings.each do |heading|
|
16
|
+
xml.div :class => "level-#{heading.level}" do
|
17
|
+
xml.a heading.text, :href => [filename, heading.id].join('#')
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Rpub
|
2
|
+
class Epub
|
3
|
+
class Toc < XmlFile
|
4
|
+
attr_reader :book
|
5
|
+
|
6
|
+
def initialize(book)
|
7
|
+
@book = book
|
8
|
+
super()
|
9
|
+
end
|
10
|
+
|
11
|
+
def render
|
12
|
+
xml.instruct!
|
13
|
+
xml.declare! :DOCTYPE, :ncx, :PUBLIC, "-//W3C//DTD XHTML 1.1//EN", 'http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd'
|
14
|
+
xml.ncx :xmlns => 'http://www.daisy.org/z3986/2005/ncx/', :version => '2005-1' do
|
15
|
+
xml.head do
|
16
|
+
xml.meta :name => 'dtb:uid', :content => @book.uid
|
17
|
+
xml.meta :name => 'dtb:depth', :content => '1'
|
18
|
+
xml.meta :name => 'dtb:totalPageCount', :content => '0'
|
19
|
+
xml.meta :name => 'dtb:maxPageNumber', :content => '0'
|
20
|
+
end
|
21
|
+
xml.docTitle { xml.text @book.title }
|
22
|
+
xml.navMap do
|
23
|
+
@book.chapters.each_with_index do |chapter, n|
|
24
|
+
xml.navPoint :id => chapter.id, :playOrder => n do
|
25
|
+
xml.navLabel { xml.text chapter.title }
|
26
|
+
xml.content :src => chapter.filename
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Rpub
|
2
|
+
# Delegate missing methods to keys in a Hash atribute on the current object.
|
3
|
+
module HashDelegation
|
4
|
+
def self.included(base)
|
5
|
+
base.extend ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def delegate_to_hash(attr)
|
10
|
+
define_method(:delegated_hash) { send attr }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def respond_to?(m)
|
15
|
+
super || delegated_hash.has_key?(m.to_s)
|
16
|
+
end
|
17
|
+
|
18
|
+
def method_missing(m, *args, &block)
|
19
|
+
return super unless respond_to? m
|
20
|
+
delegated_hash.fetch m.to_s
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Rpub
|
2
|
+
# Add tracking of subclasses to an existing class by extending it with
|
3
|
+
# SubclassTracker.
|
4
|
+
#
|
5
|
+
# This allows you to set an identifier in a subclass using the `identifier`
|
6
|
+
# macro, and find subclasses based on that value.
|
7
|
+
#
|
8
|
+
# Example:
|
9
|
+
#
|
10
|
+
# class ParentClass
|
11
|
+
# extend SubclassTracker
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# class ChildClass < ParentClass
|
15
|
+
# identifier 'foo'
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# ParentClass.matching('foo') # => ChildClass
|
19
|
+
# ParentClass.matching('bar') # => raises SubclassTracker::NoSuchSubclass
|
20
|
+
#
|
21
|
+
# Note that you don't HAVE to set an identifier. If you don't, your child
|
22
|
+
# class will never be found by `#matching`.
|
23
|
+
module SubclassTracker
|
24
|
+
class NoSuchSubclass < StandardError
|
25
|
+
def initialize(subcommand)
|
26
|
+
super "Unrecognized identifier: #{subcommand}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Set or return the identifier for this class.
|
31
|
+
def identifier(id = nil)
|
32
|
+
return @identifier if id.nil?
|
33
|
+
@identifier = id
|
34
|
+
end
|
35
|
+
|
36
|
+
def inherited(child)
|
37
|
+
@subclasses ||= []
|
38
|
+
@subclasses << child
|
39
|
+
super
|
40
|
+
end
|
41
|
+
|
42
|
+
def each
|
43
|
+
@subclasses ||= []
|
44
|
+
@subclasses.each { |subclass| yield subclass }
|
45
|
+
end
|
46
|
+
|
47
|
+
def matching(identifier)
|
48
|
+
find { |subclass| subclass.identifier === identifier } or raise NoSuchSubclass, identifier
|
49
|
+
end
|
50
|
+
|
51
|
+
include Enumerable
|
52
|
+
end
|
53
|
+
end
|
data/lib/rpub/version.rb
ADDED
data/rpub.gemspec
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require './lib/rpub/version'
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
# Metadata
|
6
|
+
s.name = 'rpub'
|
7
|
+
s.version = Rpub::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ['Arjan van der Gaag']
|
10
|
+
s.email = %q{arjan@arjanvandergaag.nl}
|
11
|
+
s.description = %q{an ePub generation library in Ruby}
|
12
|
+
s.homepage = %q{http://avdgaag.github.com/rpub}
|
13
|
+
s.summary = <<-EOS
|
14
|
+
rPub is a command-line tool that generates a collection of plain text input
|
15
|
+
files into an eBook in ePub format. It provides several related functions to
|
16
|
+
make working with ePub files a little easier:
|
17
|
+
|
18
|
+
* Generation of table of contents
|
19
|
+
* Tracking of references to tables or figures
|
20
|
+
* Validation of output file
|
21
|
+
* Packaging your eBook in an archive with additional README file
|
22
|
+
* Very simple version control
|
23
|
+
EOS
|
24
|
+
|
25
|
+
# Files
|
26
|
+
s.files = `git ls-files`.split("\n")
|
27
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
28
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
29
|
+
s.require_paths = ["lib"]
|
30
|
+
|
31
|
+
# Rdoc
|
32
|
+
s.rdoc_options = ['--charset=UTF-8']
|
33
|
+
s.extra_rdoc_files = [
|
34
|
+
'LICENSE',
|
35
|
+
'README.md',
|
36
|
+
'HISTORY.md'
|
37
|
+
]
|
38
|
+
|
39
|
+
# Dependencies
|
40
|
+
s.add_runtime_dependency 'typogruby'
|
41
|
+
s.add_runtime_dependency 'kramdown'
|
42
|
+
s.add_runtime_dependency 'rubyzip'
|
43
|
+
s.add_runtime_dependency 'builder'
|
44
|
+
s.add_development_dependency 'yard'
|
45
|
+
s.add_development_dependency 'rspec'
|
46
|
+
s.add_development_dependency 'rake'
|
47
|
+
s.add_development_dependency 'guard'
|
48
|
+
s.add_development_dependency 'guard-rspec'
|
49
|
+
s.add_development_dependency 'rb-fsevent'
|
50
|
+
s.add_development_dependency 'growl'
|
51
|
+
end
|
52
|
+
|
File without changes
|
File without changes
|
File without changes
|
@@ -0,0 +1 @@
|
|
1
|
+
foo
|
@@ -0,0 +1 @@
|
|
1
|
+
bar
|