rpub 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.
Files changed (61) hide show
  1. data/.gitignore +8 -0
  2. data/.rspec +5 -0
  3. data/.travis.yml +5 -0
  4. data/.yardopts +6 -0
  5. data/Gemfile +2 -0
  6. data/Gemfile.lock +51 -0
  7. data/Guardfile +5 -0
  8. data/HISTORY.md +5 -0
  9. data/LICENSE +0 -0
  10. data/README.md +182 -0
  11. data/Rakefile +11 -0
  12. data/bin/rpub +6 -0
  13. data/example/advanced/README +1 -0
  14. data/example/advanced/config.yml +11 -0
  15. data/example/advanced/layout.html +0 -0
  16. data/example/advanced/styles.css +0 -0
  17. data/example/simple/01-introduction.md +3 -0
  18. data/example/simple/02-foo.md +4 -0
  19. data/example/simple/03-bar.md +4 -0
  20. data/example/simple/config.yml +8 -0
  21. data/lib/rpub.rb +53 -0
  22. data/lib/rpub/book.rb +70 -0
  23. data/lib/rpub/chapter.rb +84 -0
  24. data/lib/rpub/commander.rb +14 -0
  25. data/lib/rpub/commands/base.rb +33 -0
  26. data/lib/rpub/commands/clean.rb +55 -0
  27. data/lib/rpub/commands/compile.rb +56 -0
  28. data/lib/rpub/commands/help.rb +37 -0
  29. data/lib/rpub/commands/main.rb +45 -0
  30. data/lib/rpub/commands/package.rb +45 -0
  31. data/lib/rpub/commands/preview.rb +56 -0
  32. data/lib/rpub/compilation_helpers.rb +43 -0
  33. data/lib/rpub/compressor.rb +46 -0
  34. data/lib/rpub/epub.rb +37 -0
  35. data/lib/rpub/epub/container.rb +14 -0
  36. data/lib/rpub/epub/content.rb +80 -0
  37. data/lib/rpub/epub/cover.rb +27 -0
  38. data/lib/rpub/epub/html_toc.rb +25 -0
  39. data/lib/rpub/epub/toc.rb +35 -0
  40. data/lib/rpub/hash_delegation.rb +23 -0
  41. data/lib/rpub/subclass_tracker.rb +53 -0
  42. data/lib/rpub/version.rb +3 -0
  43. data/lib/rpub/xml_file.rb +14 -0
  44. data/rpub.gemspec +52 -0
  45. data/spec/fixtures/clean/config.yml +2 -0
  46. data/spec/fixtures/clean/example.epub +0 -0
  47. data/spec/fixtures/clean/preview.html +0 -0
  48. data/spec/fixtures/no_files/config.yml +0 -0
  49. data/spec/fixtures/preview/a.md +1 -0
  50. data/spec/fixtures/preview/b.md +1 -0
  51. data/spec/fixtures/preview/config.yml +2 -0
  52. data/spec/rpub/book_spec.rb +62 -0
  53. data/spec/rpub/chapter_spec.rb +61 -0
  54. data/spec/rpub/commands/clean_spec.rb +46 -0
  55. data/spec/rpub/commands/main_spec.rb +26 -0
  56. data/spec/rpub/commands/preview_spec.rb +42 -0
  57. data/spec/rpub_spec.rb +7 -0
  58. data/spec/spec_helper.rb +21 -0
  59. data/support/layout.html +22 -0
  60. data/support/styles.css +66 -0
  61. metadata +310 -0
@@ -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