makebook 0.0.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.