hologram 1.0.1 → 1.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.
@@ -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