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,95 @@
1
+ module Burr
2
+ class PDF < 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
+ self.run_plugins_of_type(:before_parse)
12
+
13
+ unless item['skip']
14
+ item['content'] = Burr::Converter.new(self.book).convert(item['original'])
15
+ end
16
+
17
+ self.run_plugins_of_type(:after_parse)
18
+
19
+ parsed_items << self.book.current_item
20
+ end
21
+ self.book.items = parsed_items
22
+ end
23
+
24
+ # Decorate the contents with template
25
+ #
26
+ def decorate_contents
27
+
28
+ decorated_items = []
29
+
30
+ self.book.items.each do |item|
31
+ self.book.current_item = item
32
+
33
+ self.run_plugins_of_type(:before_decorate)
34
+
35
+ item['content'] = self.book.render(self.book.template_for(item['element']), { 'item' => item, 'toc' => toc_html })
36
+
37
+ self.run_plugins_of_type(:after_decorate)
38
+
39
+ decorated_items << self.book.current_item
40
+ end
41
+
42
+ self.book.items = decorated_items
43
+ end
44
+
45
+ def assemble_book
46
+ base = File.join(self.book.outputs_dir, 'pdf')
47
+ html_path = File.join(base, "#{self.book.slug}.html")
48
+ pdf_path = File.join(base, "#{self.book.slug}-#{Time.new.strftime('%Y%m%d')}.pdf")
49
+
50
+ html = self.book.render(self.book.template_for('book'), {
51
+ 'frontmatter' => self.frontmatter,
52
+ 'bodymatter' => self.bodymatter,
53
+ 'backmatter' => self.backmatter
54
+ })
55
+ File.open(html_path, 'w') { |f| f.puts html }
56
+
57
+ system "prince #{html_path} -o #{pdf_path}"
58
+
59
+ File.unlink html_path
60
+ end
61
+
62
+ private
63
+
64
+ def toc_html
65
+ html = '<ol class="toc-list">'
66
+ self.book.items.each do |item|
67
+ # editions define the *tocable* items
68
+ if self.book.config['formats']['pdf']['toc']['elements'].include?(item['element'])
69
+ # item has several elements in its toc
70
+ if item['toc'].size > 0
71
+ item['toc'].each do |entry|
72
+ if entry['level'] <= self.book.config['formats'][self.book.format]['toc']['deep']
73
+ html << <<-LI1
74
+ <li class="#{ item['matter'] } #{ item['element'] } level-#{ entry['level'] }">
75
+ <a href="\##{ entry['id'] }">#{ entry['label'] } #{ entry['title'] }</a>
76
+ </li>
77
+ LI1
78
+ end
79
+ end
80
+ end
81
+
82
+ # empty or special item (anything different from 'chapter' and 'appendix')
83
+ elsif !%w(cover blank toc).include?(item['element'])
84
+ html << <<-LI2
85
+ <li class="#{ item['matter'] } #{ item['element'] } level-1">
86
+ <a href="\##{ item['id'] }">#{ item['label'] } #{ item['title'] }</a>
87
+ </li>
88
+ LI2
89
+ end
90
+ end
91
+ html << '</ol>'
92
+ end
93
+
94
+ end
95
+ end
@@ -0,0 +1,101 @@
1
+ module Burr
2
+ class Site < 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
+ self.run_plugins_of_type(:before_parse)
12
+
13
+ unless item['skip']
14
+ item['content'] = Burr::Converter.new(self.book).convert(item['original'])
15
+ end
16
+
17
+ self.run_plugins_of_type(:after_parse)
18
+
19
+ parsed_items << self.book.current_item
20
+ end
21
+ self.book.items = parsed_items
22
+ end
23
+
24
+ # Decorate the contents with template
25
+ #
26
+ def decorate_contents
27
+ decorated_items = []
28
+ self.book.items.each do |item|
29
+ self.book.current_item = item
30
+ self.run_plugins_of_type(:before_decorate)
31
+ self.run_plugins_of_type(:after_decorate)
32
+ decorated_items << self.book.current_item
33
+ end
34
+ self.book.items = decorated_items
35
+ end
36
+
37
+ def assemble_book
38
+ [nil, *flatten_items(self.book.items), nil].each_cons(3) do |pre, item, nxt|
39
+ item['toc'] = item_toc_html(item['toc'])
40
+
41
+ unless pre.nil?
42
+ item['pre'] = ''
43
+ item['pre'] << "#{pre['label']}" unless pre['label'].blank?
44
+ item['pre'] << "#{pre['title']}"
45
+ item['pre_url'] = get_html_path_of(pre['element'], pre['file'])
46
+ end
47
+
48
+ unless nxt.nil?
49
+ item['nxt'] = ''
50
+ item['nxt'] << "#{nxt['label']}" unless nxt['label'].blank?
51
+ item['nxt'] << "#{nxt['title']}"
52
+ item['nxt_url'] = get_html_path_of(nxt['element'], nxt['file'])
53
+ end
54
+
55
+ File.open(get_html_path_of(item['element'], item['file'], false), 'w') do |f|
56
+ f.puts self.book.render(self.book.template_for(item['element']), { 'item' => item })
57
+ end
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ # Deletes item if item['element'] includes in %w(cover toc blank).
64
+ #
65
+ def flatten_items(items)
66
+ items.delete_if { |item| %w(cover toc blank).include?(item['element']) }
67
+ end
68
+
69
+ # Gets toc HTML for a single item.
70
+ #
71
+ def item_toc_html(toc)
72
+ # do not need the first element(level 1) in output
73
+ toc.shift
74
+
75
+ html = '<ol class="toc">'
76
+ toc.each do |entry|
77
+ next if entry['level'] - 1 > self.book.config['formats']['site']['toc']['deep']
78
+ html << <<-LI
79
+ <li class="level-#{ entry['level'] }">
80
+ <a href="\##{ entry['id'] }">#{ entry['label'] } #{ entry['title'] }</a>
81
+ </li>
82
+ LI
83
+ end
84
+ html << '</ol>'
85
+ end
86
+
87
+ def get_html_path_of(element, path, relative = true)
88
+ base = File.join(self.book.outputs_dir, 'site')
89
+ basename = if path.blank?
90
+ element
91
+ else
92
+ path.split('.')[0..-2].join('.')
93
+ end
94
+
95
+ return "/#{ basename }.html" if relative
96
+
97
+ File.join(base, "#{ basename }.html")
98
+ end
99
+
100
+ end
101
+ end
@@ -0,0 +1,41 @@
1
+ module Burr
2
+ class Generator < Thor::Group
3
+
4
+ include Thor::Actions
5
+
6
+ def self.source_root
7
+ File.dirname(__FILE__) + '/../../generators'
8
+ end
9
+
10
+ def copy_gemfile
11
+ copy_file 'Gemfile.txt', 'Gemfile'
12
+ end
13
+
14
+ def copy_config_file
15
+ copy_file 'config.yml'
16
+ end
17
+
18
+ def copy_contents
19
+ directory 'contents'
20
+ end
21
+
22
+ def outputs_dir
23
+ empty_directory 'outputs/pdf'
24
+ empty_directory 'outputs/site'
25
+ empty_directory 'outputs/epub'
26
+ empty_directory 'outputs/mobi'
27
+
28
+ empty_directory 'outputs/site/figures'
29
+ end
30
+
31
+ def copy_stylesheets
32
+ copy_file 'stylesheets/pdf.css', 'outputs/pdf/style.css'
33
+ copy_file 'stylesheets/site.css', 'outputs/site/style.css'
34
+ end
35
+
36
+ def caches_dir
37
+ empty_directory 'caches/code'
38
+ end
39
+
40
+ end
41
+ end
@@ -0,0 +1,145 @@
1
+ require 'burr/kramdown_ext/parser'
2
+ require 'pygments'
3
+
4
+ module Kramdown
5
+ module Converter
6
+ class Bshtml < Html
7
+
8
+ attr_accessor :book
9
+
10
+ def initialize(root, options)
11
+ super
12
+ @book = options[:register]
13
+ end
14
+
15
+ # Converts paragraph contents image with caption and normal paragraph.
16
+ #
17
+ def convert_p(el, indent)
18
+ if el.children.size == 1 && el.children.first.type == :img && !el.children.first.attr['caption'].nil?
19
+ convert_image_with_caption(el, indent)
20
+ else
21
+ super
22
+ end
23
+ end
24
+
25
+ # Converts the codeblock to HTML, using pygments to highlight.
26
+ #
27
+ def convert_codeblock(el, indent)
28
+ attr = el.attr.dup
29
+ lang = extract_code_language!(attr)
30
+ lang = 'text' if lang.blank?
31
+
32
+ pyg_opts = {
33
+ :encoding => 'utf-8',
34
+ :cssclass => "highlight type-#{lang}"
35
+ }
36
+ caption = attr['caption']
37
+ file = attr['file']
38
+ code = ::Pygments.highlight(el.value, :lexer => lang, :options => pyg_opts).chomp << "\n"
39
+ output = '<div class="codeblock'
40
+ output << ' has-caption' if caption
41
+ output << '">'
42
+ if caption
43
+ caption_el = ::Kramdown::Parser::Bsmarkdown.parse(caption).first
44
+ caption_html = inner(caption_el.children.first, 0)
45
+ output << "<p class=\"caption\">#{caption_html}</p>"
46
+ end
47
+ output << "<p class=\"file\"><code>#{file}</code></p>" if file
48
+ output << "#{code}</div>"
49
+ end
50
+
51
+ # Converts headers
52
+ #
53
+ def convert_header(el, indent)
54
+ attr = el.attr.dup
55
+ item = self.book.current_item
56
+ if @options[:auto_ids] && !attr['id']
57
+ attr['id'] = generate_id(el.options[:raw_text])
58
+ end
59
+ #@toc << [el.options[:level], attr['id'], el.children] if attr['id'] && in_toc?(el)
60
+ unless attr['class'] == 'skip-toc'
61
+ item['toc'] << {
62
+ 'level' => el.options[:level],
63
+ 'title' => el.options[:raw_text]
64
+ }
65
+ end
66
+
67
+ level = output_header_level(el.options[:level])
68
+ format_as_block_html("h#{level}", attr, inner(el, indent), indent)
69
+ end
70
+
71
+ def convert_footnote(el, indent)
72
+ if self.book.format == 'pdf'
73
+ inline = format_as_span_html('span', { 'class'=> 'footnote', 'id' => "fn-#{ el.options[:name] }"}, inner(el.value, 0))
74
+ inline.sub!(/\s*<p>/, '').sub!(/<\/p>\n/, '')
75
+ else
76
+ number = @footnote_counter
77
+ @footnote_counter += 1
78
+ @footnotes << [el.options[:name], el.value]
79
+ "<sup class=\"footnote\" id=\"fnref-#{el.options[:name]}\"><a href=\"#fn-#{el.options[:name]}\" rel=\"footnote\">#{number}</a></sup>"
80
+ end
81
+ end
82
+
83
+ ::Kramdown::Parser::Bsmarkdown::BOXES.each do |box|
84
+ define_method("convert_#{box}_box") { |el, indent| <<-EOF }
85
+ #{' '*indent}<div class="#{box} box">\n#{inner(el, indent)}#{' '*indent}</div>\n
86
+ EOF
87
+ end
88
+
89
+ alias :orin_convert_table :convert_table
90
+
91
+ alias :convert_thead :orin_convert_table
92
+ alias :convert_tbody :orin_convert_table
93
+ alias :convert_tfoot :orin_convert_table
94
+ alias :convert_tr :orin_convert_table
95
+
96
+ def convert_table(el, indent)
97
+ caption = el.attr.delete('caption')
98
+ output = '<div class="table'
99
+ if caption
100
+ caption_el = ::Kramdown::Parser::Bsmarkdown.parse(caption).first
101
+ caption_html = inner(caption_el.children.first, 0)
102
+ output << " has-caption\"><p class=\"caption\">#{caption_html}</p>"
103
+ else
104
+ output << '">'
105
+ end
106
+ output << format_as_indented_block_html(el.type, el.attr, inner(el, indent), indent)
107
+ output << '</div>'
108
+ end
109
+
110
+ # Return a HTML ordered list with the footnote content for the used footnotes.
111
+ def footnote_content
112
+ ol = Element.new(:ol)
113
+ ol.attr['start'] = @footnote_start if @footnote_start != 1
114
+ @footnotes.each do |name, data|
115
+ li = Element.new(:li, nil, {'id' => "fn-#{name}"})
116
+ li.children = Marshal.load(Marshal.dump(data.children))
117
+ ol.children << li
118
+
119
+ ref = Element.new(:raw, "<a href=\"#fnref-#{name}\" rel=\"reference\">&#8617;</a>")
120
+ if li.children.last.type == :p
121
+ para = li.children.last
122
+ else
123
+ li.children << (para = Element.new(:p))
124
+ end
125
+ para.children << ref
126
+ end
127
+ (ol.children.empty? ? '' : format_as_indented_block_html('div', {:class => "footnotes"}, convert(ol, 2), 0))
128
+ end
129
+
130
+ private
131
+
132
+ def convert_image_with_caption(el, indent)
133
+ img_el = el.children.first
134
+ caption = img_el.attr.delete('caption')
135
+ img_html = convert_img(img_el, indent)
136
+
137
+ caption_el = ::Kramdown::Parser::Bsmarkdown.parse(caption).first
138
+ caption_html = inner(caption_el.children.first, 0)
139
+
140
+ "<div class=\"figure\">#{img_html}<p class=\"caption\">#{caption_html}</p></div>"
141
+ end
142
+
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,38 @@
1
+ module Kramdown
2
+ module Options
3
+
4
+ # Parse the given value +data+ as if it was a value for the option +name+ and return the parsed
5
+ # value with the correct type.
6
+ #
7
+ # If +data+ already has the correct type, it is just returned. Otherwise it is converted to a
8
+ # String and then to the correct type.
9
+ def self.parse(name, data)
10
+ raise ArgumentError, "No option named #{name} defined" if !@options.has_key?(name)
11
+ if !(@options[name].type === data)
12
+ data = data.to_s
13
+ data = if @options[name].type == String
14
+ data
15
+ elsif @options[name].type == Integer
16
+ Integer(data) rescue raise Kramdown::Error, "Invalid integer value for option '#{name}': '#{data}'"
17
+ elsif @options[name].type == Float
18
+ Float(data) rescue raise Kramdown::Error, "Invalid float value for option '#{name}': '#{data}'"
19
+ elsif @options[name].type == Symbol
20
+ data.strip!
21
+ data = data[1..-1] if data[0] == ?:
22
+ (data.empty? || data == 'nil' ? nil : data.to_sym)
23
+ elsif @options[name].type == Boolean
24
+ data.downcase.strip != 'false' && !data.empty?
25
+ else
26
+ data
27
+ end
28
+ end
29
+ data = @options[name].validator[data] if @options[name].validator
30
+ data
31
+ end
32
+
33
+ define(:register, Object, nil, 'Pass in another object.') do |r|
34
+ r
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,65 @@
1
+ module Kramdown
2
+ module Parser
3
+ class Bsmarkdown < Kramdown
4
+
5
+ BOXES = %w(aside discussion error information question tip warning)
6
+
7
+ def initialize(source, options)
8
+ super
9
+ @block_parsers.unshift(:gfm_codeblock_fenced, *BOXES.map{ |b| :"#{b}_box" })
10
+ end
11
+
12
+ GFM_FENCED_CODEBLOCK_START = /^`{3,}/
13
+ GFM_FENCED_CODEBLOCK_MATCH = /^(`{3,})\s*?(\w+)?\s*?\n(.*?)^\1`*\s*?\n/m
14
+
15
+ # Parser the GitHub Flavored Markdown fenced code block.
16
+ #
17
+ # Examples
18
+ #
19
+ # ```ruby
20
+ # def hello
21
+ # puts 'Hello'
22
+ # end
23
+ # ```
24
+ #
25
+ def parse_gfm_codeblock_fenced
26
+ if @src.check(GFM_FENCED_CODEBLOCK_MATCH)
27
+ @src.pos += @src.matched_size
28
+ el = new_block_el(:codeblock, @src[3])
29
+ lang = @src[2].to_s.strip
30
+ el.attr['class'] = "language-#{lang}" unless lang.empty?
31
+ @tree.children << el
32
+ true
33
+ else
34
+ false
35
+ end
36
+ end
37
+ define_parser(:gfm_codeblock_fenced, GFM_FENCED_CODEBLOCK_START)
38
+
39
+ ASIDE_BOX_START = /^#{OPT_SPACE}A> ?/u
40
+ DISCUSSION_BOX_START = /^#{OPT_SPACE}D> ?/
41
+ ERROR_BOX_START = /^#{OPT_SPACE}E> ?/
42
+ INFORMATION_BOX_START = /^#{OPT_SPACE}I> ?/
43
+ QUESTION_BOX_START = /^#{OPT_SPACE}Q> ?/
44
+ TIP_BOX_START = /^#{OPT_SPACE}T> ?/
45
+ WARNING_BOX_START = /^#{OPT_SPACE}W> ?/
46
+
47
+ BOXES.each do |box|
48
+ define_method("parse_#{box}_box") do
49
+ result = @src.scan(PARAGRAPH_MATCH)
50
+ while !@src.match?(self.class::LAZY_END)
51
+ result << @src.scan(PARAGRAPH_MATCH)
52
+ end
53
+ result.gsub!(self.class.const_get("#{box.upcase}_BOX_START"), '')
54
+
55
+ el = new_block_el(:"#{box}_box")
56
+ @tree.children << el
57
+ parse_blocks(el, result)
58
+ true
59
+ end
60
+ define_parser(:"#{box}_box", self.const_get("#{box.upcase}_BOX_START"))
61
+ end
62
+
63
+ end
64
+ end
65
+ end