jekyll-pandoc-multiple-formats-jekyll34 0.2.10 → 0.2.12

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,81 @@
1
+ # Copyright (c) 2012-2015 Nicolás Reynolds <fauno@endefensadelsl.org>
2
+ # 2012-2013 Mauricio Pasquier Juan <mpj@endefensadelsl.org>
3
+ # 2013 Brian Candler <b.candler@pobox.com>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining
6
+ # a copy of this software and associated documentation files (the
7
+ # "Software"), to deal in the Software without restriction, including
8
+ # without limitation the rights to use, copy, modify, merge, publish,
9
+ # distribute, sublicense, and/or sell copies of the Software, and to
10
+ # permit persons to whom the Software is furnished to do so, subject to
11
+ # the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be
14
+ # included in all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ # Namespacing and metaprogramming FTW
25
+ module JekyllPandocMultipleFormats
26
+ # Determines the correct module where it lives the converter
27
+ def self.namespace
28
+ Jekyll::VERSION >= '1.0.0' ? Jekyll::Converters : Jekyll
29
+ end
30
+
31
+ # Determines the correct class name. Jekyll has the converters class kinda
32
+ # hardcoded
33
+ def self.class_name
34
+ Jekyll::VERSION >= '1.0.0' ? 'Markdown' : 'MarkdownConverter'
35
+ end
36
+
37
+ def self.build
38
+ namespace::const_get(class_name).send :include, ConverterMethods
39
+ end
40
+
41
+ # When included in the correspondant markdown class this module redefines the
42
+ # three needed Converter instance methods
43
+ module ConverterMethods
44
+ def self.included(base)
45
+ base.class_eval do
46
+ # Just return html5
47
+ def convert(content)
48
+ flags = "#{@config['pandoc']['flags']} #{@config['pandoc']['site_flags']}"
49
+
50
+ output = ''
51
+ Dir::chdir(@config['source']) do
52
+ Open3::popen3("pandoc -t html5 #{flags}") do |stdin, stdout, stderr, thread|
53
+ stdin.puts content
54
+ stdin.close
55
+
56
+ output = stdout.read.strip
57
+ STDERR.print stderr.read
58
+
59
+ # Wait for the process to finish
60
+ thread.value
61
+ end
62
+ end
63
+
64
+ output
65
+ end
66
+
67
+ def matches(ext)
68
+ rgx = '(' + @config['markdown_ext'].gsub(',','|') +')'
69
+ ext =~ Regexp.new(rgx, Regexp::IGNORECASE)
70
+ end
71
+
72
+ def output_ext(ext)
73
+ '.html'
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ # Conjures the metamagic
81
+ JekyllPandocMultipleFormats.build
@@ -0,0 +1,110 @@
1
+ # Copyright (c) 2012-2015 Nicolás Reynolds <fauno@endefensadelsl.org>
2
+ # 2012-2013 Mauricio Pasquier Juan <mpj@endefensadelsl.org>
3
+ # 2013 Brian Candler <b.candler@pobox.com>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining
6
+ # a copy of this software and associated documentation files (the
7
+ # "Software"), to deal in the Software without restriction, including
8
+ # without limitation the rights to use, copy, modify, merge, publish,
9
+ # distribute, sublicense, and/or sell copies of the Software, and to
10
+ # permit persons to whom the Software is furnished to do so, subject to
11
+ # the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be
14
+ # included in all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ module Jekyll
25
+
26
+ class PandocGenerator < Generator
27
+ safe true
28
+
29
+ attr_accessor :site, :config
30
+
31
+ def generate(site)
32
+ @site ||= site
33
+ @config ||= JekyllPandocMultipleFormats::Config.new(@site.config['pandoc'])
34
+
35
+ return if @config.skip?
36
+
37
+ # we create a single array of files
38
+ @pandoc_files = []
39
+
40
+ @config.outputs.each_pair do |output, extra_flags|
41
+ @site.posts.docs.each do |post|
42
+
43
+ pandoc_file = PandocFile.new(@site, output, post)
44
+ next unless pandoc_file.write
45
+
46
+ @site.keep_files << pandoc_file.relative_path
47
+ @pandoc_files << pandoc_file
48
+ end
49
+
50
+ @site.post_attr_hash('categories').each_pair do |title, posts|
51
+ posts.sort!
52
+ pandoc_file = PandocFile.new(@site, output, posts, title)
53
+
54
+ if @site.keep_files.include? pandoc_file.relative_path
55
+ puts "#{pandoc_file.relative_path} is a category file AND a post file"
56
+ puts 'change the category name to fix this'
57
+ next
58
+ end
59
+
60
+ next unless pandoc_file.write
61
+
62
+ @site.keep_files << pandoc_file.relative_path
63
+ @pandoc_files << pandoc_file
64
+ end
65
+ end
66
+
67
+ @pandoc_files.each do |pandoc_file|
68
+ # If output is PDF, we also create the imposed PDF
69
+ if pandoc_file.pdf?
70
+
71
+ if @config.imposition?
72
+
73
+ imposed_file = JekyllPandocMultipleFormats::Imposition
74
+ .new(pandoc_file.path, pandoc_file.papersize,
75
+ pandoc_file.sheetsize, pandoc_file.signature)
76
+
77
+ imposed_file.write
78
+ @site.keep_files << imposed_file.relative_path(@site.dest)
79
+ end
80
+
81
+ # If output is PDF, we also create the imposed PDF
82
+ if @config.binder?
83
+
84
+ binder_file = JekyllPandocMultipleFormats::Binder
85
+ .new(pandoc_file.path, pandoc_file.papersize,
86
+ pandoc_file.sheetsize)
87
+
88
+ binder_file.write
89
+ @site.keep_files << binder_file.relative_path(@site.dest)
90
+ end
91
+
92
+ # Add covers to PDFs after building ready for print files
93
+ if pandoc_file.has_cover?
94
+ # Generate the cover
95
+ next unless pandoc_file.pdf_cover!
96
+ united_output = pandoc_file.path.gsub(/\.pdf\Z/, '-cover.pdf')
97
+ united_file = JekyllPandocMultipleFormats::Unite
98
+ .new(united_output, [pandoc_file.pdf_cover,pandoc_file.path])
99
+
100
+ if united_file.write
101
+ # Replace the original file with the one with cover
102
+ FileUtils.rm_f(pandoc_file.path)
103
+ FileUtils.mv(united_output, pandoc_file.path)
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,97 @@
1
+ # Copyright (c) 2012-2015 Nicolás Reynolds <fauno@endefensadelsl.org>
2
+ # 2012-2013 Mauricio Pasquier Juan <mpj@endefensadelsl.org>
3
+ # 2013 Brian Candler <b.candler@pobox.com>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining
6
+ # a copy of this software and associated documentation files (the
7
+ # "Software"), to deal in the Software without restriction, including
8
+ # without limitation the rights to use, copy, modify, merge, publish,
9
+ # distribute, sublicense, and/or sell copies of the Software, and to
10
+ # permit persons to whom the Software is furnished to do so, subject to
11
+ # the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be
14
+ # included in all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ module JekyllPandocMultipleFormats
25
+ class Imposition < Printer
26
+
27
+ attr_accessor :rounded_pages, :blank_pages, :signature
28
+
29
+ def initialize(file, papersize = nil, sheetsize = nil, signature = nil, extra_options = nil)
30
+ super(file, papersize, sheetsize, extra_options)
31
+ @output_file = file.gsub(/\.pdf\Z/, '-imposed.pdf')
32
+ # Total pages must be modulo 4
33
+ @rounded_pages = round_to_nearest(@pages, 4)
34
+ @blank_pages = @rounded_pages - @pages
35
+ # If we don't use a signature, make a single fold
36
+ @signature = signature || @rounded_pages
37
+ # Also if we specified 0
38
+ @signature = @rounded_pages if signature == 0
39
+
40
+ render_template
41
+ self
42
+ end
43
+
44
+ def to_nup
45
+ # 14 pages example:
46
+ # [ {}, 1, 2, {}, 14, 3, 4, 13, 12, 5, 6, 11, 10, 7, 8, 9 ]
47
+ #
48
+ # * Add {} to missing pages
49
+ # [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, {}, {}]
50
+ # * Take first half [ 1, 2, 3, 4, 5, 6, 7, 8 ]
51
+ # * Reverse second half [ {}, {}, 14, 13, 12, 11, 10, 9 ]
52
+ # * Intercalate the first half into the second half by two
53
+ # elements
54
+ # [ {}, 1, 2, {}, 14, 3, 4, 13, 12, 5, 6, 11, 10, 7, 8, 9 ]
55
+ #
56
+ # An array of numbered pages padded with blank pages ('{}')
57
+ #
58
+ # [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 ] + [ '{}', '{}' ]
59
+ padded = @pages.times.map{|i|i+1} + Array.new(@blank_pages, '{}')
60
+
61
+ # If we have a signature, we have to split in groups up to the
62
+ # amount of pages per signature, and then continue with the rest
63
+ #
64
+ # If we have no signature, we assume it's equal to the total
65
+ # amount of pages, so you only have one fold
66
+ signed = padded.each_slice(@signature).to_a
67
+ folds = []
68
+ signed.each do |fold|
69
+ #
70
+ # Split in halves
71
+ # [ [ 1, 2, 3, 4, 5, 6, 7, 8 ],
72
+ # [ 9, 10, 11, 12, 13, 14, '{}', '{}' ] ]
73
+ halved = fold.each_slice(fold.size / 2).to_a
74
+ # Add a nil as last page. When we reverse it and intercalate by
75
+ # two pages, we'll have [nil, last_page] instead of
76
+ # [last_page,second_to_last_page]
77
+ #
78
+ # [ [ 1, 2, 3, 4, 5, 6, 7, 8 ],
79
+ # [ 9, 10, 11, 12, 13, 14, '{}', '{}', nil ] ]
80
+ halved.last << nil
81
+ # Reverse the second half and intercalate by two pages into the
82
+ # first one. Then remove nil elements and flatten the array.
83
+ #
84
+ # [ [ 1, 2, 3, 4, 5, 6, 7, 8 ],
85
+ # [ nil, '{}', '{}', 14, 13, 12, 11, 10 ] ]
86
+ #
87
+ # [ {}, 1, 2, {}, 14, 3, 4, 13, 12, 5, 6, 11, 10, 7, 8, 9 ]
88
+ folds << halved.last.reverse.each_slice(2).zip(halved.first.each_slice(2).to_a).flatten.compact
89
+ end
90
+
91
+ # Create the matrix of pages (N-Up) per fold
92
+ #
93
+ # ["{}", 1, "{}", 1, 2, "{}", 2, "{}", 14, 3, 14, 3, 4, 13, 4, 13, 12, 5, 12, 5, 6, 11, 6, 11, 10, 7, 10, 7, 8, 9, 8, 9]
94
+ folds.map { |o| o.each_slice(2).map{ |i| a=[]; (@nup/2).times { a = a+i }; a }}.flatten
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,276 @@
1
+ # Copyright (c) 2012-2015 Nicolás Reynolds <fauno@endefensadelsl.org>
2
+ # 2012-2013 Mauricio Pasquier Juan <mpj@endefensadelsl.org>
3
+ # 2013 Brian Candler <b.candler@pobox.com>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining
6
+ # a copy of this software and associated documentation files (the
7
+ # "Software"), to deal in the Software without restriction, including
8
+ # without limitation the rights to use, copy, modify, merge, publish,
9
+ # distribute, sublicense, and/or sell copies of the Software, and to
10
+ # permit persons to whom the Software is furnished to do so, subject to
11
+ # the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be
14
+ # included in all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ module Jekyll
25
+ class PandocFile
26
+ include Convertible
27
+
28
+
29
+ attr_reader :format, :site, :config, :flags, :posts, :slug, :title, :url
30
+ attr_reader :papersize, :sheetsize, :signature
31
+
32
+ def initialize(site, format, posts, title = nil)
33
+ @site = site
34
+ @config = JekyllPandocMultipleFormats::Config.new(@site.config['pandoc']).config
35
+ @format = format
36
+ @flags = []
37
+
38
+ if posts.is_a? Array
39
+ @posts = posts
40
+
41
+ raise ArgumentError.new "'title' argument is required for multipost file" unless title
42
+
43
+ @title = title
44
+ else
45
+ @posts = [posts]
46
+ @title = posts.data['title'] unless title
47
+ end
48
+
49
+ @slug = Utils.slugify(title)
50
+ end
51
+
52
+ def path
53
+ # path is full destination path with all elements of permalink
54
+ path = @site.in_dest_dir(relative_path)
55
+ end
56
+
57
+ def relative_path
58
+ path = URL.unescape_path(url)
59
+ path.gsub! /^\//, ''
60
+
61
+ # but if the post is going to be index.html, use slug + format
62
+ # (ie /year/month/slug/slug.pdf)
63
+ if url.end_with? '/'
64
+ path = File.join(path, @slug)
65
+ path << '.'
66
+ path << @format
67
+ end
68
+
69
+ path
70
+ end
71
+
72
+ # if it's just one article, the url is the same except it's not html
73
+ # otherwise we use the permalink template with some placeholder
74
+ def url
75
+ @url = if single_post?
76
+ single_post.url.gsub(/\.html$/, ".#{@format}")
77
+ else
78
+ URL.new({
79
+ template: @config['bundle_permalink'],
80
+ placeholders: url_placeholders,
81
+ permalink: nil }).to_s
82
+ end
83
+ end
84
+
85
+ def url_placeholders
86
+ { output_ext: @format,
87
+ slug: @slug,
88
+ title: @title }
89
+ end
90
+
91
+ # adds post metadata as yaml metadata
92
+ def yaml_metadata
93
+ if single_post?
94
+
95
+ # if we were to merge config to data, the default options would
96
+ # take precedence
97
+ data = Jekyll::Utils.deep_merge_hashes @config, single_post.data
98
+ single_post.merge_data! data
99
+
100
+
101
+ # we extract the excerpt because it serializes as an object and
102
+ # breaks pandoc
103
+ metadata = single_post.data.reject{ |k| k == 'excerpt' }
104
+
105
+ if @config['date_format']
106
+ metadata['date'] = metadata['date'].strftime(@config['date_format'])
107
+ else
108
+ metadata.delete('date')
109
+ end
110
+ else
111
+ # we have to use this fugly syntax because jekyll doesn't do
112
+ # symbols
113
+ metadata = {
114
+ 'date' => @config['date_format'] ? Date.today.strftime(@config['date_format']) : nil,
115
+ 'title' => @title,
116
+ 'author' => nil,
117
+ 'papersize' => papersize,
118
+ 'sheetsize' => sheetsize,
119
+ 'signature' => signature
120
+ }
121
+ end
122
+
123
+ # fix page sizes, pandoc uses 'A4' while printer.rb uses
124
+ # 'a4paper'
125
+ %w[papersize sheetsize].each do |size|
126
+ metadata[size] = fix_size metadata[size]
127
+ end
128
+
129
+ metadata.to_yaml << "\n---\n"
130
+ end
131
+
132
+ def content
133
+ if single_post?
134
+ single_post.content
135
+ else
136
+ header_re = /^(#+.*\n*|.*\n[=-]+\n*)\Z/
137
+ bib_title = ""
138
+ @posts.map do |post|
139
+ bib_title = post.content.match(header_re).to_s if bib_title.empty?
140
+ # remove bibliography titles
141
+ # since pandoc does it's own bibliography output, it recommends
142
+ # leaving an empty chapter title to mark it as such
143
+ post.content.gsub(header_re, '')
144
+ # we add the first bibliography title we can find in the end
145
+ end.join("\n\n\n") << bib_title
146
+ end
147
+ end
148
+
149
+ def write
150
+ FileUtils.mkdir_p(File.dirname(path))
151
+ # Remove the file before creating it
152
+ FileUtils.rm_f(path)
153
+ # Move to the source dir since everything will be relative to that
154
+ Dir::chdir(@site.config['source']) do
155
+ # Do the stuff
156
+ Open3::popen3(command) do |stdin, stdout, stderr, thread|
157
+ stdin.puts yaml_metadata
158
+ stdin.puts content
159
+ stdin.close
160
+ STDERR.print stderr.read
161
+
162
+ # Wait for the process to finish
163
+ thread.value
164
+ end
165
+ end
166
+
167
+ File.exists?(path)
168
+ end
169
+
170
+ # Returns a cover, without checking if it exists
171
+ #
172
+ # It assumes covers are in PNG format
173
+ def cover
174
+ if single_post? && single_post.data['cover']
175
+ File.join(@site.config['source'], single_post.data['cover'])
176
+ else
177
+ File.join(@site.config['source'], @config['covers_dir'], "#{@slug}.png")
178
+ end
179
+ end
180
+
181
+ # Returns a PDF cover
182
+ def pdf_cover
183
+ cover.gsub(/\.[^\.]+\Z/, '.pdf')
184
+ end
185
+
186
+ def pdf_cover!
187
+ if has_cover? && !File.exists?(pdf_cover)
188
+ Open3::popen3("convert \"#{cover}\" \"#{pdf_cover}\"") do |stdin, stdout, stderr, thread|
189
+ STDERR.print stderr.read
190
+
191
+ # Wait for the process to finish
192
+ thread.value
193
+ end
194
+ end
195
+
196
+ File.exists?(pdf_cover)
197
+ end
198
+
199
+ def flags
200
+ return @flags.join ' ' unless @flags.empty?
201
+
202
+ @flags << @config['flags']
203
+ @flags << @config['outputs'][@format] if @config['outputs'].key?(@format)
204
+ @flags << '-o'
205
+ @flags << path
206
+
207
+ # Binary formats don't need a -t flag
208
+ unless binary?
209
+ @flags << '-t'
210
+ @flags << @format
211
+ end
212
+
213
+ if epub? && has_cover?
214
+ @flags << '--epub-cover-image'
215
+ @flags << cover
216
+ end
217
+
218
+ @flags.join ' '
219
+ end
220
+
221
+ def command
222
+ 'pandoc ' << flags
223
+ end
224
+
225
+ def pdf?
226
+ @format == 'pdf'
227
+ end
228
+
229
+ def epub?
230
+ %w[epub epub3].include? @format
231
+ end
232
+
233
+ # These formats are binary files and must use the -o flag
234
+ def binary?
235
+ %w[pdf epub epub3 odt docx].include? @format
236
+ end
237
+
238
+ def single_post?
239
+ @posts.count == 1
240
+ end
241
+
242
+ def has_cover?
243
+ File.exists? cover
244
+ end
245
+
246
+ def papersize
247
+ @papersize ||= find_option 'papersize'
248
+ end
249
+
250
+ def sheetsize
251
+ @sheetsize ||= find_option 'sheetsize'
252
+ end
253
+
254
+ def signature
255
+ @signature ||= find_option 'signature'
256
+ end
257
+
258
+ private
259
+
260
+ def single_post
261
+ @posts.first
262
+ end
263
+
264
+ def find_option(name)
265
+ if @posts.any? { |p| p.data.key? name }
266
+ @posts.select { |p| p.data.key? name }.first.data[name]
267
+ else
268
+ @config[name]
269
+ end
270
+ end
271
+
272
+ def fix_size(size)
273
+ size.gsub /paper$/, ''
274
+ end
275
+ end
276
+ end