bookingit 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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