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
data/lib/rpub/chapter.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
module Rpub
|
2
|
+
# Representation of a chapter in the book, from a single input file from the
|
3
|
+
# project directory. The Chapter object knows how to turn itself into HTML
|
4
|
+
# suitable for writing to the epub archive with the appropriate identifiers
|
5
|
+
# to be listed in the epub manifest files.
|
6
|
+
class Chapter
|
7
|
+
# @return [String] raw textual contents of this chapter
|
8
|
+
attr_reader :content
|
9
|
+
|
10
|
+
# @return [Fixnum] chapter number provided by its associated Book object
|
11
|
+
attr_reader :number
|
12
|
+
|
13
|
+
# @return [String] filename of the layout to use, to be passed directly to the Kramdown gem.
|
14
|
+
attr_reader :layout
|
15
|
+
|
16
|
+
def initialize(content, number, layout)
|
17
|
+
@content, @number, @layout = content, number, layout
|
18
|
+
@document = Kramdown::Document.new(content, KRAMDOWN_OPTIONS.merge(:template => layout))
|
19
|
+
end
|
20
|
+
|
21
|
+
# @return [String] Unique identifier for this chapter.
|
22
|
+
def uid
|
23
|
+
@uid ||= Digest::SHA1.hexdigest([content, id.to_s, layout].join)
|
24
|
+
end
|
25
|
+
|
26
|
+
# @return [String] XML-friendly slug for this chapter based on its number.
|
27
|
+
def id
|
28
|
+
@id ||= "chapter-#{number}"
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return [String] content parsed to HTML by the markdown engine.
|
32
|
+
def to_html
|
33
|
+
@to_html ||= @document.to_html
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [String] name for the file in the zip to use, based on the title
|
37
|
+
def filename
|
38
|
+
@filename ||= id.to_s + '-' + title.gsub(/[^\w\.]/i, '-').squeeze('-').downcase.chomp('-') + '.html'
|
39
|
+
end
|
40
|
+
|
41
|
+
# Ordered headers for this chapter, each header as an object responding
|
42
|
+
# to #level and #text.
|
43
|
+
#
|
44
|
+
# @return [Array<#text,#level>] list of headers for this chapter
|
45
|
+
def outline
|
46
|
+
@outline ||= elements(:header).map do |element|
|
47
|
+
OpenStruct.new({
|
48
|
+
:level => element.options[:level],
|
49
|
+
:text => element_text(element),
|
50
|
+
:id => Kramdown::Converter::Html.send(:new, @document, { :auto_id_prefix => '' }).generate_id(element.options[:raw_text])
|
51
|
+
})
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# @return [Array<String>] list of all image references
|
56
|
+
def images
|
57
|
+
@images ||= elements(:img).map { |e| e.attr['src'] }
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return [String] Text of the first heading in this chapter
|
61
|
+
def title
|
62
|
+
@title ||= begin
|
63
|
+
(heading = outline.first) ? heading.text : 'untitled'
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def element_text(element)
|
70
|
+
elements(:text, element).map { |e| e.value }.join
|
71
|
+
end
|
72
|
+
|
73
|
+
def elements(type, root = @document.root)
|
74
|
+
collector = lambda do |element|
|
75
|
+
element.children.select { |e|
|
76
|
+
e.type == type
|
77
|
+
} + element.children.map { |e|
|
78
|
+
collector.call(e)
|
79
|
+
}.flatten
|
80
|
+
end
|
81
|
+
collector.call(root)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Rpub
|
2
|
+
module Commander
|
3
|
+
def invoke(args = [])
|
4
|
+
subcommand, *options = args
|
5
|
+
Commands::Base.matching(subcommand).new(options).invoke
|
6
|
+
rescue SubclassTracker::NoSuchSubclass
|
7
|
+
Commands::Main.new(args).invoke
|
8
|
+
rescue NoConfiguration
|
9
|
+
abort 'The current directory does not look like an rpub project.'
|
10
|
+
end
|
11
|
+
|
12
|
+
extend self
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Rpub
|
2
|
+
module Commands
|
3
|
+
class Base
|
4
|
+
extend SubclassTracker
|
5
|
+
|
6
|
+
attr_reader :options
|
7
|
+
|
8
|
+
def initialize(options = [], stdout = $stdout)
|
9
|
+
@options, @stdout = options, stdout
|
10
|
+
end
|
11
|
+
|
12
|
+
def invoke
|
13
|
+
parser.parse!(options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def help
|
17
|
+
puts parser
|
18
|
+
end
|
19
|
+
|
20
|
+
protected
|
21
|
+
|
22
|
+
def parser
|
23
|
+
OptionParser.new
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def puts(*args)
|
29
|
+
@stdout.puts(*args)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Rpub
|
2
|
+
module Commands
|
3
|
+
class Clean < Base
|
4
|
+
include CompilationHelpers
|
5
|
+
|
6
|
+
identifier 'clean'
|
7
|
+
|
8
|
+
def invoke
|
9
|
+
super
|
10
|
+
remove create_book.filename
|
11
|
+
remove 'preview.html'
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def parser
|
17
|
+
OptionParser.new do |opts|
|
18
|
+
opts.banner = <<-EOS
|
19
|
+
Usage: rpub clean [-d]'
|
20
|
+
|
21
|
+
Clean up all generated files, such as the standard generated .epub
|
22
|
+
file, package files and preview files.
|
23
|
+
|
24
|
+
Options:
|
25
|
+
EOS
|
26
|
+
|
27
|
+
opts.separator ''
|
28
|
+
|
29
|
+
opts.on '-d', '--dry-run', 'Dry-run: only list files to be removed' do
|
30
|
+
@dry_run = true
|
31
|
+
end
|
32
|
+
|
33
|
+
opts.separator ''
|
34
|
+
opts.separator 'Generic options:'
|
35
|
+
opts.separator ''
|
36
|
+
|
37
|
+
opts.on_tail '-h', '--help', 'Display this message' do
|
38
|
+
puts opts
|
39
|
+
exit
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def remove(filename)
|
45
|
+
if File.exist?(filename)
|
46
|
+
unless @dry_run
|
47
|
+
File.unlink(filename)
|
48
|
+
else
|
49
|
+
puts filename
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Rpub
|
2
|
+
module Commands
|
3
|
+
class Compile < Base
|
4
|
+
include CompilationHelpers
|
5
|
+
|
6
|
+
identifier 'compile'
|
7
|
+
|
8
|
+
def invoke
|
9
|
+
super
|
10
|
+
book = create_book
|
11
|
+
Compressor.open(book.filename) do |zip|
|
12
|
+
Epub.new(book, File.read(styles)).manifest_in(zip)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def parser
|
19
|
+
OptionParser.new do |opts|
|
20
|
+
opts.banner = <<-EOS
|
21
|
+
Usage: rpub compile [options]
|
22
|
+
|
23
|
+
Compile your Markdown-formatted input files to a valid .epub output
|
24
|
+
file using the options described in config.yml. This will use the
|
25
|
+
layout.html and styles.css files in your project directory if
|
26
|
+
present.
|
27
|
+
|
28
|
+
Options:
|
29
|
+
EOS
|
30
|
+
opts.separator ''
|
31
|
+
|
32
|
+
opts.on '-l', '--layout FILENAME', 'Specify an explicit layout file to use' do |filename|
|
33
|
+
@layout = filename
|
34
|
+
end
|
35
|
+
|
36
|
+
opts.on '-s', '--styles FILENAME', 'Specify an explicit stylesheet file to use' do |filename|
|
37
|
+
@styles = filename
|
38
|
+
end
|
39
|
+
|
40
|
+
opts.on '-c', '--config FILENAME', 'Specify an explicit configuration file to use' do |filename|
|
41
|
+
@config_file = filename
|
42
|
+
end
|
43
|
+
|
44
|
+
opts.separator ''
|
45
|
+
opts.separator 'Generic options:'
|
46
|
+
opts.separator ''
|
47
|
+
|
48
|
+
opts.on_tail '-h', '--help', 'Display this message' do
|
49
|
+
puts opts
|
50
|
+
exit
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Rpub
|
2
|
+
module Commands
|
3
|
+
class Help < Base
|
4
|
+
identifier 'help'
|
5
|
+
|
6
|
+
def invoke
|
7
|
+
if options.empty?
|
8
|
+
Main.new.invoke
|
9
|
+
else
|
10
|
+
Base.matching(options.shift).new.help
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def parser
|
17
|
+
OptionParser.new do |opts|
|
18
|
+
opts.banner = <<-EOS
|
19
|
+
Usage: rpub help subcommand
|
20
|
+
|
21
|
+
Describe the usage and options for rpub subcommands.
|
22
|
+
|
23
|
+
Options:
|
24
|
+
EOS
|
25
|
+
opts.separator ''
|
26
|
+
opts.separator 'Generic options:'
|
27
|
+
opts.separator ''
|
28
|
+
|
29
|
+
opts.on_tail '-h', '--help', 'Display this message' do
|
30
|
+
puts opts
|
31
|
+
exit
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Rpub
|
2
|
+
module Commands
|
3
|
+
class Main < Base
|
4
|
+
|
5
|
+
def invoke
|
6
|
+
options << '-h' if options.empty?
|
7
|
+
super
|
8
|
+
raise InvalidSubcommand, options[0] unless options.empty?
|
9
|
+
end
|
10
|
+
|
11
|
+
protected
|
12
|
+
|
13
|
+
def parser
|
14
|
+
OptionParser.new do |opts|
|
15
|
+
opts.banner = <<-EOS
|
16
|
+
Usage: rpub [subcommand] [options]
|
17
|
+
|
18
|
+
Compile multiple Markdown-formatted input files into a machine-readable epub
|
19
|
+
file for distribution as an ebook.
|
20
|
+
|
21
|
+
Available subcommands:
|
22
|
+
|
23
|
+
compile
|
24
|
+
preview
|
25
|
+
clean
|
26
|
+
package
|
27
|
+
help
|
28
|
+
EOS
|
29
|
+
|
30
|
+
opts.separator ''
|
31
|
+
opts.separator 'Generic options:'
|
32
|
+
opts.separator ''
|
33
|
+
|
34
|
+
opts.on_tail '-v', '--version', 'Display version information' do
|
35
|
+
puts "rpub #{Rpub::VERSION}"
|
36
|
+
end
|
37
|
+
|
38
|
+
opts.on_tail '-h', '--help', 'Display command reference' do
|
39
|
+
puts opts
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Rpub
|
2
|
+
module Commands
|
3
|
+
class Package < Base
|
4
|
+
include CompilationHelpers
|
5
|
+
|
6
|
+
identifier 'package'
|
7
|
+
|
8
|
+
def invoke
|
9
|
+
super
|
10
|
+
Compile.new(options).invoke
|
11
|
+
return unless config.has_key?('package_file')
|
12
|
+
Compressor.open(config.fetch('package_file')) do |zip|
|
13
|
+
zip.store_file create_book.filename, File.read(create_book.filename)
|
14
|
+
config.fetch('package') { [] }.each do |file|
|
15
|
+
zip.compress_file file, File.read(file)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def parser
|
23
|
+
OptionParser.new do |opts|
|
24
|
+
opts.banner = <<-EOS
|
25
|
+
Usage: rpub package
|
26
|
+
|
27
|
+
Compile your ebook to an ePub file and package it into an archive together with
|
28
|
+
optional other files for easy distibution. You might want to include a README
|
29
|
+
file, a license or other promotion material.
|
30
|
+
|
31
|
+
Options:
|
32
|
+
EOS
|
33
|
+
opts.separator ''
|
34
|
+
opts.separator 'Generic options:'
|
35
|
+
opts.separator ''
|
36
|
+
|
37
|
+
opts.on_tail '-h', '--help', 'Display this message' do
|
38
|
+
puts opts
|
39
|
+
exit
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Rpub
|
2
|
+
module Commands
|
3
|
+
class Preview < Base
|
4
|
+
include CompilationHelpers
|
5
|
+
|
6
|
+
identifier 'preview'
|
7
|
+
|
8
|
+
def initialize(*args)
|
9
|
+
super
|
10
|
+
@filename = 'preview.html'
|
11
|
+
end
|
12
|
+
|
13
|
+
def invoke
|
14
|
+
super
|
15
|
+
return unless markdown_files.any?
|
16
|
+
concatenation = markdown_files.join("\n")
|
17
|
+
File.open(@filename, 'w') do |f|
|
18
|
+
f.write Kramdown::Document.new(concatenation, KRAMDOWN_OPTIONS.merge(:template => layout)).to_html
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def parser
|
25
|
+
OptionParser.new do |opts|
|
26
|
+
opts.banner = <<-EOS
|
27
|
+
Usage: rpub preview [options]
|
28
|
+
|
29
|
+
Generate a single-page HTML file for easy previewing of your content with the
|
30
|
+
layout and styles used when generating .epub files. By default, the output
|
31
|
+
file will be named "preview.html".
|
32
|
+
|
33
|
+
Options:
|
34
|
+
EOS
|
35
|
+
opts.separator ''
|
36
|
+
opts.on '-l', '--layout FILENAME', 'Specify an explicit layout file to use' do |filename|
|
37
|
+
@layout = filename
|
38
|
+
end
|
39
|
+
|
40
|
+
opts.on '-o', '--output FILENAME', 'Specify an explicit output file' do |filename|
|
41
|
+
@filename = filename
|
42
|
+
end
|
43
|
+
|
44
|
+
opts.separator ''
|
45
|
+
opts.separator 'Generic options:'
|
46
|
+
opts.separator ''
|
47
|
+
|
48
|
+
opts.on_tail '-h', '--help', 'Display this message' do
|
49
|
+
puts opts
|
50
|
+
exit
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Rpub
|
2
|
+
module CompilationHelpers
|
3
|
+
def create_book
|
4
|
+
book = Book.new(layout, config)
|
5
|
+
markdown_files.each(&book.method(:<<))
|
6
|
+
book
|
7
|
+
end
|
8
|
+
|
9
|
+
def markdown_files
|
10
|
+
@markdown_files ||= filter_exceptions(Dir['*.md']).sort.map(&File.method(:read))
|
11
|
+
end
|
12
|
+
|
13
|
+
def layout
|
14
|
+
@layout ||= own_or_support_file('layout.html')
|
15
|
+
end
|
16
|
+
|
17
|
+
def styles
|
18
|
+
@styles ||= own_or_support_file('styles.css')
|
19
|
+
end
|
20
|
+
|
21
|
+
def config
|
22
|
+
@config_file ||= begin
|
23
|
+
raise NoConfiguration unless File.exist?('config.yml')
|
24
|
+
YAML.load_file('config.yml') || {}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def filter_exceptions(filenames)
|
31
|
+
return filenames unless config.has_key?('ignore')
|
32
|
+
filenames.reject(&config['ignore'].method(:include?))
|
33
|
+
end
|
34
|
+
|
35
|
+
def own_or_support_file(filename)
|
36
|
+
if File.exists?(filename)
|
37
|
+
filename
|
38
|
+
else
|
39
|
+
Rpub.support_file(filename)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|