makebook 0.0.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: bc96443605ecbb4bc2292739399d78bca1ed5dfe
4
- data.tar.gz: cc4582fa85d2d3c8a146a75632f8968cbfabd007
3
+ metadata.gz: 6e0fc9cdd0fdc3c57455e7d4b0cd6d5ac37facee
4
+ data.tar.gz: 5a33589ba0d2878076ff2665e61f0c833aa49aaa
5
5
  SHA512:
6
- metadata.gz: 69e253043ee5d518c90ea349f8d5d057ea88933160bce17e1732792d643cfaddda9f057d55d52c51e859bafb956e9858bf3391699cbf4a028b1d98fc195c44d7
7
- data.tar.gz: 8703a9ec3771644e3619ed65416be682802539ab6da64d4b3a0d741d407acbdac0ac1332861b5243b62dcb661dab299015caa4cff8b1d5df500f60554f10133b
6
+ metadata.gz: f1ffb6ec591c5171dfbab6b55ef5109d0cfbaa6dc91d07317e25376b7eab234b159a0ce2cbda30d7967af6e1568db25994305176d7b6215e2c877512dceb6e78
7
+ data.tar.gz: 9195ae0e5c4f67efc44faea0f0334bd83afa87a5b2a5db5a21a4a98f44f658563e19edc85a81cdb838f8c30359ff87aa7cd63199a74baec2d05c060d1b5e992d
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'makebook/cli'
4
+
5
+ MakeBook::CLI.start(ARGV)
@@ -1 +1,11 @@
1
+ # Make books from Markdown
1
2
  module MakeBook; end
3
+
4
+ require 'makebook/build'
5
+ require 'makebook/book'
6
+ require 'makebook/chapter'
7
+ require 'makebook/formats'
8
+
9
+ module MakeBook
10
+ EXT = '.book.yml'.freeze
11
+ end
@@ -0,0 +1,54 @@
1
+ require 'yaml'
2
+
3
+ # A book
4
+ class MakeBook::Book
5
+ attr_reader :source
6
+
7
+ def initialize(file)
8
+ @source = Pathname.new(file)
9
+ end
10
+
11
+ private def book
12
+ @book ||= YAML.load(File.read(source))
13
+ end
14
+
15
+ def root
16
+ source.dirname
17
+ end
18
+
19
+ def name
20
+ book['name'] || source.basename(MakeBook::EXT)
21
+ end
22
+
23
+ def dir
24
+ root + name
25
+ end
26
+
27
+ def formats
28
+ book['output']['formats'] || []
29
+ end
30
+
31
+ def title
32
+ book['title'] || name
33
+ end
34
+
35
+ def chapters
36
+ (book['chapters'] || []).map do |c|
37
+ MakeBook::Chapter.new(book: self, name: c)
38
+ end
39
+ end
40
+
41
+ def styles
42
+ (book['styles'] || []).map do |s|
43
+ MakeBook::Formats::HTML::Stylesheet.new(root + s)
44
+ end
45
+ end
46
+
47
+ def toc
48
+ book['toc'] && Integer(book['toc'])
49
+ end
50
+
51
+ def toc?
52
+ !toc.nil?
53
+ end
54
+ end
@@ -0,0 +1,28 @@
1
+ # Build context
2
+ class MakeBook::Build
3
+ attr_reader :root
4
+
5
+ def initialize(out: nil)
6
+ @root = Pathname.new(out || MakeBook::Build.default_root)
7
+ end
8
+
9
+ def build(book)
10
+ b = MakeBook::Book.new(book)
11
+ b.formats.each do |format|
12
+ format, options = format.first if format.is_a? Hash
13
+ options ||= {}
14
+
15
+ case format
16
+ when 'html' then MakeBook::Formats::HTML.new(b, self, options).make
17
+ when 'pdf' then MakeBook::Formats::PDF.new(b, self, options).make
18
+ else raise NotImplementedError, format
19
+ end
20
+ end
21
+ end
22
+
23
+ class << self
24
+ def default_root
25
+ Pathname.pwd + 'out'
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,13 @@
1
+ # A book chapter
2
+ class MakeBook::Chapter
3
+ attr_reader :source
4
+
5
+ def initialize(file = nil, book: nil, name: nil)
6
+ file ||= book.root + "#{name}.md"
7
+ @source = Pathname.new(file)
8
+ end
9
+
10
+ def root
11
+ source.dirname
12
+ end
13
+ end
@@ -0,0 +1,20 @@
1
+ require 'thor'
2
+ require 'makebook'
3
+
4
+ # CLI commands
5
+ class MakeBook::CLI < Thor
6
+ class_option :verbose, type: :boolean
7
+
8
+ desc 'build [--out=DIRECTORY] [BOOK|...]', 'Make a book or more'
9
+ options out: :optional
10
+ def build(*books)
11
+ build = MakeBook::Build.new(out: options[:out])
12
+
13
+ books = Pathname.glob("**/*#{MakeBook::EXT}") if books.empty?
14
+
15
+ books.each do |book|
16
+ puts "building #{book}" if options[:verbose]
17
+ build.build(book)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,5 @@
1
+ # Output format
2
+ module MakeBook::Formats; end
3
+
4
+ require 'makebook/formats/html'
5
+ require 'makebook/formats/pdf'
@@ -0,0 +1,304 @@
1
+ require 'kramdown'
2
+ require 'nokogiri'
3
+ require 'base64'
4
+
5
+ # HTML output format
6
+ # rubocop:disable Metrics/ClassLength
7
+ class MakeBook::Formats::HTML
8
+ attr_reader :book, :build
9
+
10
+ def initialize(book, build, options = {})
11
+ @book = book
12
+ @build = build
13
+ @options = options
14
+ end
15
+
16
+ # External element
17
+ class Asset
18
+ attr_reader :source, :mime
19
+
20
+ def initialize(source, mime: nil)
21
+ @source = source
22
+ @mime = mime
23
+ end
24
+
25
+ def digest
26
+ sha1 = Digest::SHA1.new
27
+ sha1 << read
28
+ sha1.hexdigest
29
+ end
30
+
31
+ def build_name
32
+ "#{digest}#{source.extname}"
33
+ end
34
+
35
+ def read
36
+ File.read(source)
37
+ end
38
+
39
+ def base64
40
+ Base64.encode64(read)
41
+ end
42
+
43
+ def data_uri
44
+ "data:#{mime};base64,#{base64}"
45
+ end
46
+ end
47
+
48
+ # Wrap a string range for manipulation
49
+ class Range
50
+ def initialize(string, range)
51
+ @string = string
52
+ @range = range
53
+ end
54
+
55
+ def value=(string)
56
+ @string[@range] = string
57
+ end
58
+ end
59
+
60
+ # Stylesheet asset
61
+ class Stylesheet < Asset
62
+ TYPES = {
63
+ 'eot' => 'application/vnd.ms-fontobject',
64
+ 'otf' => 'application/font-sfnt',
65
+ 'ttf' => 'application/font-sfnt',
66
+ 'woff' => 'application/font-woff',
67
+ 'png' => 'image/png',
68
+ 'jpg' => 'image/jpg',
69
+ 'jpeg' => 'image/jpg',
70
+ 'svg' => 'image/svg+xml',
71
+ 'gif' => 'image/gif',
72
+ }.freeze
73
+
74
+ def initialize(source, mime: 'text/css')
75
+ super
76
+ end
77
+
78
+ def assets
79
+ scan(/url\("([^"]+\.(#{TYPES.keys.join('|')}))"\)/).map do |path, ext|
80
+ [
81
+ Asset.new(source.dirname + path, mime: TYPES[ext]),
82
+ wrap(Regexp.last_match.begin(1)...Regexp.last_match.end(1)),
83
+ ]
84
+ end
85
+ end
86
+
87
+ def to_css
88
+ read
89
+ end
90
+
91
+ def read
92
+ @read ||= super
93
+ end
94
+
95
+ private
96
+
97
+ def scan(re)
98
+ read.enum_for(:scan, re)
99
+ end
100
+
101
+ def wrap(range)
102
+ Range.new(read, range)
103
+ end
104
+ end
105
+
106
+ # Section output
107
+ class Section
108
+ attr_reader :source
109
+
110
+ def initialize(source)
111
+ @source = source
112
+ end
113
+
114
+ def source_document
115
+ Kramdown::Document.new(File.read(source),
116
+ input: 'GFM', hard_wrap: false)
117
+ end
118
+
119
+ def node
120
+ @node ||= wrap(Nokogiri::HTML(source_document.to_html))
121
+ end
122
+
123
+ def to_html
124
+ node.to_s
125
+ end
126
+
127
+ # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
128
+ def toc(levels, ordered: false)
129
+ fragment = Nokogiri::HTML.fragment('')
130
+ tag = ordered ? 'ol' : 'ul'
131
+
132
+ toc_elements(levels).reduce([fragment, nil]) do |(level, last), header|
133
+ if last.nil? || last.name < header.name
134
+ level << node = Nokogiri::XML::Node.new(tag, fragment).tap do |ul|
135
+ ul['class'] = header.name
136
+ end
137
+ level = node
138
+ elsif last.name > header.name
139
+ level = level.parent
140
+ end
141
+
142
+ level << Nokogiri::XML::Node.new('li', fragment).tap do |li|
143
+ li['class'] = header.name
144
+ li << Nokogiri::XML::Node.new('a', fragment).tap do |a|
145
+ a['href'] = "##{header['id']}"
146
+ a.content = header.text
147
+ end
148
+ end
149
+
150
+ [level, header]
151
+ end
152
+
153
+ fragment
154
+ end
155
+ # rubocop:enable Metrics/AbcSize,Metrics/MethodLength
156
+
157
+ private def toc_elements(levels)
158
+ node.css(toc_selector(levels))
159
+ end
160
+
161
+ private def toc_selector(levels)
162
+ Array.new(levels) { |i| "h#{i + 1}" }.join(', ')
163
+ end
164
+
165
+ def images
166
+ node.css('img').map { |img| [Asset.new(root + img['src']), img] }
167
+ end
168
+
169
+ def root
170
+ source.dirname
171
+ end
172
+
173
+ private def wrap(doc)
174
+ doc.css('body').first.tap do |c|
175
+ c.name = 'section'
176
+ c['class'] = 'chapter'
177
+ end
178
+ end
179
+ end
180
+
181
+ def sections
182
+ @sections ||= book.chapters.map { |c| Section.new(c.source) }
183
+ end
184
+
185
+ # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
186
+ def make
187
+ build_dir.mkpath
188
+
189
+ head << <<-HTML
190
+ <title>#{book.title}</title>
191
+ HTML
192
+
193
+ book.styles.each do |stylesheet|
194
+ stylesheet.assets.reverse_each do |asset, range|
195
+ range.value = asset.build_name
196
+ FileUtils.cp(asset.source, build_dir + asset.build_name)
197
+ end
198
+
199
+ File.open(build_dir + stylesheet.build_name, 'wb') do |f|
200
+ f.write(stylesheet.to_css)
201
+ end
202
+
203
+ head << <<-HTML
204
+ <link rel="stylesheet" href="#{build_dir.basename + stylesheet.build_name}" />
205
+ HTML
206
+ end
207
+
208
+ header << book.title
209
+
210
+ sections.each do |section|
211
+ nav << section.toc(book.toc).to_s if book.toc?
212
+
213
+ section.images.each do |asset, img|
214
+ img['src'] = build_dir.basename + asset.build_name
215
+ FileUtils.cp(asset.source, build_dir + asset.build_name)
216
+ end
217
+
218
+ main << section.to_html
219
+ end
220
+
221
+ open { |io| assemble(io) }
222
+ end
223
+ # rubocop:enable Metrics/AbcSize,Metrics/MethodLength
224
+
225
+ # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
226
+ private def assemble(io)
227
+ io.write("<!DOCTYPE html>\n")
228
+ io.write(<<-EOS)
229
+ <html>
230
+ <head>
231
+ <meta charset="utf-8" />
232
+ EOS
233
+ io.write(head)
234
+ io.write(<<-EOS)
235
+ </head>
236
+ <body class='book'>
237
+ <header>
238
+ EOS
239
+ io.write(header)
240
+ io.write(<<-EOS)
241
+ </header>
242
+ <nav>
243
+ EOS
244
+ io.write(nav)
245
+ io.write(<<-EOS)
246
+ </nav>
247
+ <main>
248
+ EOS
249
+ io.write(main)
250
+ io.write(<<-EOS)
251
+ </main>
252
+ <footer>
253
+ EOS
254
+ io.write(footer)
255
+ io.write(<<-EOS)
256
+ </footer>
257
+ </body>
258
+ </html>
259
+ EOS
260
+ end
261
+ # rubocop:enable Metrics/AbcSize,Metrics/MethodLength
262
+
263
+ private def head
264
+ @head ||= ''
265
+ end
266
+
267
+ private def header
268
+ @header ||= ''
269
+ end
270
+
271
+ private def nav
272
+ @nav ||= ''
273
+ end
274
+
275
+ private def main
276
+ @main ||= ''
277
+ end
278
+
279
+ private def footer
280
+ @footer ||= ''
281
+ end
282
+
283
+ def build_name
284
+ build.root + book.root + book.name
285
+ end
286
+
287
+ def build_file
288
+ Pathname("#{build_name}.html")
289
+ end
290
+
291
+ def build_dir
292
+ build_name
293
+ end
294
+
295
+ def stale?
296
+ book.source.mtime > build_file.mtime
297
+ rescue Errno::ENOENT
298
+ true
299
+ end
300
+
301
+ private def open
302
+ File.open(build_file, 'wb') { |f| yield f }
303
+ end
304
+ end
@@ -0,0 +1,61 @@
1
+ require 'pdfkit'
2
+
3
+ # PDF output format
4
+ class MakeBook::Formats::PDF
5
+ attr_reader :book, :build
6
+
7
+ def initialize(book, build, options = {})
8
+ @book = book
9
+ @build = build
10
+ @options = kit_options_from(options)
11
+ end
12
+
13
+ def make
14
+ html = MakeBook::Formats::HTML.new(book, build)
15
+ html.make if html.stale?
16
+
17
+ kit = PDFKit.new(File.new(html.build_file), options)
18
+ kit.to_file(build_file)
19
+ end
20
+
21
+ private def options
22
+ {
23
+ print_media_type: true,
24
+ page_size: 'A4',
25
+ margin_top: '2cm',
26
+ margin_bottom: '2cm',
27
+ margin_left: '2cm',
28
+ margin_right: '2cm',
29
+ }.merge(@options)
30
+ end
31
+
32
+ private def kit_options_from(options)
33
+ options.dup.tap do |o|
34
+ %w(header-html footer-html).each do |k|
35
+ o[:"#{k}"] = uri_from(relative_path(o.delete(k))) if o.key?(k)
36
+ end
37
+ end
38
+ end
39
+
40
+ private def uri_from(path)
41
+ "file://#{Pathname.pwd + path}"
42
+ end
43
+
44
+ private def relative_path(path)
45
+ book.source + path
46
+ end
47
+
48
+ def build_name
49
+ build.root + book.root + book.name
50
+ end
51
+
52
+ def build_file
53
+ Pathname("#{build_name}.pdf")
54
+ end
55
+
56
+ def stale?
57
+ book.source.mtime > build_file.mtime
58
+ rescue Errno::ENOENT
59
+ true
60
+ end
61
+ end
metadata CHANGED
@@ -1,26 +1,145 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: makebook
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
- - ADHOC-GTI
7
+ - Loic Nageleisen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-05-02 00:00:00.000000000 Z
12
- dependencies: []
13
- description: ''
14
- email: foss@adhoc-gti.com
15
- executables: []
11
+ date: 2018-06-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: kramdown
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.9'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.9'
27
+ - !ruby/object:Gem::Dependency
28
+ name: nokogiri
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.6'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.6'
41
+ - !ruby/object:Gem::Dependency
42
+ name: tilt-pdf
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.10.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.10.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: thor
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 0.19.1
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 0.19.1
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 0.46.0
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 0.46.0
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '10.5'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '10.5'
111
+ - !ruby/object:Gem::Dependency
112
+ name: minitest
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '5.8'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '5.8'
125
+ description: " Assembling books with style from Markdown sources\n"
126
+ email: l.nageleisen@adhoc-gti.com
127
+ executables:
128
+ - makebook
16
129
  extensions: []
17
130
  extra_rdoc_files: []
18
131
  files:
19
- - LICENSE
132
+ - bin/makebook
20
133
  - lib/makebook.rb
21
- homepage: https://github.com/adhoc-gti/makebook
22
- licenses:
23
- - BSD-3-Clause
134
+ - lib/makebook/book.rb
135
+ - lib/makebook/build.rb
136
+ - lib/makebook/chapter.rb
137
+ - lib/makebook/cli.rb
138
+ - lib/makebook/formats.rb
139
+ - lib/makebook/formats/html.rb
140
+ - lib/makebook/formats/pdf.rb
141
+ homepage: https://gitlab.adhoc-gti.com/lnageleisen/makebook
142
+ licenses: []
24
143
  metadata: {}
25
144
  post_install_message:
26
145
  rdoc_options: []
@@ -41,5 +160,5 @@ rubyforge_project:
41
160
  rubygems_version: 2.6.14
42
161
  signing_key:
43
162
  specification_version: 4
44
- summary: Generate HTML and PDF from Markdown chapters
163
+ summary: Creating books from Markdown
45
164
  test_files: []
data/LICENSE DELETED
@@ -1,24 +0,0 @@
1
- Copyright (c) 2016, ADHOC-GTI
2
- All rights reserved.
3
-
4
- Redistribution and use in source and binary forms, with or without
5
- modification, are permitted provided that the following conditions are met:
6
- * Redistributions of source code must retain the above copyright
7
- notice, this list of conditions and the following disclaimer.
8
- * Redistributions in binary form must reproduce the above copyright
9
- notice, this list of conditions and the following disclaimer in the
10
- documentation and/or other materials provided with the distribution.
11
- * Neither the name of the copyright holders nor the
12
- names of its contributors may be used to endorse or promote products
13
- derived from this software without specific prior written permission.
14
-
15
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
16
- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17
- WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18
- DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE
19
- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20
- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21
- SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22
- CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23
- OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24
- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.