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.
- 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
|