hologram 1.0.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -7,25 +7,15 @@ module Hologram
7
7
  end
8
8
 
9
9
  # this should throw an error if we have a match, but no yaml_match
10
- def add_doc_block(comment_block)
11
- yaml_match = /^\s*---\s(.*?)\s---$/m.match(comment_block)
12
- return unless yaml_match
13
-
14
- markdown = comment_block.sub(yaml_match[0], '')
15
-
16
- begin
17
- config = YAML::load(yaml_match[1])
18
- rescue
19
- DisplayMessage.error("Could not parse YAML:\n#{yaml_match[1]}")
20
- end
21
-
22
- if config['name'].nil?
23
- DisplayMessage.warning("Missing required name config value. This hologram comment will be skipped. \n #{config.inspect}")
24
- else
25
- doc_block = DocumentBlock.new(config, markdown)
10
+ def add_doc_block(comment_block, file_name)
11
+ doc_block = DocumentBlock.from_comment(comment_block)
12
+ return unless doc_block
13
+ if !doc_block.is_valid?
14
+ skip_block(doc_block, file_name)
15
+ return
26
16
  end
27
17
 
28
- @doc_blocks[doc_block.name] = doc_block if doc_block.is_valid?
18
+ @doc_blocks[doc_block.name] = doc_block
29
19
  end
30
20
 
31
21
  def create_nested_structure
@@ -35,14 +25,27 @@ module Hologram
35
25
  next if !doc_block.parent
36
26
 
37
27
  parent = @doc_blocks[doc_block.parent]
38
- parent.children[doc_block.name] = doc_block
39
- doc_block.parent = parent
40
- blocks_to_remove_from_top_level << doc_block.name
28
+ if parent.nil?
29
+ DisplayMessage.warning("Hologram comment refers to parent: #{doc_block.parent}, but no other hologram comment has name: #{doc_block.parent}, skipping." )
30
+ else
31
+ parent.children[doc_block.name] = doc_block
32
+ doc_block.parent = parent
33
+
34
+ if doc_block.categories.empty?
35
+ doc_block.categories = parent.categories
36
+ end
37
+
38
+ blocks_to_remove_from_top_level << doc_block.name
39
+ end
41
40
  end
42
41
 
43
42
  blocks_to_remove_from_top_level.each do |key|
44
43
  @doc_blocks.delete(key)
45
44
  end
46
45
  end
46
+
47
+ def skip_block(doc_block, file_name)
48
+ DisplayMessage.warning(doc_block.errors.join("\n") << " in #{file_name}. This hologram comment will be skipped.")
49
+ end
47
50
  end
48
51
  end
@@ -1,203 +1,208 @@
1
1
  module Hologram
2
2
  class DocBuilder
3
- attr_accessor :doc_blocks, :config, :pages
3
+ attr_accessor :source, :destination, :documentation_assets, :dependencies, :index, :base_path, :renderer, :doc_blocks, :pages, :config_yml
4
+ attr_reader :errors
5
+ attr :doc_assets_dir, :output_dir, :input_dir, :header_erb, :footer_erb
4
6
 
5
- def init(args)
6
- @pages = {}
7
+ def self.from_yaml(yaml_file)
7
8
 
8
- begin
9
- if args[0] == 'init' then
10
- if File.exists?("hologram_config.yml")
11
- DisplayMessage.warning("Cowardly refusing to overwrite existing hologram_config.yml")
12
- else
13
- FileUtils.cp_r INIT_TEMPLATE_FILES, Dir.pwd
14
- new_files = ["hologram_config.yml", "doc_assets/", "doc_assets/_header.html", "doc_assets/_footer.html"]
15
- DisplayMessage.created(new_files)
16
- end
17
- else
18
- begin
19
- config_file = args[0] ? args[0] : 'hologram_config.yml'
20
-
21
- begin
22
- @config = YAML::load_file(config_file)
23
- rescue SyntaxError => e
24
- DisplayMessage.error("Could not load config file, check the syntax or try 'hologram init' to get started")
25
- rescue
26
- DisplayMessage.error("Could not load config file, try 'hologram init' to get started")
27
- end
28
-
29
- if @config.is_a? Hash
30
-
31
- validate_config
32
-
33
- current_path = Dir.pwd
34
- base_path = Pathname.new(config_file)
35
- Dir.chdir(base_path.dirname)
36
-
37
- # the real work happens here.
38
- build_docs
39
-
40
- Dir.chdir(current_path)
41
- DisplayMessage.success("Build completed. (-: ")
42
- else
43
- DisplayMessage.error("Could not read config file, check the syntax or try 'hologram init' to get started")
44
- end
45
- rescue RuntimeError => e
46
- DisplayMessage.error("#{e}")
47
- end
48
- end
49
- end
50
- end
9
+ #Change dir so that our paths are relative to the config file
10
+ base_path = Pathname.new(yaml_file)
11
+ yaml_file = base_path.realpath.to_s
12
+ Dir.chdir(base_path.dirname)
51
13
 
14
+ config = YAML::load_file(yaml_file)
15
+ raise SyntaxError if !config.is_a? Hash
52
16
 
53
- private
54
- def build_docs
55
- # Create the output directory if it doesn't exist
56
- FileUtils.mkdir_p(config['destination']) unless File.directory?(config['destination'])
57
-
58
- begin
59
- input_directory = Pathname.new(config['source']).realpath
60
- rescue
61
- DisplayMessage.error("Can not read source directory (#{config['source'].inspect}), does it exist?")
62
- end
17
+ new(config.merge(
18
+ 'config_yml' => config,
19
+ 'base_path' => Pathname.new(yaml_file).dirname,
20
+ 'renderer' => Utils.get_markdown_renderer(config['custom_markdown'])
21
+ ))
63
22
 
64
- output_directory = Pathname.new(config['destination']).realpath
65
- doc_assets = Pathname.new(config['documentation_assets']).realpath unless !File.directory?(config['documentation_assets'])
23
+ rescue SyntaxError, ArgumentError, Psych::SyntaxError
24
+ raise SyntaxError, "Could not load config file, check the syntax or try 'hologram init' to get started"
25
+ end
66
26
 
67
- if doc_assets.nil?
68
- DisplayMessage.warning("Could not find documentation assets at #{config['documentation_assets']}")
27
+ def self.setup_dir
28
+ if File.exists?("hologram_config.yml")
29
+ DisplayMessage.warning("Cowardly refusing to overwrite existing hologram_config.yml")
30
+ return
69
31
  end
70
32
 
71
- doc_parser = DocParser.new(input_directory, config['index'])
72
- @pages, @categories = doc_parser.parse
33
+ FileUtils.cp_r INIT_TEMPLATE_FILES, Dir.pwd
34
+ new_files = ["hologram_config.yml", "doc_assets/", "doc_assets/_header.html", "doc_assets/_footer.html"]
35
+ DisplayMessage.created(new_files)
36
+ end
73
37
 
74
- if config['index'] && !@pages.has_key?(config['index'] + '.html')
75
- DisplayMessage.warning("Could not generate index.html, there was no content generated for the category #{config['index']}.")
76
- end
38
+ def initialize(options)
39
+ @pages = {}
40
+ @errors = []
41
+ @dependencies = options.fetch('dependencies', [])
42
+ @index = options['index']
43
+ @base_path = options.fetch('base_path', Dir.pwd)
44
+ @renderer = options.fetch('renderer', MarkdownRenderer)
45
+ @source = options['source']
46
+ @destination = options['destination']
47
+ @documentation_assets = options['documentation_assets']
48
+ @config_yml = options['config_yml']
49
+ end
77
50
 
78
- write_docs(output_directory, doc_assets)
79
-
80
- # Copy over dependencies
81
- if config['dependencies']
82
- config['dependencies'].each do |dir|
83
- begin
84
- dirpath = Pathname.new(dir).realpath
85
- if File.directory?("#{dir}")
86
- `rm -rf #{output_directory}/#{dirpath.basename}`
87
- `cp -R #{dirpath} #{output_directory}/#{dirpath.basename}`
88
- end
89
- rescue
90
- DisplayMessage.warning("Could not copy dependency: #{dir}")
91
- end
92
- end
93
- end
51
+ def build
52
+ set_dirs
53
+ return false if !is_valid?
94
54
 
95
- if !doc_assets.nil?
96
- Dir.foreach(doc_assets) do |item|
97
- # ignore . and .. directories and files that start with
98
- # underscore
99
- next if item == '.' or item == '..' or item.start_with?('_')
100
- `rm -rf #{output_directory}/#{item}`
101
- `cp -R #{doc_assets}/#{item} #{output_directory}/#{item}`
102
- end
55
+ set_header_footer
56
+ current_path = Dir.pwd
57
+ Dir.chdir(base_path)
58
+ # Create the output directory if it doesn't exist
59
+ if !output_dir
60
+ FileUtils.mkdir_p(destination)
61
+ set_dirs #need to reset output_dir post-creation for build_docs.
103
62
  end
63
+ # the real work happens here.
64
+ build_docs
65
+ Dir.chdir(current_path)
66
+ DisplayMessage.success("Build completed. (-: ")
67
+ true
104
68
  end
105
69
 
70
+ def is_valid?
71
+ errors.clear
72
+ set_dirs
73
+ errors << "No source directory specified in the config file" if !source
74
+ errors << "No destination directory specified in the config" if !destination
75
+ errors << "No documentation assets directory specified" if !documentation_assets
76
+ errors << "Can not read source directory (#{source}), does it exist?" if source && !input_dir
77
+ errors.empty?
78
+ end
106
79
 
107
- def write_docs(output_directory, doc_assets)
108
- # load the markdown renderer we are going to use
109
- renderer = get_markdown_renderer
80
+ private
110
81
 
111
- if File.exists?("#{doc_assets}/_header.html")
112
- header_erb = ERB.new(File.read("#{doc_assets}/_header.html"))
113
- elsif File.exists?("#{doc_assets}/header.html")
114
- header_erb = ERB.new(File.read("#{doc_assets}/header.html"))
115
- else
116
- header_erb = nil
117
- DisplayMessage.warning("No _header.html found in documentation assets. Without this your css/header will not be included on the generated pages.")
118
- end
82
+ def set_dirs
83
+ @output_dir = real_path(destination)
84
+ @doc_assets_dir = real_path(documentation_assets)
85
+ @input_dir = real_path(source)
86
+ end
119
87
 
120
- if File.exists?("#{doc_assets}/_footer.html")
121
- footer_erb = ERB.new(File.read("#{doc_assets}/_footer.html"))
122
- elsif File.exists?("#{doc_assets}/footer.html")
123
- footer_erb = ERB.new(File.read("#{doc_assets}/footer.html"))
124
- else
125
- footer_erb = nil
126
- DisplayMessage.warning("No _footer.html found in documentation assets. This might be okay to ignore...")
127
- end
88
+ def real_path(dir)
89
+ return if !File.directory?(String(dir))
90
+ Pathname.new(dir).realpath
91
+ end
128
92
 
129
- tpl_vars = TemplateVariables.new({:categories => @categories})
130
- #generate html from markdown
131
- @pages.each do |file_name, page|
132
- fh = get_fh(output_directory, file_name)
93
+ def build_docs
94
+ doc_parser = DocParser.new(input_dir, index)
95
+ @pages, @categories = doc_parser.parse
133
96
 
134
- title = page[:blocks].empty? ? "" : page[:blocks][0][:category]
97
+ if index && !@pages.has_key?(index + '.html')
98
+ DisplayMessage.warning("Could not generate index.html, there was no content generated for the category #{index}.")
99
+ end
135
100
 
136
- tpl_vars.set_args({:title =>title, :file_name => file_name, :blocks => page[:blocks]})
101
+ warn_missing_doc_assets
102
+ write_docs
103
+ copy_dependencies
104
+ copy_assets
105
+ end
137
106
 
138
- # generate doc nav html
139
- unless header_erb.nil?
140
- fh.write(header_erb.result(tpl_vars.get_binding))
141
- end
107
+ def copy_assets
108
+ return unless doc_assets_dir
109
+ Dir.foreach(doc_assets_dir) do |item|
110
+ # ignore . and .. directories and files that start with
111
+ # underscore
112
+ next if item == '.' or item == '..' or item.start_with?('_')
113
+ `rm -rf #{output_dir}/#{item}`
114
+ `cp -R #{doc_assets_dir}/#{item} #{output_dir}/#{item}`
115
+ end
116
+ end
142
117
 
143
- # write the docs
118
+ def copy_dependencies
119
+ dependencies.each do |dir|
144
120
  begin
145
- fh.write(renderer.render(page[:md]))
146
- rescue Exception => e
147
- DisplayMessage.error(e.message)
148
- end
149
-
150
- # write the footer
151
- unless footer_erb.nil?
152
- fh.write(footer_erb.result(tpl_vars.get_binding))
121
+ dirpath = Pathname.new(dir).realpath
122
+ if File.directory?("#{dir}")
123
+ `rm -rf #{output_dir}/#{dirpath.basename}`
124
+ `cp -R #{dirpath} #{output_dir}/#{dirpath.basename}`
125
+ end
126
+ rescue
127
+ DisplayMessage.warning("Could not copy dependency: #{dir}")
153
128
  end
154
-
155
- fh.close()
156
129
  end
157
130
  end
158
131
 
132
+ def write_docs
133
+ markdown = Redcarpet::Markdown.new(renderer, { :fenced_code_blocks => true, :tables => true })
134
+ tpl_vars = TemplateVariables.new({:categories => @categories, :config => @config_yml, :pages => @pages})
135
+ #generate html from markdown
136
+ @pages.each do |file_name, page|
137
+ if file_name.nil?
138
+ raise NoCategoryError
139
+ else
140
+ if page[:blocks] && page[:blocks].empty?
141
+ title = ''
142
+ else
143
+ title, _ = @categories.rassoc(file_name)
144
+ end
159
145
 
160
- def get_markdown_renderer
161
- if config['custom_markdown'].nil?
162
- renderer = Redcarpet::Markdown.new(HologramMarkdownRenderer, { :fenced_code_blocks => true, :tables => true })
163
- else
164
- begin
165
- load config['custom_markdown']
166
- renderer_class = File.basename(config['custom_markdown'], '.rb').split(/_/).map(&:capitalize).join
167
- DisplayMessage.info("Custom markdown renderer #{renderer_class} loaded.")
168
- renderer = Redcarpet::Markdown.new(Module.const_get(renderer_class), { :fenced_code_blocks => true, :tables => true })
169
- rescue LoadError => e
170
- DisplayMessage.error("Could not load #{config['custom_markdown']}.")
171
- rescue NameError => e
172
- DisplayMessage.error("Class #{renderer_class} not found in #{config['custom_markdown']}.")
146
+ tpl_vars.set_args({:title => title, :file_name => file_name, :blocks => page[:blocks]})
147
+ if page.has_key?(:erb)
148
+ write_erb(file_name, page[:erb], tpl_vars.get_binding)
149
+ else
150
+ write_page(file_name, markdown.render(page[:md]), tpl_vars.get_binding)
151
+ end
173
152
  end
174
153
  end
175
- renderer
176
154
  end
177
155
 
156
+ def write_erb(file_name, content, binding)
157
+ fh = get_fh(output_dir, file_name)
158
+ erb = ERB.new(content)
159
+ fh.write(erb.result(binding))
160
+ ensure
161
+ fh.close
162
+ end
178
163
 
179
- def validate_config
180
- unless @config.key?('source')
181
- DisplayMessage.error("No source directory specified in the config file")
182
- end
164
+ def write_page(file_name, body, binding)
165
+ fh = get_fh(output_dir, file_name)
166
+ fh.write(header_erb.result(binding)) if header_erb
167
+ fh.write(body)
168
+ fh.write(footer_erb.result(binding)) if footer_erb
169
+ ensure
170
+ fh.close
171
+ end
183
172
 
184
- unless @config.key?('destination')
185
- DisplayMessage.error("No destination directory specified in the config")
173
+ def set_header_footer
174
+ # load the markdown renderer we are going to use
175
+
176
+ if File.exists?("#{doc_assets_dir}/_header.html")
177
+ @header_erb = ERB.new(File.read("#{doc_assets_dir}/_header.html"))
178
+ elsif File.exists?("#{doc_assets_dir}/header.html")
179
+ @header_erb = ERB.new(File.read("#{doc_assets_dir}/header.html"))
180
+ else
181
+ @header_erb = nil
182
+ DisplayMessage.warning("No _header.html found in documentation assets. Without this your css/header will not be included on the generated pages.")
186
183
  end
187
184
 
188
- unless @config.key?('documentation_assets')
189
- DisplayMessage.error("No documentation assets directory specified")
185
+ if File.exists?("#{doc_assets_dir}/_footer.html")
186
+ @footer_erb = ERB.new(File.read("#{doc_assets_dir}/_footer.html"))
187
+ elsif File.exists?("#{doc_assets_dir}/footer.html")
188
+ @footer_erb = ERB.new(File.read("#{doc_assets_dir}/footer.html"))
189
+ else
190
+ @footer_erb = nil
191
+ DisplayMessage.warning("No _footer.html found in documentation assets. This might be okay to ignore...")
190
192
  end
191
193
  end
192
194
 
193
-
194
195
  def get_file_name(str)
195
- str = str.gsub(' ', '_').downcase + '.html'
196
+ str.gsub(' ', '_').downcase + '.html'
196
197
  end
197
198
 
199
+ def get_fh(output_dir, output_file)
200
+ File.open("#{output_dir}/#{output_file}", 'w')
201
+ end
198
202
 
199
- def get_fh(output_directory, output_file)
200
- File.open("#{output_directory}/#{output_file}", 'w')
203
+ def warn_missing_doc_assets
204
+ return if doc_assets_dir
205
+ DisplayMessage.warning("Could not find documentation assets at #{documentation_assets}")
201
206
  end
202
207
  end
203
208
  end
@@ -1,13 +1,13 @@
1
1
  module Hologram
2
2
  class DocParser
3
- SUPPORTED_EXTENSIONS = ['.css', '.scss', '.less', '.sass', '.styl', '.js', '.md', '.markdown' ]
3
+ SUPPORTED_EXTENSIONS = ['.css', '.scss', '.less', '.sass', '.styl', '.js', '.md', '.markdown', '.erb' ]
4
4
  attr_accessor :source_path, :pages, :doc_blocks
5
5
 
6
6
  def initialize(source_path, index_name = nil)
7
7
  @source_path = source_path
8
8
  @index_name = index_name
9
9
  @pages = {}
10
- @categories = {}
10
+ @output_files_by_category = {}
11
11
  end
12
12
 
13
13
  def parse
@@ -38,7 +38,7 @@ module Hologram
38
38
  end
39
39
  end
40
40
 
41
- return @pages, @categories
41
+ return @pages, @output_files_by_category
42
42
  end
43
43
 
44
44
  private
@@ -52,7 +52,7 @@ module Hologram
52
52
  directories.each do |directory|
53
53
  # filter and sort the files in our directory
54
54
  files = []
55
- Dir.foreach(directory).select{ |file| is_supported_file_type?(file) }.each do |file|
55
+ Dir.foreach(directory).select{ |file| is_supported_file_type?("#{directory}/#{file}") }.each do |file|
56
56
  files << file
57
57
  end
58
58
  files.sort!
@@ -65,6 +65,8 @@ module Hologram
65
65
  files.each do |input_file|
66
66
  if input_file.end_with?('md')
67
67
  @pages[File.basename(input_file, '.md') + '.html'] = {:md => File.read("#{directory}/#{input_file}"), :blocks => []}
68
+ elsif input_file.end_with?('erb')
69
+ @pages[File.basename(input_file, '.erb')] = {:erb => File.read("#{directory}/#{input_file}")}
68
70
  else
69
71
  process_file("#{directory}/#{input_file}", doc_block_collection)
70
72
  end
@@ -84,42 +86,41 @@ module Hologram
84
86
  return unless hologram_comments
85
87
 
86
88
  hologram_comments.each do |comment_block|
87
- doc_block_collection.add_doc_block(comment_block[0])
89
+ doc_block_collection.add_doc_block(comment_block[0], file)
88
90
  end
89
91
  end
90
92
 
91
93
  def build_output(doc_blocks, output_file = nil, depth = 1)
92
- doc_blocks.sort.map do |key, doc_block|
93
-
94
- # if the doc_block has a category set then use that, this will be
95
- # true of all top level doc_blocks. The output file they set will then
96
- # be passed into the recursive call for adding children to the output
97
- if doc_block.category
98
- output_file = get_file_name(doc_block.category)
99
- @categories[doc_block.category] = output_file
100
- end
101
-
102
- if !@pages.has_key?(output_file)
103
- @pages[output_file] = {:md => "", :blocks => []}
104
- end
94
+ return if doc_blocks.nil?
105
95
 
106
- @pages[output_file][:blocks].push(doc_block.get_hash)
107
- @pages[output_file][:md] << doc_block.markdown_with_heading(depth)
96
+ # sort elements in alphabetical order ignoring case
97
+ doc_blocks.sort{|a, b| a[0].downcase<=>b[0].downcase}.map do |key, doc_block|
108
98
 
109
- if doc_block.children
110
- depth += 1
111
- build_output(doc_block.children, output_file, depth)
112
- depth -= 1
99
+ #doc_blocks are guaranteed to always have categories (top-level have categories, children get parent categories if empty).
100
+ doc_block.categories.each do |category|
101
+ output_file = get_file_name(category)
102
+ @output_files_by_category[category] = output_file
103
+ add_doc_block_to_page(depth, doc_block, output_file)
113
104
  end
105
+ build_output(doc_block.children, nil, depth + 1)
114
106
  end
115
107
  end
116
108
 
117
109
  def is_supported_file_type?(file)
118
- SUPPORTED_EXTENSIONS.include?(File.extname(file))
110
+ SUPPORTED_EXTENSIONS.include?(File.extname(file)) and !Dir.exists?(file)
119
111
  end
120
112
 
121
113
  def get_file_name(str)
122
114
  str = str.gsub(' ', '_').downcase + '.html'
123
115
  end
116
+
117
+ def add_doc_block_to_page(depth, doc_block, output_file)
118
+ if !@pages.has_key?(output_file)
119
+ @pages[output_file] = {:md => "", :blocks => []}
120
+ end
121
+
122
+ @pages[output_file][:blocks].push(doc_block.get_hash)
123
+ @pages[output_file][:md] << doc_block.markdown_with_heading(depth)
124
+ end
124
125
  end
125
126
  end