hologram 0.6.0 → 1.0.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.
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