hologram 0.6.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +7 -4
  3. data/Rakefile +5 -0
  4. data/hologram.gemspec +1 -1
  5. data/lib/hologram.rb +13 -445
  6. data/lib/hologram/display_message.rb +79 -0
  7. data/lib/hologram/doc_block_collection.rb +48 -0
  8. data/lib/hologram/doc_builder.rb +196 -0
  9. data/lib/hologram/doc_parser.rb +125 -0
  10. data/lib/hologram/document_block.rb +36 -0
  11. data/lib/hologram/template_variables.rb +21 -0
  12. data/lib/hologram/version.rb +1 -1
  13. data/lib/template/doc_assets/_header.html +7 -2
  14. data/lib/template/hologram_config.yml +3 -0
  15. data/spec/display_message_spec.rb +115 -0
  16. data/spec/doc_block_collection_spec.rb +80 -0
  17. data/spec/doc_builder_spec.rb +92 -0
  18. data/spec/doc_parser_spec.rb +89 -0
  19. data/spec/document_block_spec.rb +62 -0
  20. data/spec/fixtures/source/components/background/backgrounds.css +46 -0
  21. data/spec/fixtures/source/components/button/buttons.css +87 -0
  22. data/spec/fixtures/source/components/button/skin/buttonSkins.css +113 -0
  23. data/spec/fixtures/source/components/index.md +23 -0
  24. data/spec/fixtures/source/config.yml +17 -0
  25. data/spec/fixtures/source/extra/css/screen.css +1 -0
  26. data/spec/fixtures/source/templates/_footer.html +9 -0
  27. data/spec/fixtures/source/templates/_header.html +57 -0
  28. data/spec/fixtures/source/templates/static/css/doc.css +132 -0
  29. data/spec/fixtures/styleguide/base_css.html +170 -0
  30. data/spec/fixtures/styleguide/extra/css/screen.css +1 -0
  31. data/spec/fixtures/styleguide/index.html +84 -0
  32. data/spec/fixtures/styleguide/static/css/doc.css +132 -0
  33. data/spec/spec_helper.rb +7 -0
  34. metadata +66 -22
@@ -0,0 +1,79 @@
1
+ module Hologram
2
+ module DisplayMessage
3
+ @@quiet = false
4
+
5
+ def self.quiet!
6
+ @@quiet = true
7
+ return self
8
+ end
9
+
10
+ def self.show!
11
+ @@quiet = false
12
+ return self
13
+ end
14
+
15
+ def self.quiet?
16
+ @@quiet
17
+ end
18
+
19
+ def self.puts(str)
20
+ return if quiet?
21
+ super(str)
22
+ end
23
+
24
+ def self.info(message)
25
+ puts message
26
+ end
27
+
28
+ def self.error(message)
29
+ if RUBY_VERSION.to_f > 1.8 then
30
+ puts angry_table_flipper + red(" Build not complete.")
31
+ else
32
+ puts red("Build not complete.")
33
+ end
34
+
35
+ puts " #{message}"
36
+ exit 1
37
+ end
38
+
39
+ def self.created(files)
40
+ puts "Created the following files and directories:"
41
+ files.each do |file_name|
42
+ puts " #{file_name}"
43
+ end
44
+ end
45
+
46
+ def self.warning(message)
47
+ puts yellow("Warning: #{message}")
48
+ end
49
+
50
+ def self.success(message)
51
+ puts green(message)
52
+ end
53
+
54
+ def self.angry_table_flipper
55
+ green("(\u{256F}\u{00B0}\u{25A1}\u{00B0}\u{FF09}\u{256F}") + red("\u{FE35} \u{253B}\u{2501}\u{253B} ")
56
+ end
57
+
58
+ # colorization
59
+ def self.colorize(color_code, str)
60
+ "\e[#{color_code}m#{str}\e[0m"
61
+ end
62
+
63
+ def self.red(str)
64
+ colorize(31, str)
65
+ end
66
+
67
+ def self.green(str)
68
+ colorize(32, str)
69
+ end
70
+
71
+ def self.yellow(str)
72
+ colorize(33, str)
73
+ end
74
+
75
+ def self.pink(str)
76
+ colorize(35, str)
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,48 @@
1
+ module Hologram
2
+ class DocBlockCollection
3
+ attr_accessor :doc_blocks
4
+
5
+ def initialize
6
+ @doc_blocks = {}
7
+ end
8
+
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)
26
+ end
27
+
28
+ @doc_blocks[doc_block.name] = doc_block if doc_block.is_valid?
29
+ end
30
+
31
+ def create_nested_structure
32
+ blocks_to_remove_from_top_level = []
33
+ @doc_blocks.each do |key, doc_block|
34
+ # don't do anything to top level doc_blocks
35
+ next if !doc_block.parent
36
+
37
+ 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
41
+ end
42
+
43
+ blocks_to_remove_from_top_level.each do |key|
44
+ @doc_blocks.delete(key)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,196 @@
1
+ module Hologram
2
+ class DocBuilder
3
+ attr_accessor :doc_blocks, :config, :pages
4
+
5
+ def init(args)
6
+ @pages = {}
7
+
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
24
+ DisplayMessage.error("Could not load config file, try 'hologram init' to get started")
25
+ end
26
+
27
+ validate_config
28
+
29
+ current_path = Dir.pwd
30
+ base_path = Pathname.new(config_file)
31
+ Dir.chdir(base_path.dirname)
32
+
33
+ # the real work happens here.
34
+ build_docs
35
+
36
+ Dir.chdir(current_path)
37
+ DisplayMessage.success("Build completed. (-: ")
38
+ rescue RuntimeError => e
39
+ DisplayMessage.error("#{e}")
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+
46
+ private
47
+ def build_docs
48
+ # Create the output directory if it doesn't exist
49
+ FileUtils.mkdir_p(config['destination']) unless File.directory?(config['destination'])
50
+
51
+ begin
52
+ input_directory = Pathname.new(config['source']).realpath
53
+ rescue
54
+ DisplayMessage.error("Can not read source directory (#{config['source'].inspect}), does it exist?")
55
+ end
56
+
57
+ output_directory = Pathname.new(config['destination']).realpath
58
+ doc_assets = Pathname.new(config['documentation_assets']).realpath unless !File.directory?(config['documentation_assets'])
59
+
60
+ if doc_assets.nil?
61
+ DisplayMessage.warning("Could not find documentation assets at #{config['documentation_assets']}")
62
+ end
63
+
64
+ doc_parser = DocParser.new(input_directory, config['index'])
65
+ @pages, @categories = doc_parser.parse
66
+
67
+ if config['index'] && !@pages.has_key?(config['index'] + '.html')
68
+ DisplayMessage.warning("Could not generate index.html, there was no content generated for the category #{config['index']}.")
69
+ end
70
+
71
+ write_docs(output_directory, doc_assets)
72
+
73
+ # Copy over dependencies
74
+ if config['dependencies']
75
+ config['dependencies'].each do |dir|
76
+ begin
77
+ dirpath = Pathname.new(dir).realpath
78
+ if File.directory?("#{dir}")
79
+ `rm -rf #{output_directory}/#{dirpath.basename}`
80
+ `cp -R #{dirpath} #{output_directory}/#{dirpath.basename}`
81
+ end
82
+ rescue
83
+ DisplayMessage.warning("Could not copy dependency: #{dir}")
84
+ end
85
+ end
86
+ end
87
+
88
+ if !doc_assets.nil?
89
+ Dir.foreach(doc_assets) do |item|
90
+ # ignore . and .. directories and files that start with
91
+ # underscore
92
+ next if item == '.' or item == '..' or item.start_with?('_')
93
+ `rm -rf #{output_directory}/#{item}`
94
+ `cp -R #{doc_assets}/#{item} #{output_directory}/#{item}`
95
+ end
96
+ end
97
+ end
98
+
99
+
100
+ def write_docs(output_directory, doc_assets)
101
+ # load the markdown renderer we are going to use
102
+ renderer = get_markdown_renderer
103
+
104
+ if File.exists?("#{doc_assets}/_header.html")
105
+ header_erb = ERB.new(File.read("#{doc_assets}/_header.html"))
106
+ elsif File.exists?("#{doc_assets}/header.html")
107
+ header_erb = ERB.new(File.read("#{doc_assets}/header.html"))
108
+ else
109
+ header_erb = nil
110
+ DisplayMessage.warning("No _header.html found in documentation assets. Without this your css/header will not be included on the generated pages.")
111
+ end
112
+
113
+ if File.exists?("#{doc_assets}/_footer.html")
114
+ footer_erb = ERB.new(File.read("#{doc_assets}/_footer.html"))
115
+ elsif File.exists?("#{doc_assets}/footer.html")
116
+ footer_erb = ERB.new(File.read("#{doc_assets}/footer.html"))
117
+ else
118
+ footer_erb = nil
119
+ DisplayMessage.warning("No _footer.html found in documentation assets. This might be okay to ignore...")
120
+ end
121
+
122
+ tpl_vars = TemplateVariables.new({:categories => @categories})
123
+ #generate html from markdown
124
+ @pages.each do |file_name, page|
125
+ fh = get_fh(output_directory, file_name)
126
+
127
+ title = page[:blocks].empty? ? "" : page[:blocks][0][:category]
128
+
129
+ tpl_vars.set_args({:title =>title, :file_name => file_name, :blocks => page[:blocks]})
130
+
131
+ # generate doc nav html
132
+ unless header_erb.nil?
133
+ fh.write(header_erb.result(tpl_vars.get_binding))
134
+ end
135
+
136
+ # write the docs
137
+ begin
138
+ fh.write(renderer.render(page[:md]))
139
+ rescue Exception => e
140
+ DisplayMessage.error(e.message)
141
+ end
142
+
143
+ # write the footer
144
+ unless footer_erb.nil?
145
+ fh.write(footer_erb.result(tpl_vars.get_binding))
146
+ end
147
+
148
+ fh.close()
149
+ end
150
+ end
151
+
152
+
153
+ def get_markdown_renderer
154
+ if config['custom_markdown'].nil?
155
+ renderer = Redcarpet::Markdown.new(HologramMarkdownRenderer, { :fenced_code_blocks => true, :tables => true })
156
+ else
157
+ begin
158
+ load config['custom_markdown']
159
+ renderer_class = File.basename(config['custom_markdown'], '.rb').split(/_/).map(&:capitalize).join
160
+ DisplayMessage.info("Custom markdown renderer #{renderer_class} loaded.")
161
+ renderer = Redcarpet::Markdown.new(Module.const_get(renderer_class), { :fenced_code_blocks => true, :tables => true })
162
+ rescue LoadError => e
163
+ DisplayMessage.error("Could not load #{config['custom_markdown']}.")
164
+ rescue NameError => e
165
+ DisplayMessage.error("Class #{renderer_class} not found in #{config['custom_markdown']}.")
166
+ end
167
+ end
168
+ renderer
169
+ end
170
+
171
+
172
+ def validate_config
173
+ unless @config.key?('source')
174
+ DisplayMessage.error("No source directory specified in the config file")
175
+ end
176
+
177
+ unless @config.key?('destination')
178
+ DisplayMessage.error("No destination directory specified in the config")
179
+ end
180
+
181
+ unless @config.key?('documentation_assets')
182
+ DisplayMessage.error("No documentation assets directory specified")
183
+ end
184
+ end
185
+
186
+
187
+ def get_file_name(str)
188
+ str = str.gsub(' ', '_').downcase + '.html'
189
+ end
190
+
191
+
192
+ def get_fh(output_directory, output_file)
193
+ File.open("#{output_directory}/#{output_file}", 'w')
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,125 @@
1
+ module Hologram
2
+ class DocParser
3
+ SUPPORTED_EXTENSIONS = ['.css', '.scss', '.less', '.sass', '.styl', '.js', '.md', '.markdown' ]
4
+ attr_accessor :source_path, :pages, :doc_blocks
5
+
6
+ def initialize(source_path, index_name = nil)
7
+ @source_path = source_path
8
+ @index_name = index_name
9
+ @pages = {}
10
+ @categories = {}
11
+ end
12
+
13
+ def parse
14
+ # recursively traverse our directory structure looking for files that
15
+ # match our "parseable" file types. Open those files pulling out any
16
+ # comments matching the hologram doc style /*doc */ and create DocBlock
17
+ # objects from those comments, then add those to a collection object which
18
+ # is then returned.
19
+ doc_block_collection = process_dir(source_path)
20
+
21
+ # doc blocks can define parent/child relationships that will nest their
22
+ # documentation appropriately. we can't put everything into that structure
23
+ # on our first pass through because there is no guarantee we'll parse files
24
+ # in the correct order. This step takes the full collection and creates the
25
+ # proper structure.
26
+ doc_block_collection.create_nested_structure
27
+
28
+
29
+ # hand off our properly nested collection to the output generator
30
+ build_output(doc_block_collection.doc_blocks)
31
+
32
+ # if we have an index category defined in our config copy that
33
+ # page to index.html
34
+ if @index_name
35
+ name = @index_name + '.html'
36
+ if @pages.has_key?(name)
37
+ @pages['index.html'] = @pages[name]
38
+ end
39
+ end
40
+
41
+ return @pages, @categories
42
+ end
43
+
44
+ private
45
+
46
+ def process_dir(base_directory)
47
+ #get all directories in our library folder
48
+ doc_block_collection = DocBlockCollection.new
49
+ directories = Dir.glob("#{base_directory}/**/*/")
50
+ directories.unshift(base_directory)
51
+
52
+ directories.each do |directory|
53
+ # filter and sort the files in our directory
54
+ files = []
55
+ Dir.foreach(directory).select{ |file| is_supported_file_type?(file) }.each do |file|
56
+ files << file
57
+ end
58
+ files.sort!
59
+ process_files(files, directory, doc_block_collection)
60
+ end
61
+ doc_block_collection
62
+ end
63
+
64
+ def process_files(files, directory, doc_block_collection)
65
+ files.each do |input_file|
66
+ if input_file.end_with?('md')
67
+ @pages[File.basename(input_file, '.md') + '.html'] = {:md => File.read("#{directory}/#{input_file}"), :blocks => []}
68
+ else
69
+ process_file("#{directory}/#{input_file}", doc_block_collection)
70
+ end
71
+ end
72
+ end
73
+
74
+ def process_file(file, doc_block_collection)
75
+ file_str = File.read(file)
76
+ # get any comment blocks that match the patterns:
77
+ # .sass: //doc (follow by other lines proceeded by a space)
78
+ # other types: /*doc ... */
79
+ if file.end_with?('.sass')
80
+ hologram_comments = file_str.scan(/\s*\/\/doc\s*((( [^\n]*\n)|\n)+)/)
81
+ else
82
+ hologram_comments = file_str.scan(/^\s*\/\*doc(.*?)\*\//m)
83
+ end
84
+ return unless hologram_comments
85
+
86
+ hologram_comments.each do |comment_block|
87
+ doc_block_collection.add_doc_block(comment_block[0])
88
+ end
89
+ end
90
+
91
+ 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
105
+
106
+ @pages[output_file][:blocks].push(doc_block.get_hash)
107
+ @pages[output_file][:md] << doc_block.markdown_with_heading(depth)
108
+
109
+ if doc_block.children
110
+ depth += 1
111
+ build_output(doc_block.children, output_file, depth)
112
+ depth -= 1
113
+ end
114
+ end
115
+ end
116
+
117
+ def is_supported_file_type?(file)
118
+ SUPPORTED_EXTENSIONS.include?(File.extname(file))
119
+ end
120
+
121
+ def get_file_name(str)
122
+ str = str.gsub(' ', '_').downcase + '.html'
123
+ end
124
+ end
125
+ end