rpub 0.1.0

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