burr 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/.ruby-version +1 -0
  4. data/Gemfile +3 -0
  5. data/LICENSE.md +30 -0
  6. data/README.md +180 -0
  7. data/Rakefile +118 -0
  8. data/bin/burr +9 -0
  9. data/burr.gemspec +36 -0
  10. data/generators/Gemfile.txt +3 -0
  11. data/generators/config.yml +55 -0
  12. data/generators/contents/chapter1.md +7 -0
  13. data/generators/contents/chapter2.md +7 -0
  14. data/generators/stylesheets/pdf.css +569 -0
  15. data/generators/stylesheets/site.css +1 -0
  16. data/lib/burr.rb +56 -0
  17. data/lib/burr/book.rb +289 -0
  18. data/lib/burr/cli.rb +64 -0
  19. data/lib/burr/converter.rb +19 -0
  20. data/lib/burr/core_ext/blank.rb +107 -0
  21. data/lib/burr/dependency.rb +28 -0
  22. data/lib/burr/eeepub_ext/maker.rb +131 -0
  23. data/lib/burr/exporter.rb +137 -0
  24. data/lib/burr/exporters/epub.rb +163 -0
  25. data/lib/burr/exporters/pdf.rb +95 -0
  26. data/lib/burr/exporters/site.rb +101 -0
  27. data/lib/burr/generator.rb +41 -0
  28. data/lib/burr/kramdown_ext/converter.rb +145 -0
  29. data/lib/burr/kramdown_ext/options.rb +38 -0
  30. data/lib/burr/kramdown_ext/parser.rb +65 -0
  31. data/lib/burr/liquid_ext/block.rb +58 -0
  32. data/lib/burr/liquid_ext/extends.rb +114 -0
  33. data/lib/burr/plugin.rb +70 -0
  34. data/lib/burr/plugins/aside.rb +44 -0
  35. data/lib/burr/plugins/codeblock.rb +42 -0
  36. data/lib/burr/plugins/figure.rb +62 -0
  37. data/lib/burr/plugins/link.rb +47 -0
  38. data/lib/burr/plugins/parser_plugin.rb +18 -0
  39. data/lib/burr/plugins/table.rb +42 -0
  40. data/lib/burr/plugins/toc.rb +105 -0
  41. data/lib/burr/ruby_version_check.rb +4 -0
  42. data/lib/burr/server.rb +27 -0
  43. data/lib/burr/ui.rb +46 -0
  44. data/lib/burr/version.rb +8 -0
  45. data/resources/locales/labels/en.yml +45 -0
  46. data/resources/locales/labels/zh_CN.yml +46 -0
  47. data/resources/locales/titles/en.yml +21 -0
  48. data/resources/locales/titles/zh_CN.yml +21 -0
  49. data/resources/templates/epub/_layout.liquid +17 -0
  50. data/resources/templates/epub/acknowledgement.liquid +10 -0
  51. data/resources/templates/epub/afterword.liquid +10 -0
  52. data/resources/templates/epub/appendix.liquid +10 -0
  53. data/resources/templates/epub/author.liquid +10 -0
  54. data/resources/templates/epub/chapter.liquid +10 -0
  55. data/resources/templates/epub/conclusion.liquid +10 -0
  56. data/resources/templates/epub/cover.liquid +9 -0
  57. data/resources/templates/epub/dedication.liquid +10 -0
  58. data/resources/templates/epub/edition.liquid +1 -0
  59. data/resources/templates/epub/epilogue.liquid +1 -0
  60. data/resources/templates/epub/foreword.liquid +10 -0
  61. data/resources/templates/epub/glossary.liquid +1 -0
  62. data/resources/templates/epub/introduction.liquid +1 -0
  63. data/resources/templates/epub/license.liquid +1 -0
  64. data/resources/templates/epub/lof.liquid +24 -0
  65. data/resources/templates/epub/lot.liquid +24 -0
  66. data/resources/templates/epub/part.liquid +4 -0
  67. data/resources/templates/epub/preface.liquid +10 -0
  68. data/resources/templates/epub/prologue.liquid +1 -0
  69. data/resources/templates/epub/table.liquid +7 -0
  70. data/resources/templates/epub/title.liquid +3 -0
  71. data/resources/templates/epub/toc.liquid +10 -0
  72. data/resources/templates/pdf/_item.liquid +6 -0
  73. data/resources/templates/pdf/acknowledgement.liquid +1 -0
  74. data/resources/templates/pdf/afterword.liquid +1 -0
  75. data/resources/templates/pdf/appendix.liquid +4 -0
  76. data/resources/templates/pdf/author.liquid +1 -0
  77. data/resources/templates/pdf/blank.liquid +3 -0
  78. data/resources/templates/pdf/book.liquid +42 -0
  79. data/resources/templates/pdf/chapter.liquid +4 -0
  80. data/resources/templates/pdf/code.liquid +3 -0
  81. data/resources/templates/pdf/conclusion.liquid +1 -0
  82. data/resources/templates/pdf/cover.liquid +6 -0
  83. data/resources/templates/pdf/dedication.liquid +3 -0
  84. data/resources/templates/pdf/edition.liquid +1 -0
  85. data/resources/templates/pdf/epilogue.liquid +1 -0
  86. data/resources/templates/pdf/foreword.liquid +1 -0
  87. data/resources/templates/pdf/glossary.liquid +1 -0
  88. data/resources/templates/pdf/introduction.liquid +1 -0
  89. data/resources/templates/pdf/license.liquid +1 -0
  90. data/resources/templates/pdf/lof.liquid +24 -0
  91. data/resources/templates/pdf/lot.liquid +24 -0
  92. data/resources/templates/pdf/part.liquid +4 -0
  93. data/resources/templates/pdf/preface.liquid +1 -0
  94. data/resources/templates/pdf/prologue.liquid +1 -0
  95. data/resources/templates/pdf/table.liquid +7 -0
  96. data/resources/templates/pdf/title.liquid +3 -0
  97. data/resources/templates/pdf/toc.liquid +4 -0
  98. data/resources/templates/site/_layout.liquid +27 -0
  99. data/resources/templates/site/author.liquid +13 -0
  100. data/resources/templates/site/chapter.liquid +13 -0
  101. data/resources/templates/site/foreword.liquid +13 -0
  102. data/resources/templates/site/preface.liquid +13 -0
  103. metadata +232 -0
@@ -0,0 +1,28 @@
1
+ module Burr
2
+ module Dependency
3
+
4
+ # Checks if PrinceXML installed.
5
+ #
6
+ # Returns true if installed, otherwise false.
7
+ def self.prince_installed?
8
+ installed? 'prince'
9
+ end
10
+
11
+ def self.kindlegen_installed?
12
+ installed? 'kindlegen'
13
+ end
14
+
15
+ # Checks if Dependent libx installed.
16
+ #
17
+ # Returns true if installed, otherwise false.
18
+ def self.installed?(cmd)
19
+ return true if which(cmd)
20
+ false
21
+ end
22
+
23
+ # Finds the executable.
24
+ def self.which(cmd)
25
+ system "which #{cmd} > /dev/null 2>&1"
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,131 @@
1
+ # Custom the eeepub maker.
2
+
3
+ require 'tmpdir'
4
+ require 'fileutils'
5
+
6
+ module Burr
7
+ class EpubMaker
8
+ [
9
+ :title,
10
+ :creator,
11
+ :publisher,
12
+ :date,
13
+ :language,
14
+ :subject,
15
+ :description,
16
+ :rights,
17
+ :relation
18
+ ].each do |name|
19
+ class_eval <<-DELIM
20
+ def #{name}(value)
21
+ @#{name}s ||= []
22
+ @#{name}s << value
23
+ end
24
+ DELIM
25
+ end
26
+
27
+ [
28
+ :uid,
29
+ :files,
30
+ :nav,
31
+ :cover,
32
+ :ncx_file,
33
+ :opf_file,
34
+ :guide
35
+ ].each do |name|
36
+ define_method(name) do |arg|
37
+ instance_variable_set("@#{name}", arg)
38
+ end
39
+ end
40
+
41
+ def identifier(id, options)
42
+ @identifiers ||= []
43
+ @identifiers << {:value => id, :scheme => options[:scheme], :id => options[:id]}
44
+ end
45
+
46
+ # @param [Proc] block the block for initialize
47
+ def initialize(&block)
48
+ @files ||= []
49
+ @nav ||= []
50
+ @ncx_file ||= 'toc.ncx'
51
+ @opf_file ||= 'content.opf'
52
+
53
+ instance_eval(&block) if block_given?
54
+ end
55
+
56
+ # Save as ePub file
57
+ #
58
+ # @param [String] filename the ePub file name to save
59
+ def save(filename)
60
+ create_epub.save(filename)
61
+ end
62
+
63
+ # instead of saving to file, output the file contents.
64
+ # important for serving on-the-fly doc creation from
65
+ # web interface where we don't want to allow file system
66
+ # writes (Heroku, et al.)
67
+ def render
68
+ create_epub.render
69
+ end
70
+
71
+ private
72
+
73
+ def create_epub
74
+ @uid ||= 'BookId'
75
+ unique_identifier = @identifiers.select{ |i| i[:id] == @uid }.first
76
+ unless unique_identifier
77
+ unique_identifier = @identifiers.first
78
+ unique_identifier[:id] = @uid
79
+ end
80
+ dir = Dir.mktmpdir
81
+ @files.each do |file|
82
+ case file
83
+ when String
84
+ FileUtils.cp(file, dir)
85
+ when Hash
86
+ file_path, dir_path = *file.first
87
+ dest_dir = File.join(dir, dir_path)
88
+ FileUtils.mkdir_p(dest_dir)
89
+ FileUtils.cp(file_path, dest_dir)
90
+ end
91
+ end
92
+
93
+ ::EeePub::NCX.new(
94
+ :uid => @identifiers.select{ |i| i[:id] == @uid }.first,
95
+ :title => @titles[0],
96
+ :nav => @nav
97
+ ).save(File.join(dir, @ncx_file))
98
+
99
+ ::EeePub::OPF.new(
100
+ :title => @titles,
101
+ :unique_identifier => @uid,
102
+ :identifier => @identifiers,
103
+ :creator => @creators,
104
+ :publisher => @publishers,
105
+ :date => @dates,
106
+ :language => @languages,
107
+ :subject => @subjects,
108
+ :description => @descriptions,
109
+ :rights => @rightss,
110
+ :cover => @cover,
111
+ :relation => @relations,
112
+ :manifest => @files.map{|file|
113
+ case file
114
+ when String
115
+ File.basename(file)
116
+ when Hash
117
+ file_path, dir_path = *file.first
118
+ File.join(dir_path, File.basename(file_path))
119
+ end
120
+ },
121
+ :ncx => @ncx_file,
122
+ :guide => @guide
123
+ ).save(File.join(dir, @opf_file))
124
+
125
+ ::EeePub::OCF.new(
126
+ :dir => dir,
127
+ :container => @opf_file
128
+ )
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,137 @@
1
+ module Burr
2
+ class Exporter
3
+
4
+ FRONTMATTER = %w(acknowledgement author cover dedication edition foreword
5
+ introduction license preface prologue title toc)
6
+ BODYMATTER = %w(appendix blank chapter conclusion part)
7
+ BACKMATTER = %w(afterword epilogue glossary lof lot)
8
+
9
+ attr_accessor :book, :config
10
+ attr_accessor :frontmatter, :bodymatter, :backmatter
11
+
12
+ def initialize(book)
13
+ @book = book
14
+ @config = book.config
15
+
16
+ @frontmatter = []
17
+ @bodymatter = []
18
+ @backmatter = []
19
+
20
+ prepare_output_dir
21
+ end
22
+
23
+ # Run all hooks of a type.
24
+ #
25
+ # type - The plugin name in Symbol, valid options are: :before_parse, :after_parse
26
+ # :before_decorate, :after_decorate.
27
+ #
28
+ # Returns nothing.
29
+ def run_plugins_of_type(type)
30
+ type = type.to_sym
31
+
32
+ Burr::Plugin.subclasses.each do |k|
33
+ k_obj = k.new(self.book)
34
+ Burr::Plugin::VALIDS.each do |h|
35
+ k_obj.send(type) if k_obj.respond_to?(h) && h == type
36
+ end
37
+ end
38
+
39
+ nil
40
+ end
41
+
42
+ # Run exporter.
43
+ #
44
+ # The details should implement in subclass.
45
+ def run
46
+ self.load_contents
47
+ self.parse_contents
48
+ self.decorate_contents
49
+ self.assemble_book
50
+ end
51
+
52
+ # Load book contents
53
+ def load_contents
54
+ special_elements = %w(cover toc blank)
55
+
56
+ self.config['contents'].each do |content_config|
57
+ item = initialize_item(content_config)
58
+
59
+ # if the element defines its own content file (usually: `chapter`, `appendix`)
60
+ if !item['file'].blank?
61
+ content_file = File.join(self.book.contents_dir, item['file'])
62
+
63
+ # check that content file exists and is readable
64
+ if !File.readable? content_file
65
+ raise <<-MESSAGE % content_config['file'], item['element'], "outputs/#{content_config['file']}"
66
+ The '%s' content associated with '%s' element doesn't exist\n
67
+ or is not readable.\n\n
68
+ Check that '%s'\n
69
+ file exists and check its permissions.
70
+ MESSAGE
71
+ end
72
+
73
+ item['original'] = File.read(content_file)
74
+ elsif item['file'].blank? && special_elements.include?(item['element'])
75
+ item['skip'] = true
76
+ else
77
+ # look for a default content defined by burr for this element
78
+ # e.g. `cover.md`, `license.md`, `title.md`
79
+ default_content_file = File.join(self.book.contents_dir, "#{item['element']}.md")
80
+ if File.exist?(default_content_file)
81
+ item['original'] = File.read(default_content_file)
82
+ else
83
+ self.book.ui.error("Missing file for #{item['element']}")
84
+ exit 1
85
+ end
86
+ end
87
+
88
+ self.frontmatter << item if item['matter'] == 'frontmatter'
89
+ self.bodymatter << item if item['matter'] == 'bodymatter'
90
+ self.backmatter << item if item['matter'] == 'backmatter'
91
+ self.book.items << item
92
+ end
93
+ end
94
+
95
+ private
96
+
97
+ def initialize_item(configs)
98
+ item = {
99
+ 'element' => '', # the type of this content (`chapter', `appendix', `toc', `license', ...)
100
+ 'number' => '', # the number/letter of the content (useful for `chapter', `part' and `appendix')
101
+ 'c_title' => '', # the title of the content defined in `config.yml' (usually only `part' defines it)
102
+ 'title' => '', # the `title' of this file, the first h1 in `content'
103
+ 'original' => '', # original content as written by book author
104
+ 'content' => '', # transformed content of the element (HTML usually)
105
+ 'file' => '', # the name of this item contents file (it's a relative path from book's `contents/')
106
+ 'toc' => [], # the table of contents of this element
107
+ 'skip' => false, # some elements, like `toc', do not need to covert, so just skip
108
+ }
109
+
110
+ item.merge!(configs)
111
+
112
+ # set the matter
113
+ if FRONTMATTER.include?(item['element'])
114
+ item['matter'] = 'frontmatter'
115
+ elsif BODYMATTER.include?(item['element'])
116
+ item['matter'] = 'bodymatter'
117
+ elsif BACKMATTER.include?(item['element'])
118
+ item['matter'] = 'backmatter'
119
+ else
120
+ self.book.ui.error("Element #{ item['element'] } not defined!")
121
+ exit 1
122
+ end
123
+
124
+ item
125
+ end
126
+
127
+ # If the outpus directory for current format not exists, create it!
128
+ #
129
+ def prepare_output_dir
130
+ dir = File.join(self.book.outputs_dir, self.book.format)
131
+ if !File.exist?(dir)
132
+ FileUtils.mkdir_p dir
133
+ end
134
+ end
135
+
136
+ end
137
+ end
@@ -0,0 +1,163 @@
1
+ module Burr
2
+ class Epub < Exporter
3
+
4
+ # Convert original contents into HTML
5
+ #
6
+ def parse_contents
7
+ parsed_items = []
8
+ self.book.items.each do |item|
9
+ self.book.current_item = item
10
+
11
+ # 'blank' element not include in epub
12
+ next if item['element'] == 'blank'
13
+
14
+ self.run_plugins_of_type(:before_parse)
15
+
16
+ unless item['skip']
17
+ item['content'] = Burr::Converter.new(self.book).convert(item['original'])
18
+ end
19
+
20
+ self.run_plugins_of_type(:after_parse)
21
+
22
+ parsed_items << self.book.current_item
23
+ end
24
+ self.book.items = parsed_items
25
+ end
26
+
27
+ # Decorate the contents with template
28
+ #
29
+ def decorate_contents
30
+ decorated_items = []
31
+ self.book.items.each do |item|
32
+ self.book.current_item = item
33
+ self.run_plugins_of_type(:before_decorate)
34
+ self.run_plugins_of_type(:after_decorate)
35
+ decorated_items << self.book.current_item
36
+ end
37
+ self.book.items = decorated_items
38
+ end
39
+
40
+ def assemble_book
41
+ # 1. create html files
42
+ special_elements = %w(blank)
43
+ base = File.join(self.book.outputs_dir, 'epub')
44
+ included_files = []
45
+ tmp_files = []
46
+
47
+ self.book.items.each do |item|
48
+ next if special_elements.include?(item['element'])
49
+
50
+ basename = if item['file'].blank?
51
+ item['element']
52
+ else
53
+ item['file'].split('.')[0..-2].join('.')
54
+ end
55
+ html_path = File.join(base, "#{ basename }.html")
56
+ included_files << html_path
57
+ tmp_files << html_path
58
+
59
+ File.open(html_path, 'w') do |f|
60
+ f.puts self.book.render(self.book.template_for(item['element']), { 'item' => item, 'toc' => html_toc })
61
+ end
62
+ end
63
+
64
+ # 2. add other files
65
+ included_files << File.join(base, 'style.css')
66
+ Dir.glob(File.join(self.book.outputs_dir, 'site', 'figures', '*.*')) do |figure|
67
+ included_files << { figure => 'figures' }
68
+ end
69
+ included_files << File.join(base, 'cover.jpg')
70
+
71
+ # 3. build epub file
72
+ config = self.book.config
73
+ nav = ncx_toc
74
+ guide = build_guide
75
+
76
+ epub = Burr::EpubMaker.new do
77
+ title config['title']
78
+ creator config['translator'].blank? ? config['author'] : config['translator']
79
+ publisher config['publisher']
80
+ date config['pubdate']
81
+ identifier config['identifier'], :scheme => config['id_scheme'], :id => config['slug']
82
+ uid config['slug']
83
+ language config['language']
84
+ cover 'cover.jpg'
85
+
86
+ files included_files
87
+ nav nav
88
+ guide guide
89
+ end
90
+
91
+ epub.save(File.join(base, "#{self.book.config['slug']}-#{Time.new.strftime('%Y%m%d')}.epub"))
92
+
93
+ # 4. remove useless files
94
+ tmp_files.each do |file|
95
+ FileUtils.remove_entry(file)
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ def ncx_toc
102
+ nav = []
103
+ self.book.items.each do |item|
104
+ special_elements = %w(cover toc blank)
105
+ next if special_elements.include?(item['element'])
106
+ level_1 = item['toc'].first
107
+ next unless level_1['level'] == 1
108
+ basename = if item['file'].blank?
109
+ item['element']
110
+ else
111
+ item['file'].split('.')[0..-2].join('.')
112
+ end
113
+ html_path = "#{ basename }.html"
114
+ nav_label = if level_1['label'].blank?
115
+ "#{level_1['title']}"
116
+ else
117
+ "#{level_1['label']} #{level_1['title']}"
118
+ end
119
+ nav << { :label => nav_label, :content => html_path }
120
+ end
121
+
122
+ nav
123
+ end
124
+
125
+ def html_toc
126
+ html = '<ol class="toc-list">'
127
+ self.book.items.each do |item|
128
+ # editions define the *tocable* items
129
+ if self.book.config['formats']['epub']['toc']['elements'].include?(item['element'])
130
+ # item has several elements in its toc
131
+ if item['toc'].size > 0
132
+ item['toc'].each do |entry|
133
+ if entry['level'] <= self.book.config['formats'][self.book.format]['toc']['deep']
134
+ anchor = "#{item['element']}#{entry['id'].split('-')[1]}.html"
135
+ html << <<-LI1
136
+ <li class="#{ item['matter'] } #{ item['element'] } level-#{ entry['level'] }">
137
+ <a href="#{ anchor }##{ entry['id']}">#{ entry['label'] } #{ entry['title'] }</a>
138
+ </li>
139
+ LI1
140
+ end
141
+ end
142
+ end
143
+
144
+ # empty or special item (anything different from 'chapter' and 'appendix')
145
+ elsif !%w(cover blank toc).include?(item['element'])
146
+ html << <<-LI2
147
+ <li class="#{ item['matter'] } #{ item['element'] } level-1">
148
+ <a href="#{ item['id'] }.html">#{ item['title'] }</a>
149
+ </li>
150
+ LI2
151
+ end
152
+ end
153
+ html << '</ol>'
154
+ end
155
+
156
+ def build_guide
157
+ o = []
158
+ o << { :type => 'toc', :href => "toc.html", :title => 'Table of Contents' }
159
+ o << { :type => 'cover', :href => "cover.html", :title => 'Cover' }
160
+ end
161
+
162
+ end
163
+ end