bookingit 0.0.1 → 0.1.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.
@@ -24,3 +24,21 @@ Given(/^this config file:$/) do |string|
24
24
  end
25
25
  end
26
26
 
27
+ Given(/^a git repo "(.*?)" in "(.*?)" containing the file "(.*?)" and a tag "(.*?)"$/) do |repo_name, repo_basedir, file_name, tag_name|
28
+ FileUtils.chdir "tmp/aruba" do
29
+ FileUtils.mkdir repo_basedir
30
+ @dirs_created << repo_basedir
31
+ FileUtils.chdir repo_basedir do
32
+ FileUtils.mkdir repo_name
33
+ FileUtils.chdir repo_name do
34
+ File.open(file_name,'w') do |file|
35
+ file.puts "Some stuff and whatnot"
36
+ end
37
+ system "git init"
38
+ system "git add #{file_name}"
39
+ system "git commit -m 'initial'"
40
+ system "git tag #{tag_name}"
41
+ end
42
+ end
43
+ end
44
+ end
@@ -8,12 +8,16 @@ Before do
8
8
  @puts = true
9
9
  @original_rubylib = ENV['RUBYLIB']
10
10
  @files_created = []
11
+ @dirs_created = []
11
12
  ENV['RUBYLIB'] = LIB_DIR + File::PATH_SEPARATOR + ENV['RUBYLIB'].to_s
12
13
  end
13
14
 
14
15
  After do
15
16
  ENV['RUBYLIB'] = @original_rubylib
16
17
  @files_created.each do |file|
17
- FileUtils.rm file
18
+ FileUtils.rm file if File.exists?(file)
19
+ end
20
+ @dirs_created.each do |dir|
21
+ FileUtils.rm_rf dir if File.exists?(dir)
18
22
  end
19
23
  end
data/lib/bookingit.rb CHANGED
@@ -1,3 +1,8 @@
1
+ require 'bookingit/views'
1
2
  require 'bookingit/version.rb'
3
+ require 'bookingit/errors.rb'
4
+ require 'bookingit/code_block_interpreter.rb'
5
+ require 'bookingit/shell_command.rb'
2
6
  require 'bookingit/renderer.rb'
3
7
  require 'bookingit/config.rb'
8
+ require 'bookingit/book.rb'
@@ -0,0 +1,94 @@
1
+ module Bookingit
2
+ class Book
3
+ def initialize(config)
4
+ @config = config
5
+ @output_dir = 'book'
6
+ end
7
+
8
+ def render_html!
9
+ mkdir @output_dir unless Dir.exists?(@output_dir)
10
+
11
+ rendering_config = @config.rendering_config
12
+ rendering_config[:cache] = File.expand_path('cache') if @config.cache
13
+ renderer = Bookingit::Renderer.new(@config)
14
+ @redcarpet = Redcarpet::Markdown.new(renderer, no_intra_emphasis: true,
15
+ tables: true,
16
+ fenced_code_blocks: true,
17
+ autolink: true,
18
+ strikethrough: true,
19
+ superscript: true)
20
+
21
+ toc = parse_chapters_to_get_headers(renderer)
22
+ generate_chapters(renderer)
23
+ generate_toc(toc,renderer.stylesheets,renderer.theme)
24
+ copy_assets(renderer)
25
+ end
26
+
27
+ private
28
+
29
+ def parse_chapters_to_get_headers(renderer)
30
+ toc = {}
31
+ each_chapter do |matter,contents,chapter|
32
+ toc[matter] ||= []
33
+ renderer.current_chapter = chapter
34
+ @redcarpet.render(contents)
35
+ chapter.title = Array(renderer.headers[1]).first
36
+ toc[matter] << chapter
37
+ end
38
+ toc
39
+ end
40
+
41
+ def each_chapter(&block)
42
+ %w(front_matter main_matter back_matter).each do |matter|
43
+ @config.send(matter).chapters.each do |chapter|
44
+ block.call(matter,File.read(chapter.markdown_path),chapter)
45
+ end
46
+ end
47
+ end
48
+
49
+ def generate_chapters(renderer)
50
+ each_chapter do |_,contents,chapter|
51
+ output_file = chapter.relative_url
52
+ renderer.current_chapter = chapter
53
+ File.open(File.join(@output_dir,output_file),'w') do |file|
54
+ file.puts @redcarpet.render(contents)
55
+ end
56
+ end
57
+ end
58
+
59
+ def generate_toc(toc,stylesheets,theme)
60
+ if @config.templates["index"] =~ /^\//
61
+ Views::IndexView.template_path = File.dirname(@config.templates["index"])
62
+ Views::IndexView.template_name = File.basename(@config.templates["index"])
63
+ else
64
+ Views::IndexView.template_name = @config.templates["index"]
65
+ end
66
+ view = Views::IndexView.new(stylesheets,
67
+ theme,
68
+ toc['front_matter'],
69
+ toc['main_matter'],
70
+ toc['back_matter'],
71
+ @config)
72
+ File.open(File.join(@output_dir,'index.html'),'w') do |index|
73
+ index.puts view.render
74
+ end
75
+ end
76
+
77
+ def copy_assets(renderer)
78
+ @config.rendering_config[:stylesheets].each do |stylesheet|
79
+ cp stylesheet, @output_dir
80
+ end
81
+ renderer.images.each do |image|
82
+ if File.exists?(image)
83
+ dest_dir = File.join(@output_dir,File.dirname(image))
84
+ unless File.exists? dest_dir
85
+ mkdir_p dest_dir
86
+ end
87
+ cp image,dest_dir
88
+ else
89
+ $stderr.puts "Missing image #{image}"
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,114 @@
1
+ module Bookingit
2
+ class CodeBlockInterpreter
3
+ def initialize(code)
4
+ @code = code.strip
5
+ @result = nil
6
+ end
7
+
8
+ def when_file(&block)
9
+ if @code =~ /^\s*file:\/\/(.*)$/
10
+ path = $1
11
+ @result = block.call(path)
12
+ end
13
+ self
14
+ end
15
+
16
+ def when_file_in_git(&block)
17
+ when_git_reference do |repo_path,path_in_repo,reference|
18
+ if reference !~ /^(.+)\.\.(.+)$/ && reference !~ /^(.+)\!(.+)$/ && reference !~ /^\.\.(.+)$/
19
+ chdir repo_path do
20
+ @result = block.call(path_in_repo,reference)
21
+ end
22
+ end
23
+ end
24
+ self
25
+ end
26
+
27
+ def when_git_diff(&block)
28
+ when_git_reference do |repo_path,path_in_repo,reference|
29
+ if reference =~ /^(.+)\.\.(.+)$/
30
+ chdir repo_path do
31
+ @result = block.call(path_in_repo,reference)
32
+ end
33
+ elsif reference =~ /^\.\.(.+)$/
34
+ tag_or_sha = $1
35
+ chdir repo_path do
36
+ @result = block.call(path_in_repo,"#{tag_or_sha}^..#{tag_or_sha}")
37
+ end
38
+ end
39
+ end
40
+ self
41
+ end
42
+
43
+ def when_shell_command_in_git(&block)
44
+ when_git_reference do |repo_path,path_in_repo,reference|
45
+ if reference =~ /^([^!]+)\!(.+)$/
46
+ reference = $1
47
+ command,exit_type = parse_shell_command($2)
48
+ chdir repo_path do
49
+ @result = block.call(reference,shell_command(path: path_in_repo,command: command, exit_type: exit_type))
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ def when_shell_command(&block)
56
+ if @code.strip =~ /^\s*sh:\/\/(.+)#([^#]+)$/
57
+ path = $1
58
+ command,exit_type = parse_shell_command($2)
59
+ @result = block.call(shell_command(path: path,command: command, exit_type: exit_type))
60
+ end
61
+ self
62
+ end
63
+
64
+ def otherwise(&block)
65
+ @result = block.call if @result.nil?
66
+ self
67
+ end
68
+
69
+ def result
70
+ raise "You didn't handle every possible case" if @result.nil?
71
+ @result
72
+ end
73
+
74
+ private
75
+
76
+ def shell_command(path: nil, command: nil, exit_type: nil)
77
+ ShellCommand.new(path: path,command: command) do |exit_status|
78
+ case exit_type
79
+ when :zero
80
+ exit_status == 0
81
+ when :nonzero
82
+ exit_status != 0
83
+ else
84
+ raise "unknown exit type #{exit_type}"
85
+ end
86
+ end
87
+ end
88
+
89
+ def parse_shell_command(shell_command)
90
+ if shell_command =~ /(^.*)!([^!]+)$/
91
+ [$1,$2.to_sym]
92
+ else
93
+ [shell_command,:zero]
94
+ end
95
+ end
96
+
97
+ def when_git_reference(&block)
98
+ if @code =~ /^\s*git:\/\/(.*)$/
99
+ path = $1
100
+ if path =~ /(^.*).git\/(.*)#([^#]+)$/
101
+ repo_path = $1
102
+ path_in_repo = $2
103
+ path_in_repo = '.' if String(path_in_repo).strip == ''
104
+ reference = $3
105
+ block.call(repo_path,path_in_repo,reference)
106
+ else
107
+ raise "You must provide a SHA1 or tagname: #{path}"
108
+ end
109
+ end
110
+ self
111
+ end
112
+
113
+ end
114
+ end
@@ -7,50 +7,84 @@ module Bookingit
7
7
 
8
8
  attr_reader :front_matter,
9
9
  :main_matter,
10
- :back_matter
10
+ :back_matter,
11
+ :rendering_config,
12
+ :cache,
13
+ :options,
14
+ :templates
11
15
 
12
16
  def initialize(config_json,root_dir)
13
17
  config_hash = JSON.parse(config_json)
14
18
 
15
- @front_matter = Matter.new(config_hash['front_matter'],root_dir)
16
- @main_matter = Matter.new(config_hash['main_matter'],root_dir)
17
- @back_matter = Matter.new(config_hash['back_matter'],root_dir)
19
+ @front_matter = Matter.new(config_hash.delete('front_matter'),root_dir)
20
+ @main_matter = Matter.new(config_hash.delete('main_matter'),root_dir)
21
+ @back_matter = Matter.new(config_hash.delete('back_matter'),root_dir)
22
+ @templates = config_hash.delete("templates") || {}
23
+ @templates["index"] ||= "index.html"
24
+ @rendering_config = create_rendering_config(config_hash.delete('rendering'))
25
+ @cache = false
26
+ @options = config_hash
27
+
28
+ all_chapters = (@front_matter.chapters + @main_matter.chapters + @back_matter.chapters)
29
+ all_chapters.each_with_index do |chapter,i|
30
+ if i > 0
31
+ all_chapters[i-1].next_chapter = chapter
32
+ chapter.previous_chapter = all_chapters[i-1]
33
+ end
34
+ if i < (all_chapters.size-1)
35
+ all_chapters[i+1].previous_chapter = chapter
36
+ chapter.next_chapter = all_chapters[i+1]
37
+ end
38
+ end
39
+ end
40
+
41
+ def cache=(cache)
42
+ @cache = cache
43
+ end
44
+
45
+ private
46
+
47
+ def create_rendering_config(raw_config)
48
+ raw_config ||= {}
49
+ rendering_config = {}
50
+ rendering_config[:stylesheets] = Array(raw_config['stylesheets'])
51
+ rendering_config[:basedir] = raw_config['git_repos_basedir']
52
+ rendering_config[:languages] = Hash[(raw_config['languages'] || {}).map { |match,language|
53
+ if match =~ /^\/(.+)\/$/
54
+ [Regexp.new($1),language]
55
+ else
56
+ [match,language]
57
+ end
58
+ }]
59
+ rendering_config[:theme] = raw_config['syntax_theme']
60
+
61
+ rendering_config
18
62
  end
19
63
 
20
64
  class Matter
21
65
  attr_reader :chapters
22
- def initialize(chapters_config,root_dir)
23
- @chapters = Array(chapters_config).map { |chapter_config|
24
- Chapter.new(chapter_config,root_dir)
66
+ def initialize(chapter_filenames,root_dir)
67
+ @chapters = Array(chapter_filenames).map { |chapter_filename|
68
+ Chapter.new(markdown_path: File.join(root_dir,chapter_filename))
25
69
  }
26
70
  end
27
71
  end
28
72
 
29
73
  class Chapter
30
- attr_reader :sections
31
- attr_reader :path
32
-
33
- def initialize(chapter_config,root_dir)
34
- files = Array(chapter_config).flatten(1).map { |file|
35
- Dir[File.join(root_dir,file)]
36
- }.flatten
37
- if files.size == 1
38
- @sections = []
39
- @path = files[0]
40
- else
41
- @sections = files.map { |file|
42
- Section.new(file)
43
- }
44
- end
74
+ attr_reader :markdown_path, :relative_url, :sections
75
+ attr_accessor :title, :previous_chapter, :next_chapter
76
+
77
+ def initialize(markdown_path: nil, relative_url: nil)
78
+ @markdown_path = markdown_path
79
+ @relative_url = relative_url || (File.basename(markdown_path, File.extname(markdown_path)) + ".html")
80
+ @sections = []
45
81
  end
46
- end
47
82
 
48
- class Section
49
- attr_reader :path
50
- def initialize(path)
51
- @path = path
83
+ def add_section(title,anchor)
84
+ section = Chapter.new(relative_url: self.relative_url + "##{anchor}")
85
+ section.title = title
86
+ @sections << section
52
87
  end
53
88
  end
54
89
  end
55
-
56
90
  end
@@ -0,0 +1,16 @@
1
+ require 'gli'
2
+ module Bookingit
3
+ class UnexpectedShellCommandExit < StandardError
4
+ include GLI::StandardException
5
+ attr_reader :command, :stdout, :stderr
6
+ def initialize(command,stdout,stderr)
7
+ @command = command
8
+ @stdout = stdout
9
+ @stderr = stderr
10
+ end
11
+
12
+ def exit_code
13
+ 126
14
+ end
15
+ end
16
+ end
@@ -6,63 +6,197 @@ module Bookingit
6
6
  class Renderer < Redcarpet::Render::HTML
7
7
  include FileUtils
8
8
 
9
- attr_accessor :headers
9
+ def initialize(config)
10
+ super()
11
+ options = config.rendering_config
12
+ additional_languages = Hash[(options[:languages] || {}).map { |ext_or_regexp,language|
13
+ if ext_or_regexp.kind_of? String
14
+ [/#{ext_or_regexp}$/,language]
15
+ else
16
+ [ext_or_regexp,language]
17
+ end
18
+ }]
19
+ @language_identifiers = EXTENSION_TO_LANGUAGE.merge(additional_languages)
20
+ @basedir = String(options[:basedir]).strip
21
+ @basedir = '.' if @basedir == ''
22
+ @stylesheets = Array(options[:stylesheets])
23
+ @theme = options[:theme] || "default"
24
+ @cachedir = options[:cache]
25
+ @config = config
26
+ @images = []
27
+ end
28
+
29
+ attr_accessor :headers, :stylesheets, :theme, :images
30
+
31
+ def current_chapter=(chapter)
32
+ @chapter = chapter
33
+ end
34
+
10
35
  def header(text,header_level,anchor)
11
36
  @headers[header_level] ||= []
12
37
  @headers[header_level] << text
13
- "<h#{header_level}>#{text}</h#{header_level}>"
38
+ if header_level == 2
39
+ @chapter.add_section(text,anchor)
40
+ end
41
+ "<a name='#{anchor}'></a><h#{header_level+1}>#{text}</h#{header_level+1}>"
42
+ end
43
+
44
+ def image(link, title, alt_text)
45
+ title = title.gsub(/'/,'"') if title
46
+ @images << link
47
+ "<img src='#{link}' alt='#{alt_text}' title='#{title}'>"
14
48
  end
15
49
 
16
50
  def doc_header
17
51
  @headers = {}
18
- ""
52
+ Views::HeaderView.new(@stylesheets,@theme,@config).render
53
+ end
54
+
55
+ def doc_footer
56
+ Views::FooterView.new(@chapter,@config).render
19
57
  end
20
58
 
21
59
  EXTENSION_TO_LANGUAGE = {
22
- '.rb' => 'ruby',
23
- '.html' => 'html',
24
- '.scala' => 'scala',
60
+ /\.rb$/ => 'ruby',
61
+ /\.html$/ => 'html',
62
+ /\.scala$/ => 'scala',
63
+ /Gemfile$/ => 'ruby',
25
64
  }
65
+
66
+ def identify_language(path)
67
+ @language_identifiers.select { |matcher,language|
68
+ path =~ matcher
69
+ }.values.first
70
+ end
71
+
72
+
26
73
  def block_code(code, language)
27
- if code.strip =~ /file:\/\/(.*)$/
28
- path = $1
29
- code = File.read(path)
30
- language = EXTENSION_TO_LANGUAGE.fetch(File.extname(path))
31
- elsif code.strip =~ /git:\/\/(.*)$/
32
- path = $1
33
- if path =~ /(^.*).git\/(.*)#([^#]+)$/
34
- repo_path = $1
35
- path_in_repo = $2
36
- reference = $3
37
- chdir repo_path do
38
- if reference =~ /^(.+)\.\.(.+)$/
39
- code = `git diff #{reference}`
40
- language = 'diff'
41
- else
42
- `git checkout #{reference} 2>&1`
43
- code = File.read(path_in_repo)
44
- `git checkout master 2>&1`
45
- language = EXTENSION_TO_LANGUAGE.fetch(File.extname(path_in_repo))
74
+ result = nil
75
+ filename = nil
76
+ chdir @basedir do
77
+ code,language,filename = CodeBlockInterpreter.new(code)
78
+ .when_file( &cache(:read_file))
79
+ .when_git_diff( &cache(:read_git_diff))
80
+ .when_shell_command_in_git(&cache(:run_shell_command_in_git))
81
+ .when_file_in_git( &cache(:read_file_in_git))
82
+ .when_shell_command( &cache(:run_shell_command))
83
+ .otherwise {
84
+ [code,language,nil]
85
+ }.result
86
+ end
87
+ Views::CodeView.new(code,filename,language,@config).render.strip
88
+ end
89
+
90
+ private
91
+
92
+ def cache(method_name)
93
+ ->(*args) {
94
+ if @cachedir && File.exist?(cached_filename(*args))
95
+ puts "Pulling from cache..."
96
+ lines = File.read(cached_filename(*args)).split(/\n/)
97
+ language = lines.shift
98
+ filename = lines.shift
99
+ [lines.join("\n") + "\n",language,filename]
100
+ else
101
+ code,language,filename = method(method_name).(*args)
102
+ if @cachedir
103
+ FileUtils.mkdir_p(@cachedir) unless File.exist?(@cachedir)
104
+ File.open(cached_filename(*args),'w') do |file|
105
+ file.puts language
106
+ file.puts filename
107
+ file.puts code
46
108
  end
109
+ puts "Cached output"
47
110
  end
111
+ [code,language,filename]
112
+ end
113
+ }
114
+ end
115
+
116
+ def cached_filename(*args)
117
+ args = args.map { |arg|
118
+ case arg
119
+ when ShellCommand
120
+ [arg.command,arg.expected_exit_status].join("_")
48
121
  else
49
- raise "You must provide a SHA1 or tagname: #{path}"
122
+ arg
50
123
  end
51
- elsif code.strip =~ /sh:\/\/(.+)#([^#]+)$/
52
- path = $1
53
- command = $2
54
- chdir path do
55
- output = `#{command}`
56
- code = "> #{command}\n#{output}"
57
- language = 'shell'
124
+ }
125
+ File.join(@cachedir,args.join('__').gsub(/[#\/\!\s><]/,'_'))
126
+ end
127
+
128
+ def at_version_in_git(reference,&block)
129
+ ShellCommand.new(command: "git checkout #{reference} 2>&1").run!
130
+ block.call
131
+ ShellCommand.new(command: "git checkout master 2>&1").run!
132
+ end
133
+
134
+ def capture_command_output(path,command,exit_type=:zero)
135
+ shell_command = ShellCommand.new(command: command,path: path) do |exit_status|
136
+ case exit_type
137
+ when :zero
138
+ exit_status == 0
139
+ when :nonzero
140
+ exit_status != 0
141
+ else
142
+ raise "unknown exit type #{exit_type}"
58
143
  end
59
144
  end
60
- css_class = if language.nil? || language.strip == ''
61
- ""
62
- else
63
- " class=\"language-#{language}\""
64
- end
65
- %{<pre><code#{css_class}>#{CGI.escapeHTML(code)}</code></pre>}
145
+ shell_command.run!
146
+ ["> #{command}\n#{shell_command.stdout}",'shell']
147
+ end
148
+
149
+ def read_file(path)
150
+ filename = path
151
+ [File.read(path),identify_language(path),filename]
152
+ end
153
+
154
+ def read_git_diff(path_in_repo,reference)
155
+ puts "Calculating git diff #{reference}"
156
+ filename = path_in_repo
157
+ shell_command = ShellCommand.new(command: "git diff #{reference} #{path_in_repo}")
158
+ shell_command.run!
159
+ [ shell_command.stdout, 'diff', filename ]
160
+ end
161
+
162
+ def run_shell_command_in_git(reference,shell_command)
163
+ code = nil
164
+ at_version_in_git(reference) do
165
+ shell_command.run!
166
+ code = "> #{shell_command.command}\n#{shell_command.stdout}"
167
+ end
168
+ [code,'shell']
169
+ end
170
+
171
+ def read_file_in_git(path_in_repo,reference)
172
+ puts "Getting file at #{reference}"
173
+ code = nil
174
+ filename = path_in_repo
175
+ at_version_in_git(reference) do
176
+ code = File.read(path_in_repo)
177
+ end
178
+ [code, identify_language(path_in_repo),filename]
179
+ end
180
+
181
+ def run_shell_command(shell_command)
182
+ shell_command.run!
183
+ ["> #{shell_command.command}\n#{shell_command.stdout}",'shell']
184
+ end
185
+
186
+ def css_class(language)
187
+ if language.nil? || language.strip == ''
188
+ ""
189
+ else
190
+ " class=\"language-#{language}\""
191
+ end
192
+ end
193
+
194
+ def filename_footer(filename)
195
+ if filename && filename.strip != ''
196
+ %{<footer><h1>#{filename}</h1></footer>}
197
+ else
198
+ ''
199
+ end
66
200
  end
67
201
  end
68
202
  end