hologram 0.6.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +7 -4
- data/Rakefile +5 -0
- data/hologram.gemspec +1 -1
- data/lib/hologram.rb +13 -445
- data/lib/hologram/display_message.rb +79 -0
- data/lib/hologram/doc_block_collection.rb +48 -0
- data/lib/hologram/doc_builder.rb +196 -0
- data/lib/hologram/doc_parser.rb +125 -0
- data/lib/hologram/document_block.rb +36 -0
- data/lib/hologram/template_variables.rb +21 -0
- data/lib/hologram/version.rb +1 -1
- data/lib/template/doc_assets/_header.html +7 -2
- data/lib/template/hologram_config.yml +3 -0
- data/spec/display_message_spec.rb +115 -0
- data/spec/doc_block_collection_spec.rb +80 -0
- data/spec/doc_builder_spec.rb +92 -0
- data/spec/doc_parser_spec.rb +89 -0
- data/spec/document_block_spec.rb +62 -0
- data/spec/fixtures/source/components/background/backgrounds.css +46 -0
- data/spec/fixtures/source/components/button/buttons.css +87 -0
- data/spec/fixtures/source/components/button/skin/buttonSkins.css +113 -0
- data/spec/fixtures/source/components/index.md +23 -0
- data/spec/fixtures/source/config.yml +17 -0
- data/spec/fixtures/source/extra/css/screen.css +1 -0
- data/spec/fixtures/source/templates/_footer.html +9 -0
- data/spec/fixtures/source/templates/_header.html +57 -0
- data/spec/fixtures/source/templates/static/css/doc.css +132 -0
- data/spec/fixtures/styleguide/base_css.html +170 -0
- data/spec/fixtures/styleguide/extra/css/screen.css +1 -0
- data/spec/fixtures/styleguide/index.html +84 -0
- data/spec/fixtures/styleguide/static/css/doc.css +132 -0
- data/spec/spec_helper.rb +7 -0
- 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
|