├── spec ├── fixtures │ ├── renderer │ │ ├── invalid_renderer.rb │ │ └── valid_renderer.rb │ ├── source │ │ ├── colors │ │ │ └── colors.css │ │ ├── components │ │ │ ├── jekyll.md │ │ │ ├── bem.md │ │ │ ├── index.md │ │ │ ├── background │ │ │ │ └── backgrounds.css │ │ │ └── button │ │ │ │ ├── buttons.css │ │ │ │ └── skin │ │ │ │ └── buttonSkins.css │ │ ├── templates │ │ │ ├── _footer.html │ │ │ ├── _header.html │ │ │ └── static │ │ │ │ └── css │ │ │ │ └── doc.css │ │ ├── config.yml │ │ ├── config_multi_source.yml │ │ └── extra │ │ │ └── css │ │ │ └── screen.css │ └── styleguide │ │ ├── jekyll.html │ │ ├── code.html │ │ ├── static │ │ └── css │ │ │ └── doc.css │ │ ├── index.html │ │ ├── base_css.html │ │ └── extra │ │ └── css │ │ └── screen.css ├── hologram_markdown_renderer_spec.rb ├── spec_helper.rb ├── code_example_renderer │ ├── example_spec.rb │ ├── template_spec.rb │ └── factory_spec.rb ├── cli_spec.rb ├── utils_spec.rb ├── link_helper_spec.rb ├── markdown_renderer_spec.rb ├── doc_block_collection_spec.rb ├── display_message_spec.rb ├── document_block_spec.rb ├── doc_parser_spec.rb ├── doc_builder_spec.rb └── block_code_renderer_spec.rb ├── Gemfile ├── bin └── hologram ├── Rakefile ├── .travis.yml ├── lib ├── hologram │ ├── code_example_renderer │ │ ├── renderers │ │ │ ├── js_renderer.rb │ │ │ ├── jsx_renderer.rb │ │ │ ├── html_renderer.rb │ │ │ ├── haml_renderer.rb │ │ │ ├── slim_renderer.rb │ │ │ └── react_renderer.rb │ │ ├── example.rb │ │ ├── template.rb │ │ └── factory.rb │ ├── errors.rb │ ├── link_helper.rb │ ├── template_variables.rb │ ├── utils.rb │ ├── block_code_renderer.rb │ ├── plugins.rb │ ├── version.rb │ ├── cli.rb │ ├── doc_block_collection.rb │ ├── markdown_renderer.rb │ ├── display_message.rb │ ├── code_example_renderer.rb │ ├── document_block.rb │ ├── plugin.rb │ ├── doc_parser.rb │ └── doc_builder.rb ├── template │ ├── code_example_templates │ │ ├── js_example_template.html.erb │ │ ├── jsx_example_template.html.erb │ │ ├── markup_example_template.html.erb │ │ └── markup_table_template.html.erb │ ├── doc_assets │ │ ├── _footer.html │ │ └── _header.html │ └── hologram_config.yml └── hologram.rb ├── .gitignore ├── hologram.gemspec ├── LICENSE.txt ├── example_markdown_renderer.rb.example ├── CHANGELOG.md └── README.md /spec/fixtures/renderer/invalid_renderer.rb: -------------------------------------------------------------------------------- 1 | class FooBar < Redcarpet::Render::HTML 2 | end 3 | -------------------------------------------------------------------------------- /spec/fixtures/renderer/valid_renderer.rb: -------------------------------------------------------------------------------- 1 | class ValidRenderer < Redcarpet::Render::HTML 2 | end 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in hologram.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /bin/hologram: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'hologram' 3 | require 'hologram/cli' 4 | 5 | cli = Hologram::CLI.new(ARGV) 6 | cli.run 7 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | script: "bundle exec rake spec" 2 | 3 | rvm: 4 | - 1.9.3 5 | - 2.0.0 6 | - 2.1 7 | 8 | notifications: 9 | email: false 10 | -------------------------------------------------------------------------------- /spec/fixtures/source/colors/colors.css: -------------------------------------------------------------------------------- 1 | /*doc 2 | --- 3 | title: Colors 4 | name: colors 5 | category: Base CSS 6 | --- 7 | 8 | Base colors 9 | 10 | 11 | */ 12 | -------------------------------------------------------------------------------- /spec/fixtures/source/components/jekyll.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: Blogging Like a Hacker 4 | --- 5 | 6 | This is an example of a markdown file whose YAML frontmatter will 7 | be ignored by Hologram. 8 | -------------------------------------------------------------------------------- /lib/hologram/code_example_renderer/renderers/js_renderer.rb: -------------------------------------------------------------------------------- 1 | Hologram::CodeExampleRenderer::Factory.define 'js' do 2 | example_template 'js_example_template' 3 | lexer { Rouge::Lexer.find('js') } 4 | end 5 | 6 | -------------------------------------------------------------------------------- /lib/hologram/code_example_renderer/renderers/jsx_renderer.rb: -------------------------------------------------------------------------------- 1 | Hologram::CodeExampleRenderer::Factory.define 'jsx' do 2 | example_template 'jsx_example_template' 3 | lexer { Rouge::Lexer.find('html') } 4 | end 5 | -------------------------------------------------------------------------------- /lib/template/code_example_templates/js_example_template.html.erb: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
<%= code_example %>
5 |
6 |
7 | 8 | -------------------------------------------------------------------------------- /lib/template/doc_assets/_footer.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /spec/fixtures/source/templates/_footer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /lib/template/code_example_templates/jsx_example_template.html.erb: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
<%= code_example %>
5 |
6 |
7 | 8 | -------------------------------------------------------------------------------- /lib/hologram/code_example_renderer/renderers/html_renderer.rb: -------------------------------------------------------------------------------- 1 | Hologram::CodeExampleRenderer::Factory.define 'html' do 2 | example_template 'markup_example_template' 3 | table_template 'markup_table_template' 4 | lexer { Rouge::Lexer.find('html') } 5 | end 6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | .sass-cache 19 | build 20 | bin 21 | !bin/hologram 22 | -------------------------------------------------------------------------------- /lib/template/code_example_templates/markup_example_template.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | <%= rendered_example %> 4 |
5 |
6 |
7 |
<%= code_example %>
8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /spec/fixtures/source/components/bem.md: -------------------------------------------------------------------------------- 1 | --- 2 | hologram: true 3 | title: BEM-like class naming 4 | name: bem 5 | category: Code 6 | --- 7 | 8 | We use a BEM-like naming convention [similar to Harry Roberts'](http://cssguidelin.es/#bem-like-naming): 9 | 10 | .block {} 11 | .block__element {} 12 | .block--modifier {} 13 | -------------------------------------------------------------------------------- /lib/hologram/errors.rb: -------------------------------------------------------------------------------- 1 | module Hologram 2 | class CommentLoadError < StandardError 3 | end 4 | end 5 | 6 | module Hologram 7 | class NoCategoryError < StandardError 8 | def message 9 | "Hologram comments found with no defined category. Are there other warnings/errors that need to be resolved?" 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/hologram_markdown_renderer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe HologramMarkdownRenderer do 4 | context 'for backwards compatability' do 5 | it 'is retained' do 6 | capture(:stdout) { 7 | class TestFoo < HologramMarkdownRenderer 8 | end 9 | } 10 | 11 | expect(TestFoo.ancestors).to include Hologram::MarkdownRenderer 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/hologram/link_helper.rb: -------------------------------------------------------------------------------- 1 | module Hologram 2 | class LinkHelper 3 | def initialize(pages) 4 | @all_links = {} 5 | pages.each do |page| 6 | page[:component_names].each do |component_name| 7 | @all_links[component_name] ||= "#{page[:name]}\##{component_name}" 8 | end 9 | end 10 | end 11 | 12 | attr_reader :all_links 13 | 14 | def link_for(component_name) 15 | all_links[component_name] 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/hologram/template_variables.rb: -------------------------------------------------------------------------------- 1 | module Hologram 2 | 3 | #Helper class for binding things for ERB 4 | class TemplateVariables 5 | attr_accessor :title, :file_name, :blocks, :output_files_by_category, :config, :pages 6 | 7 | def initialize(args) 8 | set_args(args) 9 | end 10 | 11 | def set_args(args) 12 | args.each do |k,v| 13 | instance_variable_set("@#{k}", v) 14 | end 15 | end 16 | 17 | def get_binding 18 | binding() 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/hologram/code_example_renderer/renderers/haml_renderer.rb: -------------------------------------------------------------------------------- 1 | Hologram::CodeExampleRenderer::Factory.define 'haml' do 2 | example_template 'markup_example_template' 3 | table_template 'markup_table_template' 4 | lexer { Rouge::Lexer.find('haml') } 5 | 6 | rendered_example do |code| 7 | begin 8 | require 'haml' 9 | rescue LoadError 10 | raise "haml must be present for you to use haml" 11 | end 12 | 13 | haml_engine = Haml::Engine.new(code.strip) 14 | haml_engine.render(Object.new, {}) 15 | end 16 | end 17 | 18 | -------------------------------------------------------------------------------- /lib/hologram/code_example_renderer/renderers/slim_renderer.rb: -------------------------------------------------------------------------------- 1 | Hologram::CodeExampleRenderer::Factory.define 'slim' do 2 | example_template 'markup_example_template' 3 | table_template 'markup_table_template' 4 | lexer { Rouge::Lexer.find('slim') } 5 | 6 | rendered_example do |code| 7 | begin 8 | require 'slim' 9 | rescue LoadError 10 | raise "slim must be present for you to use slim" 11 | end 12 | 13 | slim_engine = Slim::Template.new { code.strip } 14 | slim_engine.render(Object.new, {}) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rspec' 2 | require 'hologram' 3 | require 'tmpdir' 4 | 5 | RSpec.configure do |config| 6 | config.order = "random" 7 | end 8 | 9 | #Rails kernerl helper 10 | def capture(stream) 11 | stream = stream.to_s 12 | captured_stream = Tempfile.new(stream) 13 | stream_io = eval("$#{stream}") 14 | origin_stream = stream_io.dup 15 | stream_io.reopen(captured_stream) 16 | 17 | yield 18 | 19 | stream_io.rewind 20 | return captured_stream.read 21 | ensure 22 | captured_stream.unlink 23 | stream_io.reopen(origin_stream) 24 | end 25 | -------------------------------------------------------------------------------- /lib/hologram/code_example_renderer/example.rb: -------------------------------------------------------------------------------- 1 | module Hologram 2 | module CodeExampleRenderer 3 | class Example < Struct.new(:code) 4 | def rendered_example 5 | code 6 | end 7 | 8 | def code_example 9 | formatter.format(lexer.lex(code)).strip 10 | end 11 | 12 | def get_binding 13 | binding 14 | end 15 | 16 | private 17 | 18 | def formatter 19 | @_formatter ||= Rouge::Formatters::HTML.new(wrap: false) 20 | end 21 | 22 | def lexer 23 | @_lexer ||= Rouge::Lexer.find_fancy('guess', code) 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/template/code_example_templates/markup_table_template.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | <% examples.each do |example| %> 5 | 6 | 11 | 18 | 19 | <% end %> 20 | 21 |
7 |
8 | <%= example.rendered_example %> 9 |
10 |
12 |
13 |
14 |
<%= example.code_example %>
15 |
16 |
17 |
22 |
23 | 24 | -------------------------------------------------------------------------------- /lib/hologram/code_example_renderer/renderers/react_renderer.rb: -------------------------------------------------------------------------------- 1 | require 'securerandom' 2 | 3 | Hologram::CodeExampleRenderer::Factory.define 'react' do 4 | example_template 'markup_example_template' 5 | table_template 'markup_table_template' 6 | 7 | lexer { Rouge::Lexer.find(:html) } 8 | 9 | rendered_example do |code| 10 | div_id = SecureRandom.hex(10) 11 | [ 12 | "
", 13 | "" 19 | ].join("\n") 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/fixtures/source/config.yml: -------------------------------------------------------------------------------- 1 | # Hologram config 2 | 3 | # The directory containing the source files to parse 4 | source: ./components 5 | 6 | # The assets needed to build the docs (includes header.html, footer.html, etc) 7 | documentation_assets: ./templates 8 | 9 | # This is a custom markdown renderer (see Redcarpet documentation for 10 | # how to do this) 11 | # custom_markdown: trulia_markdown_renderer.rb 12 | 13 | # Any other asset folders that need to be copied to the destination folder 14 | # This where you should include your full stylesheets, component javascript, 15 | # libraries and any other dependencies your style guide will have 16 | dependencies: 17 | - ./extra 18 | -------------------------------------------------------------------------------- /spec/fixtures/source/config_multi_source.yml: -------------------------------------------------------------------------------- 1 | # Hologram config 2 | 3 | # The directory containing the source files to parse 4 | source: 5 | - ./components 6 | - ./templates 7 | 8 | # The assets needed to build the docs (includes header.html, footer.html, etc) 9 | documentation_assets: ./templates 10 | 11 | # This is a custom markdown renderer (see Redcarpet documentation for 12 | # how to do this) 13 | # custom_markdown: trulia_markdown_renderer.rb 14 | 15 | # Any other asset folders that need to be copied to the destination folder 16 | # This where you should include your full stylesheets, component javascript, 17 | # libraries and any other dependencies your style guide will have 18 | dependencies: 19 | - ./extra 20 | -------------------------------------------------------------------------------- /lib/hologram/utils.rb: -------------------------------------------------------------------------------- 1 | module Hologram 2 | module Utils 3 | def self.get_markdown_renderer(custom_markdown = nil) 4 | return MarkdownRenderer if custom_markdown.nil? 5 | 6 | md_file = Pathname.new(custom_markdown).realpath 7 | load md_file 8 | renderer_class = self.get_class_name(custom_markdown) 9 | DisplayMessage.info("Custom markdown renderer #{renderer_class} loaded.") 10 | Module.const_get(renderer_class) 11 | rescue LoadError => e 12 | DisplayMessage.error("Could not load #{custom_markdown}.") 13 | rescue NameError => e 14 | DisplayMessage.error("Class #{renderer_class} not found in #{custom_markdown}.") 15 | end 16 | 17 | def self.get_class_name(file) 18 | File.basename(file, '.rb').split(/_/).map(&:capitalize).join 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/template/doc_assets/_header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | a brand new styleguide: <%= title %> 7 | 8 | 9 | 10 |
11 | 18 | 19 |

a brand new style guide, it's super effective

20 |
21 | 26 | -------------------------------------------------------------------------------- /spec/code_example_renderer/example_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Hologram::CodeExampleRenderer::Example do 4 | let(:code) { 'goto 12' } 5 | let(:example) { described_class.new(code) } 6 | 7 | describe '#rendered_example' do 8 | subject { example.rendered_example } 9 | it { is_expected.to eq 'goto 12' } 10 | end 11 | 12 | describe '#code_example' do 13 | let(:formatter) { double(:formatter) } 14 | let(:lexer) { double(:lexer, lex: 'lexed_code') } 15 | 16 | before do 17 | allow(Rouge::Lexer).to receive(:find_fancy).with('guess', code) { lexer } 18 | allow(Rouge::Formatters::HTML).to receive(:new) { formatter } 19 | allow(formatter).to receive(:format).with('lexed_code') { 'formatted_lexed_code' } 20 | end 21 | 22 | subject { example.code_example } 23 | 24 | it { is_expected.to eq 'formatted_lexed_code' } 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/hologram/code_example_renderer/template.rb: -------------------------------------------------------------------------------- 1 | module Hologram 2 | module CodeExampleRenderer 3 | class Template < Struct.new(:template_name) 4 | def template 5 | return nil if !template_filename 6 | File.read(template_filename) 7 | end 8 | 9 | class << self 10 | attr_accessor :path_to_custom_example_templates 11 | end 12 | 13 | private 14 | 15 | def template_filename 16 | return nil if !template_name 17 | custom_file_exists? ? custom_file : default_file 18 | end 19 | 20 | def custom_file_exists? 21 | !!self.class.path_to_custom_example_templates && File.file?(custom_file) 22 | end 23 | 24 | def custom_file 25 | File.join(self.class.path_to_custom_example_templates, "#{template_name}.html.erb") 26 | end 27 | 28 | def default_file 29 | File.join(File.dirname(__FILE__), '..', '..', 'template', 'code_example_templates', "#{template_name}.html.erb") 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/fixtures/source/components/index.md: -------------------------------------------------------------------------------- 1 | #Welcome 2 | 3 | This style guide is an example of what 4 | [hologram](http://trulia.github.io/hologram) can help you build and 5 | maintain. 6 | 7 | The goal of hologram is to make it easy to document your CSS and to 8 | display the code examples to use that CSS. Hologram has no 9 | opinions about how you should actually organize/style your style guide. 10 | You could do everything as a single file with no header/footer and it 11 | would work just fine. Or, you could break it up into an individual file 12 | for each component. Top navigation, left navigation, no 13 | navigation....that's up to you. Build it however you'd like. 14 | 15 | 16 | We've borrowed some of [Trulia](http://trulia.com)'s own CSS library to 17 | demonstrate how hologram can be used. If you want more details about 18 | hologram see the [git repository](http://github.com/trulia/hologram). 19 | 20 | 21 | This is a work in progress, please consider contributing to 22 | [hologram](http://github.com/trulia/hologram). 23 | 24 | -------------------------------------------------------------------------------- /spec/cli_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'hologram/cli' 3 | 4 | describe Hologram::CLI do 5 | let(:argv) { [] } 6 | subject(:cli) { Hologram::CLI.new(argv) } 7 | 8 | context '#run' do 9 | context 'when arg is "init"' do 10 | let(:argv) { ['init'] } 11 | 12 | it 'setups the dir' do 13 | expect(Hologram::DocBuilder).to receive(:setup_dir) 14 | cli.run 15 | end 16 | end 17 | 18 | context 'when arg is empty' do 19 | subject(:builder) { double(Hologram::DocBuilder, is_valid?: true, build: true) } 20 | 21 | it 'builds the documentation' do 22 | expect(Hologram::DocBuilder).to receive(:from_yaml).and_return(builder) 23 | cli.run 24 | end 25 | end 26 | 27 | context 'when a config file is passed' do 28 | let(:argv) { ['test.yml'] } 29 | subject(:builder) { double(Hologram::DocBuilder, is_valid?: true, build: true) } 30 | 31 | it 'builds the documentation' do 32 | expect(Hologram::DocBuilder).to receive(:from_yaml).with('test.yml', []).and_return(builder) 33 | cli.run 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /hologram.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'hologram/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "hologram" 8 | spec.version = Hologram::VERSION 9 | spec.authors = ["JD Cantrell", "August Flanagan"] 10 | spec.email = ["jcantrell@trulia.com"] 11 | spec.description = %q{Build doc type things} 12 | spec.summary = %q{Build document type things.} 13 | spec.homepage = "http://trulia.github.io/hologram" 14 | spec.license = "MIT" 15 | 16 | spec.add_dependency "redcarpet", ">= 2.2", "< 4.0" 17 | spec.add_dependency "rouge", ">= 1.5", "!= 1.9.1" 18 | 19 | spec.files = `git ls-files`.split($/) 20 | spec.executables = ['hologram'] 21 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 22 | spec.require_paths = ["lib"] 23 | 24 | spec.add_development_dependency "bundler", "~> 1.3" 25 | spec.add_development_dependency "rake" 26 | spec.add_development_dependency "rspec", "~> 2.14" 27 | spec.add_development_dependency "haml" 28 | spec.add_development_dependency "slim" 29 | end 30 | -------------------------------------------------------------------------------- /lib/hologram.rb: -------------------------------------------------------------------------------- 1 | require 'redcarpet' 2 | require 'yaml' 3 | require 'rouge' 4 | require 'fileutils' 5 | require 'pathname' 6 | require 'erb' 7 | 8 | require 'hologram/version' 9 | require 'hologram/plugin' 10 | require 'hologram/plugins' 11 | require 'hologram/document_block' 12 | require 'hologram/doc_block_collection' 13 | require 'hologram/doc_parser' 14 | require 'hologram/doc_builder' 15 | require 'hologram/template_variables' 16 | require 'hologram/display_message' 17 | require 'hologram/errors' 18 | require 'hologram/utils' 19 | require 'hologram/markdown_renderer' 20 | require 'hologram/code_example_renderer' 21 | 22 | Encoding.default_internal = Encoding::UTF_8 23 | Encoding.default_external = Encoding::UTF_8 24 | 25 | module Hologram 26 | INIT_TEMPLATE_PATH = File.expand_path('./template/', File.dirname(__FILE__)) + '/' 27 | INIT_TEMPLATE_FILES = [ 28 | INIT_TEMPLATE_PATH + '/hologram_config.yml', 29 | INIT_TEMPLATE_PATH + '/doc_assets', 30 | INIT_TEMPLATE_PATH + '/code_example_templates', 31 | ] 32 | end 33 | 34 | class HologramMarkdownRenderer < Hologram::MarkdownRenderer 35 | def self.inherited(subclass) 36 | puts "HologramMarkdownRenderer is deprecated, please inherit from Hologram::MarkdownRenderer" 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/hologram/block_code_renderer.rb: -------------------------------------------------------------------------------- 1 | require 'erb' 2 | 3 | module Hologram 4 | class BlockCodeRenderer 5 | def initialize(code, markdown_language) 6 | @code = code 7 | @markdown_language = markdown_language 8 | end 9 | 10 | def render 11 | if is_table? && table_template 12 | examples = code.split("\n\n").map { |code_snippet| example_class.new(code_snippet) } 13 | ERB.new(table_template).result(binding) 14 | else 15 | example = example_class.new(code) 16 | ERB.new(example_template).result(example.get_binding) 17 | end 18 | end 19 | 20 | private 21 | 22 | attr_reader :code, :markdown_language 23 | 24 | def example_class 25 | CodeExampleRenderer.example_class_for(example_type) 26 | end 27 | 28 | def example_template 29 | CodeExampleRenderer.example_template_for(example_type) 30 | end 31 | 32 | def table_template 33 | CodeExampleRenderer.table_template_for(example_type) 34 | end 35 | 36 | def example_type 37 | match = /(\w+)_example/.match(markdown_language) 38 | match ? match.captures.first : nil 39 | end 40 | 41 | def is_table? 42 | markdown_language && markdown_language.include?('example_table') 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/fixtures/source/components/background/backgrounds.css: -------------------------------------------------------------------------------- 1 | /*doc 2 | --- 3 | title: Background Colors 4 | name: background 5 | category: Base CSS 6 | --- 7 | 8 | We have a few background colors that can be used in various contexts. 9 | These are not for use as the entire page background but instead for 10 | specific components and modules on the page. 11 | 12 |
13 |
14 |
15 | backgroundLowlight 16 |
17 |
18 |
19 | backgroundHighlight 20 |
21 |
22 |
23 | backgroundBasic 24 |
25 |
26 |
27 | backgroundInverse 28 |
29 |
30 | */ 31 | 32 | .backgroundBasic { 33 | background-color: white; 34 | } 35 | 36 | .backgroundLowlight { 37 | background-color: #f9f9f9; 38 | } 39 | 40 | .backgroundHighlight { 41 | background-color: #5eab1f; 42 | } 43 | 44 | .backgroundInverse { 45 | background-color: #222222; 46 | } 47 | -------------------------------------------------------------------------------- /lib/hologram/plugins.rb: -------------------------------------------------------------------------------- 1 | module Hologram 2 | class Plugins 3 | 4 | attr_accessor :plugins 5 | 6 | def initialize(config = {}, args) 7 | @plugins = [] 8 | 9 | if config.has_key?('plugins') 10 | for plugin_file in config['plugins'] 11 | plugin_path = Pathname.new(plugin_file).realpath 12 | load plugin_path 13 | plugin_class = Utils.get_class_name(plugin_file) 14 | register(plugin_class, config, args) 15 | end 16 | end 17 | end 18 | 19 | def register(plugin_class, config, args) 20 | clazz = Object.const_get(plugin_class) 21 | obj = clazz.new(config, args) 22 | if obj.is_active? 23 | DisplayMessage.info("Plugin active: #{plugin_class}") 24 | else 25 | DisplayMessage.info("Plugin not active: #{plugin_class}") 26 | end 27 | 28 | @plugins.push(obj) 29 | end 30 | 31 | def block(comment_block, file) 32 | for plugin in @plugins 33 | #We parse comment blocks even when the plugin is not active, 34 | #this allows us to remove the plugin's data blocks from the 35 | #markdown output 36 | plugin.parse_block(comment_block, file) 37 | end 38 | end 39 | 40 | def finalize(pages) 41 | for plugin in @plugins 42 | if plugin.is_active? 43 | plugin.finalize(pages) 44 | end 45 | end 46 | end 47 | 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/utils_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Hologram::Utils do 4 | context '.get_markdown_renderer' do 5 | subject(:utils) { Hologram::Utils } 6 | 7 | around do |example| 8 | Hologram::DisplayMessage.quiet! 9 | current_dir = Dir.pwd 10 | Dir.chdir('spec/fixtures/renderer') 11 | 12 | example.run 13 | 14 | Dir.chdir(current_dir) 15 | Hologram::DisplayMessage.show! 16 | end 17 | 18 | context 'by default' do 19 | let(:renderer) { utils.get_markdown_renderer } 20 | 21 | it 'returns the standard hologram markdown renderer' do 22 | expect(renderer).to eql Hologram::MarkdownRenderer 23 | end 24 | end 25 | 26 | context 'when passed a valid custom renderer' do 27 | let(:renderer) { utils.get_markdown_renderer('valid_renderer.rb') } 28 | 29 | it 'returns the custom renderer' do 30 | expect(renderer).to eql ValidRenderer 31 | end 32 | end 33 | 34 | context 'when passed an invalid custom renderer' do 35 | context 'expecting a class named as the upper camel cased version of the file name' do 36 | it 'exits' do 37 | expect { 38 | utils.get_markdown_renderer('invalid_renderer.rb') 39 | }.to raise_error SystemExit 40 | end 41 | end 42 | 43 | context 'expecting a filename.rb' do 44 | it 'exits' do 45 | expect { 46 | utils.get_markdown_renderer('foo') 47 | }.to raise_error Errno::ENOENT 48 | end 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Trulia, Inc. All rights reserved. 2 | 3 | MIT License 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the Trulia, Inc. nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 16 | IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 17 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 18 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL TRULIA, INC. BE 19 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 20 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 21 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 22 | BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 23 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 24 | OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 25 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /example_markdown_renderer.rb.example: -------------------------------------------------------------------------------- 1 | class ExampleMarkdownRenderer < Redcarpet::Render::HTML 2 | def block_code(code, language) 3 | if language and language.include?('example') 4 | if language.include?('js') 5 | # first actually insert the code in the docs so that it will run and make our example work. 6 | ' 7 |
' + Pygments.highlight(code) + '
' 8 | else 9 | '
' + '
' + render_html(code, language) + '
' + '
' + Pygments.highlight(code, :lexer => get_lexer(language)) + '
' + '
' 10 | end 11 | else 12 | '
' + Pygments.highlight(code) + '
' 13 | end 14 | end 15 | 16 | private 17 | def render_html(code, language) 18 | case language 19 | when 'haml_example' 20 | safe_require('haml', language) 21 | return Haml::Engine.new(code.strip).render(template_rendering_scope, {}) 22 | else 23 | code 24 | end 25 | end 26 | 27 | def template_rendering_scope 28 | Object.new 29 | end 30 | 31 | def get_lexer(language) 32 | case language 33 | when 'haml_example' 34 | 'haml' 35 | else 36 | 'html' 37 | end 38 | end 39 | 40 | def safe_require(templating_library, language) 41 | begin 42 | require templating_library 43 | rescue LoadError 44 | raise "#{templating_library} must be present for you to use #{language}" 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/hologram/version.rb: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013, Trulia, Inc. 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # * Redistributions of source code must retain the above copyright 7 | # notice, this list of conditions and the following disclaimer. 8 | # * Redistributions in binary form must reproduce the above copyright 9 | # notice, this list of conditions and the following disclaimer in the 10 | # documentation and/or other materials provided with the distribution. 11 | # * Neither the name of the Trulia, Inc. nor the 12 | # names of its contributors may be used to endorse or promote products 13 | # derived from this software without specific prior written permission. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 16 | # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 17 | # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 18 | # PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL TRULIA, INC. BE 19 | # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 20 | # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 21 | # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 22 | # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 23 | # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 24 | # OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 25 | # ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | 27 | module Hologram 28 | VERSION = "1.4.0" 29 | end 30 | -------------------------------------------------------------------------------- /lib/hologram/cli.rb: -------------------------------------------------------------------------------- 1 | require 'optparse' 2 | 3 | module Hologram 4 | class CLI 5 | attr_reader :args 6 | 7 | def initialize(args) 8 | @args = args 9 | end 10 | 11 | def run 12 | return setup if args[0] == 'init' 13 | extra_args = [] 14 | 15 | #support passing the config file with no command line flag 16 | config = args[0].nil? ? 'hologram_config.yml' : args[0] 17 | 18 | OptionParser.new do |opt| 19 | opt.on_tail('-h', '--help', 'Show this message.') { puts opt; exit } 20 | opt.on_tail('-v', '--version', 'Show version.') { puts "hologram #{Hologram::VERSION}"; exit } 21 | opt.on('-c', '--config FILE', 'Path to config file. Default: hologram_config.yml') { |config_file| config = config_file } 22 | begin 23 | opt.parse!(args) 24 | rescue OptionParser::InvalidOption => e 25 | extra_args.push(e.to_s.sub(/invalid option:\s+/, '')) 26 | end 27 | 28 | end 29 | 30 | config.nil? ? build(extra_args) : build(extra_args, config) 31 | 32 | end 33 | 34 | private 35 | def build(extra_args = [], config = 'hologram_config.yml') 36 | builder = DocBuilder.from_yaml(config, extra_args) 37 | DisplayMessage.error(builder.errors.first) if !builder.is_valid? 38 | builder.build 39 | rescue CommentLoadError, NoCategoryError => e 40 | DisplayMessage.error(e.message) 41 | rescue Errno::ENOENT 42 | DisplayMessage.error("Could not load config file, try 'hologram init' to get started") 43 | end 44 | 45 | def setup 46 | DocBuilder.setup_dir 47 | rescue => e 48 | DisplayMessage.error("#{e}") 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/link_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'hologram/link_helper' 3 | 4 | describe Hologram::LinkHelper do 5 | let(:pages) do 6 | [ 7 | { 8 | name: 'elements.html', 9 | component_names: [ 10 | 'images', 11 | 'buttons', 12 | 'typography', 13 | ] 14 | }, { 15 | name: 'utilities.html', 16 | component_names: [ 17 | 'whitespace', 18 | 'typography' 19 | ] 20 | } 21 | ] 22 | end 23 | 24 | let(:link_helper) { described_class.new(pages) } 25 | 26 | describe '#link_for' do 27 | subject { link_helper.link_for(component_name) } 28 | 29 | context 'when the link doesnt belong to any page' do 30 | let(:component_name) { 'hamburger' } 31 | it { should be_nil } 32 | end 33 | 34 | context 'when the link belongs to only one page' do 35 | let(:component_name) { 'whitespace' } 36 | it { should == 'utilities.html#whitespace' } 37 | end 38 | 39 | context 'when the link belongs to more than one page' do 40 | let(:component_name) { 'typography' } 41 | it 'creates a link to the first page the component appears on' do 42 | expect(subject).to eq 'elements.html#typography' 43 | end 44 | end 45 | end 46 | 47 | describe '#all_links' do 48 | it 'returns a hash from component name to link' do 49 | expect(link_helper.all_links).to eq ({ 50 | 'images' => 'elements.html#images', 51 | 'buttons' => 'elements.html#buttons', 52 | 'typography' => 'elements.html#typography', 53 | 'whitespace' => 'utilities.html#whitespace', 54 | }) 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/fixtures/source/templates/_header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | My Style Guide <%= title %> 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 37 | 38 |
39 |
40 |
41 | 42 |
43 |
44 |
45 | 50 |
51 |
52 |
53 | 54 |
55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /spec/code_example_renderer/template_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Hologram::CodeExampleRenderer::Template do 4 | describe '#template' do 5 | subject { described_class.new(template_name).template } 6 | 7 | context 'when template_name is nil' do 8 | let(:template_name) { nil } 9 | it { is_expected.to be_nil } 10 | end 11 | 12 | context 'when template_name is not nil' do 13 | let(:template_name) { 'bar' } 14 | 15 | let(:custom_template_filename) { 'custom/bar.html.erb' } 16 | let(:default_template_filename) { 'cwd/../../template/code_example_templates/bar.html.erb' } 17 | 18 | before do 19 | allow(File).to receive(:dirname) { 'cwd/' } 20 | allow(File).to receive(:read).with(custom_template_filename) { 'custom template' } 21 | allow(File).to receive(:read).with(default_template_filename) { 'default template' } 22 | end 23 | 24 | context 'when path_to_custom_example_templates is defined' do 25 | before do 26 | described_class.path_to_custom_example_templates = 'custom/' 27 | allow(File).to receive(:file?).with(custom_template_filename) { has_custom_template? } 28 | end 29 | 30 | after do 31 | described_class.path_to_custom_example_templates = nil 32 | end 33 | 34 | context 'when a custom template exists' do 35 | let(:has_custom_template?) { true } 36 | it { is_expected.to eq 'custom template' } 37 | end 38 | 39 | context 'when a custom template does not exist' do 40 | let(:has_custom_template?) { false } 41 | it { is_expected.to eq 'default template' } 42 | end 43 | end 44 | 45 | context 'when a path_to_custom_example_templates is not defined' do 46 | it { is_expected.to eq 'default template' } 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/hologram/code_example_renderer/factory.rb: -------------------------------------------------------------------------------- 1 | require 'hologram/code_example_renderer' 2 | 3 | module Hologram 4 | module CodeExampleRenderer 5 | class Factory 6 | def self.define(example_type, &block) 7 | definition_proxy = DefinitionProxy.new 8 | definition_proxy.instance_eval(&block) 9 | 10 | example_class = Class.new(Example) do 11 | if definition_proxy.rendered_example_block 12 | define_method :rendered_example do 13 | definition_proxy.rendered_example_block.call(code) 14 | end 15 | end 16 | 17 | private 18 | 19 | if definition_proxy.lexer_block 20 | define_method :lexer do 21 | definition_proxy.lexer_block.call 22 | end 23 | end 24 | end 25 | 26 | CodeExampleRenderer.register(example_type, 27 | example_class: example_class, 28 | example_template: Template.new(definition_proxy.example_template_name).template, 29 | table_template: Template.new(definition_proxy.table_template_name).template, 30 | ) 31 | end 32 | end 33 | 34 | class DefinitionProxy 35 | attr_reader :example_template_name, :table_template_name, 36 | :lexer_block, :rendered_example_block 37 | 38 | def example_template(template_name) 39 | self.example_template_name = template_name 40 | end 41 | 42 | def table_template(template_name) 43 | self.table_template_name = template_name 44 | end 45 | 46 | def lexer(&block) 47 | self.lexer_block = block 48 | end 49 | 50 | def rendered_example(&block) 51 | self.rendered_example_block = block 52 | end 53 | 54 | private 55 | 56 | attr_writer :example_template_name, :table_template_name, 57 | :lexer_block, :rendered_example_block 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/hologram/doc_block_collection.rb: -------------------------------------------------------------------------------- 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, 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 16 | end 17 | 18 | if @doc_blocks.has_key?(doc_block.name) 19 | DisplayMessage.warning("Multiple Hologram comments with name: #{doc_block.name}.") 20 | else 21 | @doc_blocks[doc_block.name] = doc_block 22 | end 23 | return doc_block 24 | end 25 | 26 | def create_nested_structure 27 | blocks_to_remove_from_top_level = [] 28 | @doc_blocks.each do |key, doc_block| 29 | # don't do anything to top level doc_blocks 30 | next if !doc_block.parent 31 | 32 | parent = @doc_blocks[doc_block.parent] 33 | if parent.nil? 34 | DisplayMessage.warning("Hologram comment refers to parent: #{doc_block.parent}, but no other hologram comment has name: #{doc_block.parent}, skipping." ) 35 | else 36 | parent.children[doc_block.name] = doc_block 37 | doc_block.parent = parent 38 | 39 | if doc_block.categories.empty? 40 | doc_block.categories = parent.categories 41 | end 42 | 43 | blocks_to_remove_from_top_level << doc_block.name 44 | end 45 | end 46 | 47 | blocks_to_remove_from_top_level.each do |key| 48 | @doc_blocks.delete(key) 49 | end 50 | end 51 | 52 | def skip_block(doc_block, file_name) 53 | DisplayMessage.warning(doc_block.errors.join("\n") << " in #{file_name}. This hologram comment will be skipped.") 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/template/hologram_config.yml: -------------------------------------------------------------------------------- 1 | # Hologram will run from same directory where this config file resides 2 | # All paths should be relative to there 3 | 4 | # The directory containing the source files to parse recursively 5 | source: ./sass 6 | 7 | # The directory that hologram will build to 8 | destination: ./docs 9 | 10 | # The assets needed to build the docs (includes header.html, 11 | # footer.html, etc) 12 | # You may put doc related assets here too: images, css, etc. 13 | documentation_assets: ./doc_assets 14 | 15 | # The folder that contains templates for rendering code examples. 16 | # If you want to change the way code examples appear in the styleguide, 17 | # modify the files in this folder 18 | code_example_templates: ./code_example_templates 19 | 20 | # The folder that contains custom code example renderers. 21 | # If you want to create additional renderers that are not provided 22 | # by Hologram (i.e. coffeescript renderer, jade renderer, etc) 23 | # place them in this folder 24 | code_example_renderers: ./code_example_renderers 25 | 26 | # Any other asset folders that need to be copied to the destination 27 | # folder. Typically this will include the css that you are trying to 28 | # document. May also include additional folders as needed. 29 | dependencies: 30 | - ./build 31 | 32 | # Mark which category should be the index page 33 | # Alternatively, you may have an index.md in the documentation assets 34 | # folder instead of specifying this config. 35 | index: basics 36 | 37 | # To additionally output navigation for top level sections, set the value to 38 | # 'section'. To output navigation for sub-sections, 39 | # set the value to `all` 40 | nav_level: all 41 | 42 | # Hologram displays warnings when there are issues with your docs 43 | # (e.g. if a component's parent is not found, if the _header.html and/or 44 | # _footer.html files aren't found) 45 | # If you want Hologram to exit on these warnings, set the value to 'true' 46 | # (Default value is 'false') 47 | exit_on_warnings: false 48 | -------------------------------------------------------------------------------- /lib/hologram/markdown_renderer.rb: -------------------------------------------------------------------------------- 1 | require 'hologram/block_code_renderer' 2 | 3 | include ERB::Util 4 | 5 | module Hologram 6 | class MarkdownRenderer < Redcarpet::Render::HTML 7 | def initialize(opts={}) 8 | super(opts) 9 | @link_helper = opts[:link_helper] 10 | end 11 | 12 | def list(contents, list_type) 13 | case list_type 14 | when :ordered 15 | "
    #{contents}
" 16 | else 17 | "
    #{contents}
" 18 | end 19 | end 20 | 21 | def paragraph(text) 22 | "

#{text}

" 23 | end 24 | 25 | def table(header, body) 26 | " #{header} #{body}
" 27 | end 28 | 29 | def codespan(code) 30 | "#{html_escape(code)}" 31 | end 32 | 33 | def link(link, title, content) 34 | "#{content}" 35 | end 36 | 37 | def block_code(code, language) 38 | BlockCodeRenderer.new(code, language).render 39 | end 40 | 41 | def preprocess(full_document) 42 | if link_helper 43 | link_defs + "\n" + full_document 44 | else 45 | full_document 46 | end 47 | end 48 | 49 | def postprocess(full_document) 50 | invalid_links = full_document.scan(/(?: \[ [\s\w]+ \]){2}/x) 51 | 52 | invalid_links.each do |invalid_link| 53 | component = /\[.+\]/.match(invalid_link)[1] 54 | DisplayMessage.warning("Invalid reference link - #{invalid_link}." + 55 | "Presumably the component #{component} does not exist.") 56 | end 57 | 58 | full_document 59 | end 60 | 61 | def css_class_name 62 | 'styleguide' 63 | end 64 | 65 | private 66 | 67 | attr_reader :link_helper 68 | 69 | def link_defs 70 | @_link_defs ||= link_helper.all_links.map { |c_name, link| "[#{c_name}]: #{link}" }.join("\n") 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/hologram/display_message.rb: -------------------------------------------------------------------------------- 1 | module Hologram 2 | module DisplayMessage 3 | @@quiet = false 4 | @@exit_on_warnings = false 5 | 6 | def self.quiet! 7 | @@quiet = true 8 | return self 9 | end 10 | 11 | def self.show! 12 | @@quiet = false 13 | return self 14 | end 15 | 16 | def self.quiet? 17 | @@quiet 18 | end 19 | 20 | def self.exit_on_warnings! 21 | @@exit_on_warnings = true 22 | end 23 | 24 | def self.continue_on_warnings! 25 | @@exit_on_warnings = false 26 | end 27 | 28 | def self.puts(str) 29 | return if quiet? 30 | super(str) 31 | end 32 | 33 | def self.info(message) 34 | puts message 35 | end 36 | 37 | def self.error(message) 38 | if RUBY_VERSION.to_f > 1.8 then 39 | puts angry_table_flipper + red(" Build not complete.") 40 | else 41 | puts red("Build not complete.") 42 | end 43 | 44 | puts " #{message}" 45 | exit 1 46 | end 47 | 48 | def self.created(files) 49 | puts "Created the following files and directories:" 50 | files.each do |file_name| 51 | puts " #{file_name}" 52 | end 53 | end 54 | 55 | def self.warning(message) 56 | puts yellow("Warning: #{message}") 57 | if @@exit_on_warnings 58 | puts red("Exiting due to warning") 59 | exit 1 60 | end 61 | end 62 | 63 | def self.success(message) 64 | puts green(message) 65 | end 66 | 67 | def self.angry_table_flipper 68 | green("(\u{256F}\u{00B0}\u{25A1}\u{00B0}\u{FF09}\u{256F}") + red("\u{FE35} \u{253B}\u{2501}\u{253B} ") 69 | end 70 | 71 | # colorization 72 | def self.colorize(color_code, str) 73 | "\e[#{color_code}m#{str}\e[0m" 74 | end 75 | 76 | def self.red(str) 77 | colorize(31, str) 78 | end 79 | 80 | def self.green(str) 81 | colorize(32, str) 82 | end 83 | 84 | def self.yellow(str) 85 | colorize(33, str) 86 | end 87 | 88 | def self.pink(str) 89 | colorize(35, str) 90 | end 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /spec/fixtures/styleguide/jekyll.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | My Style Guide 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 37 | 38 |
39 |
40 |
41 | 42 |
43 |
44 |
45 |
    46 | 47 |
48 |
49 |
50 |
51 | 52 |
53 | 54 | 55 | 56 |
57 |

layout: post

58 |

title: Blogging Like a Hacker

59 |

This is an example of a markdown file whose YAML frontmatter will 60 | be ignored by Hologram.

61 |
62 |
63 |
64 | The source code for this style guide is licensed under the MIT license. 65 |
66 |
67 | 68 | 69 | -------------------------------------------------------------------------------- /lib/hologram/code_example_renderer.rb: -------------------------------------------------------------------------------- 1 | require 'hologram/code_example_renderer/example' 2 | require 'hologram/code_example_renderer/template' 3 | 4 | module Hologram 5 | module CodeExampleRenderer 6 | class << self 7 | def register(example_type, args) 8 | puts "Adding renderer for #{example_type} examples" 9 | example_types[example_type] = { 10 | example_class: args[:example_class], 11 | example_template: args[:example_template], 12 | table_template: args[:table_template] 13 | } 14 | end 15 | 16 | def unregister(example_type) 17 | example_types.delete(example_type) 18 | end 19 | 20 | def example_class_for(example_type) 21 | if example_types.has_key?(example_type) 22 | example_types[example_type][:example_class] 23 | else 24 | Example 25 | end 26 | end 27 | 28 | def example_template_for(example_type) 29 | if example_types.has_key?(example_type) 30 | example_types[example_type][:example_template] 31 | else 32 | [ 33 | "
", 34 | "
", 35 | "
<%= code_example %>
", 36 | "
", 37 | "
", 38 | ].join("\n") 39 | end 40 | end 41 | 42 | def table_template_for(example_type) 43 | if example_types.has_key?(example_type) 44 | example_types[example_type][:table_template] 45 | else 46 | nil 47 | end 48 | end 49 | 50 | def load_renderers_and_templates 51 | require 'hologram/code_example_renderer/factory' 52 | 53 | Dir[File.join(File.dirname(__FILE__), 'code_example_renderer', 'renderers', '*.rb')].each do |file| 54 | require file 55 | end 56 | 57 | if path_to_custom_example_renderers 58 | Dir[File.join(path_to_custom_example_renderers, '*.rb')].each do |file| 59 | require file 60 | end 61 | end 62 | end 63 | 64 | attr_accessor :path_to_custom_example_renderers 65 | 66 | private 67 | 68 | def example_types 69 | @example_types ||= Hash.new 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /spec/fixtures/source/components/button/buttons.css: -------------------------------------------------------------------------------- 1 | /*doc 2 | --- 3 | title: Buttons 4 | name: button 5 | category: Base CSS 6 | --- 7 | 8 | Button styles can be applied to any element. Typically you'll want to 9 | use either a ` 13 | Don't click 14 | ``` 15 | 16 | If your button is actually a link to another page, please use the 17 | `` element, while if your button performs an action, such as submitting 18 | a form or triggering some javascript event, then use a ` 18 | Trulia! 19 | ``` 20 | 21 | If your button is actually a link to another page, please use the 22 | `` element, while if your button performs an action, such as 23 | submitting a form or triggering some javascript event, then use a 24 | ` | `btn btnDefault` | This is what buttons look like, this is the go to button style. 13 | | `btn btnPrimary` | Use this button as the primary call to action 14 | | `btn btnDanger` | This button is for delete actions, these actions should also have a confirmation dialog 15 | | `btn btnDisabled` | I'm afraid I can't let you do that, yet. 16 | 17 | ```haml_example_table 18 | %button.btn.btnDefault Hi 19 | 20 | %button.btn.btnPrimary Hi 21 | 22 | %button.btn.btnDanger Hi 23 | 24 | %button.btn.btnDisabled Hi 25 | ``` 26 | 27 | Here's an example combining a style with a size 28 | 29 | ```haml_example 30 | %button.btn.btnPrimary.btnFullWidth Hi 31 | ``` 32 | */ 33 | 34 | .btnDefault, 35 | a.btnDefault { 36 | border: 1px #d7d7d7 solid; 37 | background: #f3f3f3; 38 | color: #222222; 39 | text-shadow: 0 1px 0 white; 40 | -webkit-box-shadow: 0 1px 0px #cccccc; 41 | -moz-box-shadow: 0 1px 0px #cccccc; 42 | box-shadow: 0 1px 0px #cccccc; 43 | } 44 | 45 | .btnDefault:hover, 46 | .btnDefault:focus { 47 | background: #f6f6f6; 48 | } 49 | 50 | .btnDefault:active { 51 | position: relative; 52 | top: 1px; 53 | -webkit-box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.05); 54 | -moz-box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.05); 55 | box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.05); 56 | } 57 | 58 | .btnPrimary, 59 | a.btnPrimary, .btnDanger, 60 | a.btnDanger { 61 | color: white; 62 | text-shadow: 0 0 0; 63 | } 64 | 65 | .btnPrimary, 66 | a.btnPrimary { 67 | border: 1px #d14b00 solid; 68 | background: #ff5c00; 69 | -webkit-box-shadow: 0 1px 0px #d35500; 70 | -moz-box-shadow: 0 1px 0px #d35500; 71 | box-shadow: 0 1px 0px #d35500; 72 | } 73 | 74 | .btnPrimary:hover, 75 | .btnPrimary:focus { 76 | background: #ff660f; 77 | } 78 | 79 | .btnPrimary:active { 80 | position: relative; 81 | top: 1px; 82 | -webkit-box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.05); 83 | -moz-box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.05); 84 | box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.05); 85 | } 86 | 87 | .btnDanger, 88 | a.btnDanger { 89 | border: 1px #a21a10 solid; 90 | background: #cc2114; 91 | -webkit-box-shadow: 0 1px 0px #d35500; 92 | -moz-box-shadow: 0 1px 0px #d35500; 93 | box-shadow: 0 1px 0px #d35500; 94 | } 95 | 96 | .btnDanger:hover, 97 | .btnDanger:focus { 98 | background: #da2315; 99 | } 100 | 101 | .btnDanger:active { 102 | position: relative; 103 | top: 1px; 104 | -webkit-box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.05); 105 | -moz-box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.05); 106 | box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.05); 107 | } 108 | 109 | .btn[disabled], 110 | .btn[disabled]:hover, 111 | .btn[disabled]:focus 112 | .btn[disabled]:active, 113 | .btnDisabled, 114 | a.btnDisabled, 115 | .btnDisabled:hover, 116 | .btnDisabled:focus, 117 | .btnDisabled:active { 118 | border: 0px; 119 | background: #cccccc; 120 | color: #999999; 121 | text-shadow: 0 0 0; 122 | cursor: default; 123 | } 124 | 125 | .btn[disabled]:active, 126 | .btnDisabled:active { 127 | position: static; 128 | color: #999999; 129 | box-shadow: none; 130 | } 131 | -------------------------------------------------------------------------------- /spec/code_example_renderer/factory_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | Hologram::CodeExampleRenderer.load_renderers_and_templates 4 | 5 | describe Hologram::CodeExampleRenderer::Factory do 6 | describe '.define' do 7 | let(:example_type) { 'foobar' } 8 | 9 | before do 10 | allow(Hologram::CodeExampleRenderer::Template).to receive(:new).with('custom_example_template') { double(template: 'the full custom example template') } 11 | allow(Hologram::CodeExampleRenderer::Template).to receive(:new).with('custom_table_template') { double(template: 'the full custom table template') } 12 | allow(Hologram::CodeExampleRenderer::Template).to receive(:new).with(nil).and_call_original 13 | end 14 | 15 | context "when a renderer is registered with all of the options" do 16 | before do 17 | custom_lexer = double(:lexer, lex: 'lexed code from supplied lexer') 18 | 19 | Hologram::CodeExampleRenderer::Factory.define(example_type) do 20 | example_template 'custom_example_template' 21 | table_template 'custom_table_template' 22 | lexer { custom_lexer } 23 | rendered_example { |code| "special rendering for #{code}" } 24 | end 25 | end 26 | 27 | after { Hologram::CodeExampleRenderer.unregister(example_type) } 28 | 29 | describe "the registered example class" do 30 | let(:example_class) { Hologram::CodeExampleRenderer.example_class_for(example_type) } 31 | let(:example) { example_class.new('some code') } 32 | 33 | it "renders the example using the block supplied to the factory" do 34 | expect(example.rendered_example).to eq "special rendering for some code" 35 | end 36 | 37 | it "formats the code uses the supplied lexer" do 38 | formatter = double(:formatter) 39 | allow(Rouge::Formatters::HTML).to receive(:new) { formatter } 40 | 41 | expect(formatter).to receive(:format).with('lexed code from supplied lexer') { 'formatted code' } 42 | example.code_example 43 | end 44 | end 45 | 46 | describe "the registered templates" do 47 | let(:example_template) { Hologram::CodeExampleRenderer.example_template_for(example_type) } 48 | let(:table_template) { Hologram::CodeExampleRenderer.table_template_for(example_type) } 49 | 50 | it "uses the supplied example template" do 51 | expect(example_template).to eq 'the full custom example template' 52 | end 53 | 54 | it "uses the supplied table template" do 55 | expect(table_template).to eq 'the full custom table template' 56 | end 57 | end 58 | end 59 | 60 | context "when a renderer is registered with only some of the options" do 61 | before do 62 | 63 | Hologram::CodeExampleRenderer::Factory.define(example_type) do 64 | example_template 'custom_example_template' 65 | end 66 | end 67 | 68 | after { Hologram::CodeExampleRenderer.unregister(example_type) } 69 | 70 | describe "the registered example class" do 71 | let(:example_class) { Hologram::CodeExampleRenderer.example_class_for(example_type) } 72 | let(:example) { example_class.new('some code') } 73 | 74 | it "renders the example as is" do 75 | expect(example.rendered_example).to eq "some code" 76 | end 77 | 78 | it "formats the code uses the default lexer" do 79 | formatter = double(:formatter) 80 | allow(Rouge::Lexer).to receive(:find_fancy).with('guess', 'some code') { double(lex: 'default lexed code') } 81 | allow(Rouge::Formatters::HTML).to receive(:new) { formatter } 82 | 83 | expect(formatter).to receive(:format).with('default lexed code') { 'formatted code' } 84 | example.code_example 85 | end 86 | end 87 | 88 | describe "the registered templates" do 89 | let(:example_template) { Hologram::CodeExampleRenderer.example_template_for(example_type) } 90 | let(:table_template) { Hologram::CodeExampleRenderer.table_template_for(example_type) } 91 | 92 | it "uses the supplied example template" do 93 | expect(example_template).to eq 'the full custom example template' 94 | end 95 | 96 | it "does not have a table template" do 97 | expect(table_template).to be_nil 98 | end 99 | end 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /spec/document_block_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Hologram::DocumentBlock do 4 | 5 | let(:config) do 6 | { 'name' => 'foo', 'category' => 'bar', 'title' => 'baz', 'parent' => 'pop' } 7 | end 8 | let(:markdown){ 'blah' } 9 | let(:doc_block){ subject.class.new(config, markdown) } 10 | 11 | context '.from_comment' do 12 | let(:doc) do 13 | <<-eos 14 | /*doc 15 | --- 16 | title: Some other style 17 | name: otherStyle 18 | category: Foo 19 | --- 20 | Markdown stuff 21 | */ 22 | eos 23 | end 24 | 25 | let(:bad_doc) do 26 | <<-eos 27 | /*doc 28 | --- 29 | : : 30 | --- 31 | Markdown stuff 32 | */ 33 | eos 34 | end 35 | 36 | it 'initializes a new document block from a matching comment' do 37 | doc_block = Hologram::DocumentBlock.from_comment(doc) 38 | expect(doc_block).to be_a Hologram::DocumentBlock 39 | end 40 | 41 | it 'raises CommentLoadError when the yaml section is bad' do 42 | expect{ 43 | Hologram::DocumentBlock.from_comment(bad_doc) 44 | }.to raise_error Hologram::CommentLoadError 45 | end 46 | 47 | it 'returns nothing if its not a valid doc block' do 48 | doc_block = Hologram::DocumentBlock.from_comment('foo') 49 | expect(doc_block).to be_nil 50 | end 51 | 52 | it 'sets up the usual attrs using the YAML and markdown text' do 53 | doc_block = Hologram::DocumentBlock.from_comment(doc) 54 | expect(doc_block.name).to eql 'otherStyle' 55 | expect(doc_block.categories).to eql ['Foo'] 56 | expect(doc_block.title).to eql 'Some other style' 57 | expect(doc_block.markdown).to eql "/*doc\n\nMarkdown stuff\n*/\n" 58 | end 59 | end 60 | 61 | context '#set_members' do 62 | it 'sets accessors for the the block config' do 63 | expect(doc_block.send('name')).to eql 'foo' 64 | expect(doc_block.send('categories')).to eql ['bar'] 65 | expect(doc_block.send('title')).to eql 'baz' 66 | expect(doc_block.send('parent')).to eql 'pop' 67 | end 68 | end 69 | 70 | context '#get_hash' do 71 | let(:meta) do 72 | { name: 'foo', categories: ['bar'], title: 'baz', parent: 'pop' } 73 | end 74 | 75 | it 'returns a hash of meta info' do 76 | expect(doc_block.get_hash).to eql meta 77 | end 78 | end 79 | 80 | context '#is_valid?' do 81 | context 'when name and markdown is present' do 82 | it 'returns true' do 83 | expect(doc_block.is_valid?).to eql true 84 | end 85 | end 86 | 87 | context 'when name is not present' do 88 | let(:invalid_doc_block) do 89 | subject.class.new(config.merge(name: nil)) 90 | end 91 | 92 | it 'returns false' do 93 | expect(invalid_doc_block.is_valid?).to eql false 94 | end 95 | 96 | it 'populates errors' do 97 | invalid_doc_block.is_valid? 98 | expect(invalid_doc_block.errors).to include('Missing name or title config value') 99 | end 100 | end 101 | 102 | context 'when markdown is not present' do 103 | let(:invalid_doc_block) do 104 | subject.class.new(config) 105 | end 106 | 107 | it 'returns false' do 108 | expect(invalid_doc_block.is_valid?).to eql false 109 | end 110 | 111 | it 'populates errors' do 112 | invalid_doc_block.is_valid? 113 | expect(invalid_doc_block.errors).to include('Missing required markdown') 114 | end 115 | end 116 | end 117 | 118 | context '#markdown_with_heading' do 119 | context 'when include_sub_nav is false' do 120 | it 'returns markdown with a specified header' do 121 | expect(doc_block.markdown_with_heading(2)).to eql "\n\n

baz

blah" 122 | end 123 | end 124 | 125 | context 'when include_sub_nav is true' do 126 | context 'when the component has no children' do 127 | it 'returns markdown with no nav' do 128 | expect(doc_block.markdown_with_heading(2, include_sub_nav: true)).to eql "\n\n

baz

blah" 129 | end 130 | end 131 | 132 | context 'when the component has children' do 133 | before do 134 | doc_block.children['child-one'] = double( name: 'child-one', title: 'Child 1' ) 135 | doc_block.children['child-two'] = double( name: 'child-two', title: 'Child 2' ) 136 | end 137 | 138 | it 'returns markdown with no nav' do 139 | expected_output = <<-eos 140 | 141 | 142 |

baz

143 | 147 | blah 148 | eos 149 | expect(doc_block.markdown_with_heading(2, include_sub_nav: true)).to eql expected_output.rstrip 150 | end 151 | end 152 | end 153 | end 154 | end 155 | -------------------------------------------------------------------------------- /spec/doc_parser_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Hologram::DocParser do 4 | 5 | let(:doc) do 6 | <<-eos 7 | /*doc 8 | --- 9 | title: Some other style 10 | name: otherStyle 11 | category: Foo 12 | --- 13 | Markdown stuff 14 | */ 15 | eos 16 | end 17 | 18 | let(:docs_child) do 19 | <<-eos 20 | /*doc 21 | --- 22 | parent: otherStyle 23 | title: Other Style Child 24 | name: otherStyleChild 25 | --- 26 | Markdown stuff 27 | */ 28 | eos 29 | end 30 | 31 | let(:child_doc) do 32 | <<-eos 33 | /*doc 34 | --- 35 | parent: button 36 | name: otherStyle 37 | title: Some other style 38 | --- 39 | Markdown stuff 40 | */ 41 | eos 42 | end 43 | 44 | let(:grandchild_doc) do 45 | <<-eos 46 | /*doc 47 | --- 48 | parent: otherStyle 49 | name: grandbaby 50 | title: Grandbaby 51 | --- 52 | Markdown stuff 53 | */ 54 | eos 55 | end 56 | 57 | let(:multi_category_child_doc) do 58 | <<-eos 59 | /*doc 60 | --- 61 | parent: multi 62 | name: otherStyle 63 | title: MultiChild 64 | --- 65 | Markdown stuff 66 | multi-child 67 | */ 68 | eos 69 | end 70 | 71 | let(:multi_category_doc) do 72 | <<-eos 73 | /*doc 74 | --- 75 | title: MultiParent 76 | name: multi 77 | category: [Foo, Bar] 78 | --- 79 | Markdown stuff 80 | multi-parent 81 | */ 82 | eos 83 | end 84 | 85 | let(:source_path) { 'spec/fixtures/source' } 86 | let(:temp_doc) { File.join(source_path, 'components', 'button', 'skin', 'testSkin.css') } 87 | let(:plugins) { 88 | plugins = double() 89 | allow(plugins).to receive(:block) 90 | allow(plugins).to receive(:finalize) 91 | return plugins 92 | } 93 | 94 | subject(:parser) { Hologram::DocParser.new('spec/fixtures/source/components', nil, plugins) } 95 | 96 | context '#parse' do 97 | let(:result) { parser.parse } 98 | let(:pages) { result[0] } 99 | let(:output_files_by_category) { result[1] } 100 | 101 | it 'builds and returns a hash of pages and a hash of output_files_by_category' do 102 | expect(pages).to be_a Hash 103 | expect(output_files_by_category).to be_a Hash 104 | end 105 | 106 | context 'when the source has multiple paths' do 107 | subject(:parser) { Hologram::DocParser.new(['spec/fixtures/source/colors', 'spec/fixtures/source/components'], nil, plugins) } 108 | 109 | it 'parses all sources' do 110 | expect(pages['base_css.html'][:md]).to include 'Base colors' 111 | expect(pages['base_css.html'][:md]).to include 'Background Colors' 112 | end 113 | end 114 | 115 | context 'when the component has two categories' do 116 | around do |example| 117 | File.open(temp_doc, 'a+'){ |io| io << multi_category_doc } 118 | example.run 119 | FileUtils.rm(temp_doc) 120 | end 121 | 122 | it 'adds two categories to output_files_by_category' do 123 | expect(output_files_by_category).to eql({'Foo'=>'foo.html', 'Base CSS'=>'base_css.html', 'Bar'=>'bar.html', 'Code'=>'code.html'}) 124 | end 125 | end 126 | 127 | 128 | context 'when an index page is specified' do 129 | subject(:parser) { Hologram::DocParser.new('spec/fixtures/source', 'foo', plugins) } 130 | 131 | around do |example| 132 | File.open(temp_doc, 'a+'){ |io| io << doc } 133 | example.run 134 | FileUtils.rm(temp_doc) 135 | end 136 | 137 | it 'uses that page as the index' do 138 | expect(pages['index.html'][:md]).to include 'Markdown stuff' 139 | end 140 | end 141 | 142 | context 'when the source is a parent doc' do 143 | around do |example| 144 | File.open(temp_doc, 'a+'){ |io| io << doc } 145 | example.run 146 | FileUtils.rm(temp_doc) 147 | end 148 | 149 | it 'takes the category in a collection and treats them as the page names' do 150 | expect(pages.keys).to include 'foo.html' 151 | end 152 | 153 | context 'when nav_level is set to section' do 154 | before do 155 | File.open(temp_doc, 'a+'){ |io| io << docs_child } 156 | parser.nav_level = 'section' 157 | end 158 | 159 | it 'generates navigation to children from their parent' do 160 | parser.parse 161 | expect(pages['foo.html'][:md]).to include '
  • Other Style Child
  • ' 162 | end 163 | end 164 | 165 | context 'when nav_level is not set' do 166 | before do 167 | File.open(temp_doc, 'a+'){ |io| io << docs_child } 168 | parser.nav_level = nil 169 | end 170 | 171 | it 'should not generate sub navigation' do 172 | parser.parse 173 | expect(pages['foo.html'][:md]).not_to include '
  • Other Style Child
  • ' 174 | end 175 | end 176 | end 177 | 178 | context 'when a source doc is a child' do 179 | around do |example| 180 | File.open(temp_doc, 'a+'){ |io| io << child_doc } 181 | example.run 182 | FileUtils.rm(temp_doc) 183 | end 184 | 185 | it 'appends the child doc to the category page' do 186 | parser.parse 187 | expect(pages['base_css.html'][:md]).to include 'Some other style' 188 | end 189 | 190 | it 'assigns the child doc a deeper header' do 191 | parser.parse 192 | expect(pages['base_css.html'][:md]).to include '

    Some other style

    ' 193 | end 194 | 195 | context 'when nav_level is set to all' do 196 | before do 197 | File.open(temp_doc, 'a+'){ |io| io << grandchild_doc } 198 | parser.nav_level = 'all' 199 | end 200 | 201 | it 'generates navigation to grandchildren from their parent' do 202 | parser.parse 203 | expect(pages['base_css.html'][:md]).to include '
  • Grandbaby
  • ' 204 | end 205 | end 206 | end 207 | 208 | context 'when a source doc is the child of a multi category doc block' do 209 | around do |example| 210 | File.open(temp_doc, 'w'){ |io| 211 | io << multi_category_doc 212 | io << multi_category_child_doc 213 | } 214 | example.run 215 | FileUtils.rm(temp_doc) 216 | end 217 | 218 | it 'should not output duplicate content' do 219 | objects = expect(pages['foo.html'][:md]).not_to match(/multi-parent.*multi-child.*multi-child/m) 220 | objects = expect(pages['bar.html'][:md]).not_to match(/multi-parent.*multi-child.*multi-child/m) 221 | end 222 | 223 | it 'should correctly order content in each page' do 224 | objects = expect(pages['foo.html'][:md]).to match(/multi-parent.*multi-child/m) 225 | objects = expect(pages['bar.html'][:md]).to match(/multi-parent.*multi-child/m) 226 | end 227 | end 228 | end 229 | end 230 | -------------------------------------------------------------------------------- /lib/hologram/doc_parser.rb: -------------------------------------------------------------------------------- 1 | module Hologram 2 | class DocParser 3 | DEFAULT_SUPPORTED_EXTENSIONS = ['.css', '.scss', '.less', '.sass', '.styl', '.js', '.md', '.markdown', '.erb' ] 4 | attr_accessor :source_path, :pages, :doc_blocks, :nav_level 5 | 6 | def initialize(source_path, index_name = nil, plugins=[], opts={}) 7 | @plugins = plugins 8 | @source_paths = Array(source_path) 9 | @index_name = index_name 10 | @nav_level = opts[:nav_level] || 'page' 11 | @pages = {} 12 | @output_files_by_category = {} 13 | @supported_extensions = DEFAULT_SUPPORTED_EXTENSIONS 14 | @supported_extensions += opts[:custom_extensions] if opts[:custom_extensions] 15 | @ignore_paths = opts[:ignore_paths] || [] 16 | end 17 | 18 | def parse 19 | # recursively traverse our directory structure looking for files that 20 | # match our "parseable" file types. Open those files pulling out any 21 | # comments matching the hologram doc style /*doc */ and create DocBlock 22 | # objects from those comments, then add those to a collection object which 23 | # is then returned. 24 | 25 | doc_block_collection = DocBlockCollection.new 26 | 27 | @source_paths.each do |source_path| 28 | process_dir(source_path, doc_block_collection) 29 | end 30 | 31 | # doc blocks can define parent/child relationships that will nest their 32 | # documentation appropriately. we can't put everything into that structure 33 | # on our first pass through because there is no guarantee we'll parse files 34 | # in the correct order. This step takes the full collection and creates the 35 | # proper structure. 36 | doc_block_collection.create_nested_structure 37 | 38 | 39 | # hand off our properly nested collection to the output generator 40 | build_output(doc_block_collection.doc_blocks) 41 | 42 | @plugins.finalize(@pages) 43 | 44 | # if we have an index category defined in our config copy that 45 | # page to index.html 46 | if @index_name 47 | name = @index_name + '.html' 48 | if @pages.has_key?(name) 49 | @pages['index.html'] = Marshal.load(Marshal.dump(@pages[name])) 50 | title, _ = @output_files_by_category.rassoc(name) 51 | @output_files_by_category[title] = 'index.html' 52 | end 53 | end 54 | 55 | return @pages, @output_files_by_category 56 | end 57 | 58 | private 59 | 60 | def process_dir(base_directory, doc_block_collection) 61 | #get all directories in our library folder 62 | directories = Dir.glob("#{base_directory}/**/*/") 63 | directories.unshift(base_directory) 64 | 65 | directories.each do |directory| 66 | # filter and sort the files in our directory 67 | files = [] 68 | Dir.foreach(directory).select{ |file| is_supported_file_type?("#{directory}/#{file}") }.each do |file| 69 | files << file 70 | end 71 | files.sort! 72 | process_files(files, directory, doc_block_collection) 73 | end 74 | end 75 | 76 | def process_files(files, directory, doc_block_collection) 77 | 78 | if !@ignore_paths.empty? 79 | valid_files = files.select { |input_file| 80 | @ignore_paths.select { |glob| File.fnmatch(glob, input_file) }.empty? 81 | } 82 | else 83 | valid_files = files 84 | end 85 | 86 | valid_files.each do |input_file| 87 | if input_file.end_with?('md') 88 | process_markdown_file("#{directory}/#{input_file}", doc_block_collection) 89 | elsif input_file.end_with?('erb') 90 | @pages[File.basename(input_file, '.erb')] = {erb: File.read("#{directory}/#{input_file}")} 91 | else 92 | process_file("#{directory}/#{input_file}", doc_block_collection) 93 | end 94 | end 95 | end 96 | 97 | def process_markdown_file(file, doc_block_collection) 98 | file_str = File.read(file) 99 | 100 | if file_str.match(/^-{3}\n.*hologram:\s*true.*-{3}/m) 101 | doc_block_collection.add_doc_block(file_str, file) 102 | else 103 | @pages[File.basename(file, '.md') + '.html'] = {md: file_str, blocks: []} 104 | end 105 | end 106 | 107 | def process_file(file, doc_block_collection) 108 | file_str = File.read(file) 109 | # get any comment blocks that match the patterns: 110 | # .sass: //doc (follow by other lines proceeded by a space) 111 | # other types: /*doc ... */ 112 | if file.end_with?('.sass') 113 | #For sass strip out leading white spaces after we get the 114 | #comment, this fixes haml when using this comment style 115 | hologram_comments = file_str.scan(/\s*\/\/doc\s*((( [^\n]*\n)|\n)+)/).map{ |arr| [arr[0].gsub(/^[ \t]{2}/,'')] } 116 | else 117 | hologram_comments = file_str.scan(/\s*\/\*doc(.*?)\*\//m) 118 | 119 | #check if scss file has sass comments 120 | if hologram_comments.length == 0 and file.end_with?('.scss') 121 | hologram_comments = file_str.scan(/\s*\/\/doc\s*((( [^\n]*\n)|\n)+)/).map{ |arr| [arr[0].gsub(/^[ \t]{2}/,'')] } 122 | end 123 | end 124 | return unless hologram_comments 125 | 126 | 127 | 128 | hologram_comments.each do |comment_block| 129 | block = doc_block_collection.add_doc_block(comment_block[0], file) 130 | 131 | if (!block.nil?) 132 | @plugins.block(block, file) 133 | end 134 | 135 | end 136 | end 137 | 138 | def build_output(doc_blocks, output_file = nil, depth = 1) 139 | return if doc_blocks.nil? 140 | 141 | # sort elements in alphabetical order ignoring case 142 | doc_blocks.sort{|a, b| a[0].downcase<=>b[0].downcase}.map do |key, doc_block| 143 | 144 | #doc_blocks are guaranteed to always have categories (top-level have categories, children get parent categories if empty). 145 | doc_block.categories.each do |category| 146 | output_file = get_file_name(category) 147 | @output_files_by_category[category] = output_file 148 | add_doc_block_to_page(depth, doc_block, output_file) 149 | end 150 | build_output(doc_block.children, nil, depth + 1) 151 | end 152 | 153 | end 154 | 155 | def is_supported_file_type?(file) 156 | @supported_extensions.include?(File.extname(file)) and !Dir.exists?(file) 157 | end 158 | 159 | def get_file_name(str) 160 | str = str.gsub(' ', '_').downcase + '.html' 161 | end 162 | 163 | def add_doc_block_to_page(depth, doc_block, output_file) 164 | if !@pages.has_key?(output_file) 165 | @pages[output_file] = {md: "", blocks: []} 166 | end 167 | 168 | 169 | if (@nav_level == 'section' && depth == 1) || @nav_level == 'all' 170 | include_sub_nav = true 171 | else 172 | include_sub_nav = false 173 | end 174 | 175 | @pages[output_file][:blocks].push(doc_block.get_hash) 176 | @pages[output_file][:md] << doc_block.markdown_with_heading(depth, include_sub_nav: include_sub_nav) 177 | end 178 | 179 | end 180 | end 181 | -------------------------------------------------------------------------------- /spec/doc_builder_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Hologram::DocBuilder do 4 | subject(:builder) { Hologram::DocBuilder } 5 | 6 | around do |example| 7 | Hologram::DisplayMessage.quiet! 8 | example.run 9 | Hologram::DisplayMessage.show! 10 | end 11 | 12 | context '.from_yaml' do 13 | subject(:builder) { Hologram::DocBuilder } 14 | let(:spec_root) { File.expand_path('../', __FILE__) } 15 | let(:tmpdir) { @tmpdir } 16 | 17 | around do |example| 18 | Dir.mktmpdir do |tmpdir| 19 | @tmpdir = tmpdir 20 | current_dir = Dir.pwd 21 | 22 | begin 23 | Dir.chdir(tmpdir) 24 | example.run 25 | ensure 26 | Dir.chdir(current_dir) 27 | end 28 | end 29 | end 30 | 31 | context 'when passed a valid config file' do 32 | let(:config_path) { File.join(spec_root, 'fixtures/source/config.yml') } 33 | let(:config_copy_path) { File.join(spec_root, 'fixtures/source/config.yml.copy') } 34 | 35 | before do 36 | FileUtils.cp(config_path, config_copy_path) 37 | File.open(config_copy_path, 'a'){ |io| io << "destination: #{tmpdir}" } 38 | end 39 | after do 40 | FileUtils.rm(config_copy_path) 41 | end 42 | 43 | it 'returns a DocBuilder instance' do 44 | expect(subject.from_yaml(config_copy_path)).to be_a Hologram::DocBuilder 45 | end 46 | end 47 | 48 | context 'when passed an invalid config' do 49 | before do 50 | File.open('bad_config.yml', 'w'){ |io| io << '%' } 51 | end 52 | 53 | after do 54 | FileUtils.rm('bad_config.yml') 55 | end 56 | 57 | it 'exits the process' do 58 | expect { subject.from_yaml('bad_config.yml') }.to raise_error SyntaxError 59 | end 60 | end 61 | 62 | context 'when source option is an array' do 63 | let(:config_path) { File.join(spec_root, 'fixtures/source/config_multi_source.yml') } 64 | let(:config_copy_path) { File.join(spec_root, 'fixtures/source/config_multi_source.yml.copy') } 65 | 66 | before do 67 | FileUtils.cp(config_path, config_copy_path) 68 | File.open(config_copy_path, 'a'){ |io| io << "destination: #{tmpdir}" } 69 | end 70 | after do 71 | FileUtils.rm(config_copy_path) 72 | end 73 | 74 | it 'returns a DocBuilder instance' do 75 | expect(subject.from_yaml(config_copy_path)).to be_a Hologram::DocBuilder 76 | end 77 | end 78 | 79 | context 'when dependencies is left blank' do 80 | let(:yaml) { "dependencies:\n" } 81 | 82 | before do 83 | File.open('fixable_bad_config.yml', 'w'){ |io| io << yaml } 84 | end 85 | 86 | after do 87 | FileUtils.rm('fixable_bad_config.yml') 88 | end 89 | 90 | it 'deletes the empty config variable' do 91 | builder = subject.from_yaml('fixable_bad_config.yml') 92 | expect(builder).to be_a Hologram::DocBuilder 93 | expect(builder.dependencies).to eql [] 94 | end 95 | end 96 | end 97 | 98 | context '.setup_dir' do 99 | subject(:builder) { Hologram::DocBuilder } 100 | 101 | around do |example| 102 | Dir.mktmpdir do |dir| 103 | Dir.chdir(dir) do 104 | example.run 105 | end 106 | end 107 | end 108 | 109 | before do 110 | builder.setup_dir 111 | end 112 | 113 | it 'creates a config file' do 114 | expect(File.exists?('hologram_config.yml')).to be_truthy 115 | end 116 | 117 | it 'creates default assets' do 118 | Dir.chdir('doc_assets') do 119 | ['_header.html', '_footer.html'].each do |asset| 120 | expect(File.exists?(asset)).to be_truthy 121 | end 122 | end 123 | end 124 | 125 | context 'when a hologram_config.yml already exists' do 126 | it 'does nothing' do 127 | open('hologram_config.yml', 'w') {|io|io << 'foo'} 128 | builder.setup_dir 129 | expect(IO.read('hologram_config.yml')).to eql('foo') 130 | end 131 | end 132 | end 133 | 134 | context '#initialize' do 135 | subject { Hologram::DocBuilder.new(config) } 136 | 137 | context 'when the "exit_on_warnings" option is passed in as true' do 138 | let(:config) do 139 | { 'exit_on_warnings' => true } 140 | end 141 | 142 | it 'calls DisplayMessage.exit_on_warnings!' do 143 | expect(Hologram::DisplayMessage).to receive(:exit_on_warnings!) 144 | subject 145 | end 146 | end 147 | 148 | context 'when the "exit_on_warnings" option is not passed in' do 149 | let(:config) do 150 | { } 151 | end 152 | 153 | it 'does not call DisplayMessage.exit_on_warnings!' do 154 | expect(Hologram::DisplayMessage).not_to receive(:exit_on_warnings!) 155 | subject 156 | end 157 | end 158 | end 159 | 160 | context '#is_valid?' do 161 | 162 | let(:config) do 163 | { 164 | 'source' => 'spec/fixtures/source/components', 165 | 'documentation_assets' => 'spec/fixtures/source/templates', 166 | 'base_path' => 'spec/fixtures/source/' 167 | } 168 | end 169 | 170 | let(:builder) { Hologram::DocBuilder.new(config) } 171 | 172 | around do |example| 173 | Dir.mktmpdir do |tmpdir| 174 | config['destination'] = tmpdir 175 | example.run 176 | end 177 | end 178 | 179 | context 'when config vars are present and directories exists' do 180 | it 'returns true' do 181 | expect(builder.is_valid?).to be_truthy 182 | end 183 | end 184 | 185 | ['source', 'destination', 'documentation_assets'].each do |config_var| 186 | context "when the required #{config_var} parameter is missing" do 187 | before do 188 | config.delete(config_var) 189 | end 190 | 191 | it 'returns false' do 192 | expect(builder.is_valid?).to be_falsy 193 | end 194 | 195 | it 'populates errors' do 196 | builder.is_valid? 197 | expect(builder.errors.size).to eql 1 198 | end 199 | end 200 | end 201 | 202 | context 'when the source directory does not exist' do 203 | before do 204 | config['source'] = './foo' 205 | end 206 | 207 | it 'returns false' do 208 | expect(builder.is_valid?).to be_falsy 209 | end 210 | 211 | it 'populates errors' do 212 | builder.is_valid? 213 | expect(builder.errors.size).to eql 1 214 | end 215 | end 216 | 217 | context 'when source is an array' do 218 | let(:config) do 219 | { 220 | 'source' => ['spec/fixtures/source/components', 'spec/fixtures/source/templates'], 221 | 'documentation_assets' => 'spec/fixtures/source/templates', 222 | 'base_path' => 'spec/fixtures/source/' 223 | } 224 | end 225 | 226 | it 'returns true' do 227 | expect(builder.is_valid?).to be_truthy 228 | end 229 | end 230 | end 231 | 232 | context '#build' do 233 | let(:config_path) { File.join(Dir.pwd, 'spec/fixtures/source/config.yml') } 234 | let(:config_copy_path) { File.join(Dir.pwd, 'spec/fixtures/source/config.yml.copy') } 235 | let(:builder) { Hologram::DocBuilder.from_yaml(config_copy_path) } 236 | 237 | around do |example| 238 | Dir.mktmpdir do |tmpdir| 239 | FileUtils.cp(config_path, config_copy_path) 240 | File.open(config_copy_path, 'a'){ |io| io << "destination: #{tmpdir}" } 241 | current_dir = Dir.pwd 242 | Dir.chdir('spec/fixtures/source') 243 | 244 | example.run 245 | 246 | Dir.chdir(current_dir) 247 | FileUtils.rm(config_copy_path) 248 | end 249 | end 250 | 251 | it 'builds a styleguide' do 252 | builder.build 253 | expect(File.read(File.expand_path('../fixtures/styleguide/base_css.html', __FILE__))).to eq File.read(File.join(builder.destination, '.', 'base_css.html')) 254 | expect(File.read(File.expand_path('../fixtures/styleguide/index.html', __FILE__))).to eq File.read(File.join(builder.destination, '.', 'index.html')) 255 | expect(File.read(File.expand_path('../fixtures/styleguide/code.html', __FILE__))).to eq File.read(File.join(builder.destination, '.', 'code.html')) 256 | expect(File.read(File.expand_path('../fixtures/styleguide/jekyll.html', __FILE__))).to eq File.read(File.join(builder.destination, '.', 'jekyll.html')) 257 | end 258 | end 259 | end 260 | -------------------------------------------------------------------------------- /lib/hologram/doc_builder.rb: -------------------------------------------------------------------------------- 1 | require 'hologram/link_helper' 2 | 3 | module Hologram 4 | class DocBuilder 5 | attr_accessor :source, :destination, :documentation_assets, :dependencies, :index, :base_path, :renderer, :doc_blocks, :pages, :config_yml 6 | attr_reader :errors 7 | attr :doc_assets_dir, :output_dir, :input_dir, :header_erb, :footer_erb 8 | 9 | def self.from_yaml(yaml_file, extra_args = []) 10 | 11 | #Change dir so that our paths are relative to the config file 12 | base_path = Pathname.new(yaml_file) 13 | yaml_file = base_path.realpath.to_s 14 | Dir.chdir(base_path.dirname) 15 | 16 | config = YAML::load_file(yaml_file) 17 | raise SyntaxError if !config.is_a? Hash 18 | 19 | new(config.merge( 20 | 'config_yml' => config, 21 | 'base_path' => Pathname.new(yaml_file).dirname, 22 | 'renderer' => Utils.get_markdown_renderer(config['custom_markdown']) 23 | ), extra_args) 24 | 25 | rescue SyntaxError, ArgumentError, Psych::SyntaxError 26 | raise SyntaxError, "Could not load config file, check the syntax or try 'hologram init' to get started" 27 | end 28 | 29 | def self.setup_dir 30 | if File.exists?("hologram_config.yml") 31 | DisplayMessage.warning("Cowardly refusing to overwrite existing hologram_config.yml") 32 | return 33 | end 34 | 35 | FileUtils.cp_r INIT_TEMPLATE_FILES, Dir.pwd 36 | new_files = [ 37 | "hologram_config.yml", 38 | "doc_assets/", 39 | "doc_assets/_header.html", 40 | "doc_assets/_footer.html", 41 | "code_example_templates/", 42 | "code_example_templates/markdown_example_template.html.erb", 43 | "code_example_templates/markdown_table_template.html.erb", 44 | "code_example_templates/js_example_template.html.erb", 45 | "code_example_templates/jsx_example_template.html.erb", 46 | ] 47 | DisplayMessage.created(new_files) 48 | end 49 | 50 | def initialize(options, extra_args = []) 51 | @pages = {} 52 | @errors = [] 53 | @dependencies = options.fetch('dependencies', nil) || [] 54 | @index = options['index'] 55 | @base_path = options.fetch('base_path', Dir.pwd) 56 | @renderer = options.fetch('renderer', MarkdownRenderer) 57 | @source = Array(options['source']) 58 | @destination = options['destination'] 59 | @documentation_assets = options['documentation_assets'] 60 | @config_yml = options['config_yml'] 61 | @plugins = Plugins.new(options.fetch('config_yml', {}), extra_args) 62 | @nav_level = options['nav_level'] || 'page' 63 | @exit_on_warnings = options['exit_on_warnings'] 64 | @code_example_templates = options['code_example_templates'] 65 | @code_example_renderers = options['code_example_renderers'] 66 | @custom_extensions = Array(options['custom_extensions']) 67 | @ignore_paths = options.fetch('ignore_paths', []) 68 | 69 | if @exit_on_warnings 70 | DisplayMessage.exit_on_warnings! 71 | end 72 | end 73 | 74 | def build 75 | set_dirs 76 | return false if !is_valid? 77 | 78 | set_header_footer 79 | current_path = Dir.pwd 80 | Dir.chdir(base_path) 81 | # Create the output directory if it doesn't exist 82 | if !output_dir 83 | FileUtils.mkdir_p(destination) 84 | set_dirs #need to reset output_dir post-creation for build_docs. 85 | end 86 | # the real work happens here. 87 | build_docs 88 | Dir.chdir(current_path) 89 | DisplayMessage.success("Build completed. (-: ") 90 | true 91 | end 92 | 93 | def is_valid? 94 | errors.clear 95 | set_dirs 96 | validate_source 97 | validate_destination 98 | validate_document_assets 99 | 100 | errors.empty? 101 | end 102 | 103 | private 104 | 105 | def validate_source 106 | errors << "No source directory specified in the config file" if source.empty? 107 | source.each do |dir| 108 | next if real_path(dir) 109 | errors << "Can not read source directory (#{dir}), does it exist?" 110 | end 111 | end 112 | 113 | def validate_destination 114 | errors << "No destination directory specified in the config" if !destination 115 | end 116 | 117 | def validate_document_assets 118 | errors << "No documentation assets directory specified" if !documentation_assets 119 | end 120 | 121 | def set_dirs 122 | @output_dir = real_path(destination) 123 | @doc_assets_dir = real_path(documentation_assets) 124 | @input_dir = multiple_paths(source) 125 | end 126 | 127 | def real_path(dir) 128 | return if !File.directory?(String(dir)) 129 | Pathname.new(dir).realpath 130 | end 131 | 132 | def multiple_paths dirs 133 | Array(dirs).map { |dir| real_path(dir) }.compact 134 | end 135 | 136 | def build_docs 137 | doc_parser = DocParser.new(input_dir, index, @plugins, nav_level: @nav_level, 138 | custom_extensions: @custom_extensions, 139 | ignore_paths: @ignore_paths) 140 | @pages, @categories = doc_parser.parse 141 | 142 | if index && !@pages.has_key?(index + '.html') 143 | DisplayMessage.warning("Could not generate index.html, there was no content generated for the category #{index}.") 144 | end 145 | 146 | warn_missing_doc_assets 147 | write_docs 148 | copy_dependencies 149 | copy_assets 150 | end 151 | 152 | def copy_assets 153 | return unless doc_assets_dir 154 | Dir.foreach(doc_assets_dir) do |item| 155 | # ignore . and .. directories and files that start with 156 | # underscore 157 | next if item == '.' or item == '..' or item.start_with?('_') 158 | FileUtils.rm "#{output_dir}/#{item}", force: true if File.file?("#{output_dir}/#{item}") 159 | FileUtils.rm_rf "#{output_dir}/#{item}" if File.directory?("#{output_dir}/#{item}") 160 | FileUtils.cp_r "#{doc_assets_dir}/#{item}", "#{output_dir}/#{item}" 161 | end 162 | end 163 | 164 | def copy_dependencies 165 | dependencies.each do |dir| 166 | begin 167 | dirpath = Pathname.new(dir).realpath 168 | if File.directory?("#{dir}") 169 | FileUtils.rm_r "#{output_dir}/#{dirpath.basename}", force: true 170 | FileUtils.cp_r "#{dirpath}", "#{output_dir}/#{dirpath.basename}" 171 | end 172 | rescue 173 | DisplayMessage.warning("Could not copy dependency: #{dir}") 174 | end 175 | end 176 | end 177 | 178 | def write_docs 179 | load_code_example_templates_and_renderers 180 | 181 | renderer_instance = renderer.new(link_helper: link_helper) 182 | markdown = Redcarpet::Markdown.new(renderer_instance, { fenced_code_blocks: true, tables: true }) 183 | tpl_vars = TemplateVariables.new({categories: @categories, config: @config_yml, pages: @pages}) 184 | #generate html from markdown 185 | @pages.each do |file_name, page| 186 | if file_name.nil? 187 | raise NoCategoryError 188 | else 189 | if page[:blocks] && page[:blocks].empty? 190 | title = '' 191 | else 192 | title, _ = @categories.rassoc(file_name) 193 | end 194 | 195 | tpl_vars.set_args({title: title, file_name: file_name, blocks: page[:blocks]}) 196 | if page.has_key?(:erb) 197 | write_erb(file_name, page[:erb], tpl_vars.get_binding) 198 | else 199 | write_page(file_name, markdown.render(page[:md]), tpl_vars.get_binding) 200 | end 201 | end 202 | end 203 | end 204 | 205 | def load_code_example_templates_and_renderers 206 | if @code_example_templates 207 | CodeExampleRenderer::Template.path_to_custom_example_templates = real_path(@code_example_templates) 208 | end 209 | 210 | if @code_example_renderers 211 | CodeExampleRenderer.path_to_custom_example_renderers = real_path(@code_example_renderers) 212 | end 213 | 214 | CodeExampleRenderer.load_renderers_and_templates 215 | end 216 | 217 | def link_helper 218 | @_link_helper ||= LinkHelper.new(@pages.map { |page| 219 | if not page[1][:blocks].nil? 220 | { 221 | name: page[0], 222 | component_names: page[1][:blocks].map { |component| component[:name] } 223 | } 224 | else 225 | { 226 | name: page[0], 227 | component_names: {} 228 | } 229 | end 230 | }) 231 | end 232 | 233 | def write_erb(file_name, content, binding) 234 | fh = get_fh(output_dir, file_name) 235 | erb = ERB.new(content) 236 | fh.write(erb.result(binding)) 237 | ensure 238 | fh.close 239 | end 240 | 241 | def write_page(file_name, body, binding) 242 | fh = get_fh(output_dir, file_name) 243 | fh.write(header_erb.result(binding)) if header_erb 244 | fh.write(body) 245 | fh.write(footer_erb.result(binding)) if footer_erb 246 | ensure 247 | fh.close 248 | end 249 | 250 | def set_header_footer 251 | # load the markdown renderer we are going to use 252 | 253 | if File.exists?("#{doc_assets_dir}/_header.html") 254 | @header_erb = ERB.new(File.read("#{doc_assets_dir}/_header.html")) 255 | elsif File.exists?("#{doc_assets_dir}/header.html") 256 | @header_erb = ERB.new(File.read("#{doc_assets_dir}/header.html")) 257 | else 258 | @header_erb = nil 259 | DisplayMessage.warning("No _header.html found in documentation assets. Without this your css/header will not be included on the generated pages.") 260 | end 261 | 262 | if File.exists?("#{doc_assets_dir}/_footer.html") 263 | @footer_erb = ERB.new(File.read("#{doc_assets_dir}/_footer.html")) 264 | elsif File.exists?("#{doc_assets_dir}/footer.html") 265 | @footer_erb = ERB.new(File.read("#{doc_assets_dir}/footer.html")) 266 | else 267 | @footer_erb = nil 268 | DisplayMessage.warning("No _footer.html found in documentation assets. This might be okay to ignore...") 269 | end 270 | end 271 | 272 | def get_file_name(str) 273 | str.gsub(' ', '_').downcase + '.html' 274 | end 275 | 276 | def get_fh(output_dir, output_file) 277 | File.open("#{output_dir}/#{output_file}", 'w') 278 | end 279 | 280 | def warn_missing_doc_assets 281 | return if doc_assets_dir 282 | DisplayMessage.warning("Could not find documentation assets at #{documentation_assets}") 283 | end 284 | end 285 | end 286 | -------------------------------------------------------------------------------- /spec/fixtures/styleguide/base_css.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | My Style Guide Base CSS 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 37 | 38 |
    39 |
    40 |
    41 | 42 |
    43 |
    44 |
    45 | 54 |
    55 |
    56 |
    57 | 58 |
    59 | 60 | 61 | 62 |

    Background Colors

    63 |

    We have a few background colors that can be used in various contexts. 64 | These are not for use as the entire page background but instead for 65 | specific components and modules on the page.

    66 |
    67 |
    68 |
    69 | backgroundLowlight 70 |
    71 |
    72 |
    73 | backgroundHighlight 74 |
    75 |
    76 |
    77 | backgroundBasic 78 |
    79 |
    80 |
    81 | backgroundInverse 82 |
    83 |
    84 | 85 |

    Buttons

    86 |

    Button styles can be applied to any element. Typically you'll want to 87 | use either a <button> or an <a> element:

    88 |
    89 | 90 | Don't click 91 | 92 |
    93 |
    94 |
    95 |
    <button class="btn btnDefault">Click</button>
     96 | <a class="btn btnDefault" href="http://trulia.com">Don't click</a>
    97 |
    98 |
    99 |
    100 |

    If your button is actually a link to another page, please use the 101 | <a> element, while if your button performs an action, such as submitting 102 | a form or triggering some javascript event, then use a <button> element.

    103 |

    Button Sizes

    104 |

    There are three 3 sizes for buttons: Large, medium (default) 105 | and small. Simply apply the size modifier class for the desired size. 106 | There is also an additional modifier that will make the button take the 107 | full width of the container. It may be used with the any of the button 108 | size and style modifiers.

    109 | 110 | 111 | 112 | 113 | 118 | 125 | 126 | 127 | 128 | 133 | 140 | 141 | 142 | 143 | 148 | 155 | 156 | 157 | 158 | 163 | 170 | 171 | 172 | 173 |
    114 |
    115 | 116 |
    117 |
    119 |
    120 |
    121 |
    button.btn.btnDefault.btnLrg Large
    122 |
    123 |
    124 |
    129 |
    130 | 131 |
    132 |
    134 |
    135 |
    136 |
    button.btn.btnDefault Default
    137 |
    138 |
    139 |
    144 |
    145 | 146 |
    147 |
    149 |
    150 |
    151 |
    button.btn.btnDefault.btnSml Small
    152 |
    153 |
    154 |
    159 |
    160 | 161 |
    162 |
    164 |
    165 |
    166 |
    button.btn.btnDefault.btnFullWidth Full width
    167 |
    168 |
    169 |
    174 |
    175 | 176 | 177 |

    Button Styles

    178 |

    For buttons, see the buttons docs.

    179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 |
    ButtonClassDescription
    btn btnDefaultThis is what buttons look like, this is the go to button style.
    btn btnPrimaryUse this button as the primary call to action
    btn btnDangerThis button is for delete actions, these actions should also have a confirmation dialog
    btn btnDisabledI'm afraid I can't let you do that, yet.
    204 | 205 | 206 | 207 | 208 | 214 | 221 | 222 | 223 | 224 | 230 | 237 | 238 | 239 | 240 | 246 | 253 | 254 | 255 | 256 | 262 | 269 | 270 | 271 | 272 |
    209 |
    210 | 211 | 212 |
    213 |
    215 |
    216 |
    217 |
    %button.btn.btnDefault Hi
    218 |
    219 |
    220 |
    225 |
    226 | 227 | 228 |
    229 |
    231 |
    232 |
    233 |
    %button.btn.btnPrimary Hi
    234 |
    235 |
    236 |
    241 |
    242 | 243 | 244 |
    245 |
    247 |
    248 |
    249 |
    %button.btn.btnDanger Hi
    250 |
    251 |
    252 |
    257 |
    258 | 259 | 260 |
    261 |
    263 |
    264 |
    265 |
    %button.btn.btnDisabled Hi
    266 |
    267 |
    268 |
    273 |
    274 | 275 |

    Here's an example combining a style with a size

    276 |
    277 | 278 | 279 |
    280 |
    281 |
    282 |
    %button.btn.btnPrimary.btnFullWidth Hi
    283 |
    284 |
    285 |
    286 |
    287 |
    288 |
    289 |
    290 | The source code for this style guide is licensed under the MIT license. 291 |
    292 |
    293 | 294 | 295 | -------------------------------------------------------------------------------- /spec/block_code_renderer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'hologram/block_code_renderer' 3 | require 'haml' 4 | require 'securerandom' 5 | 6 | Hologram::CodeExampleRenderer.load_renderers_and_templates 7 | 8 | describe Hologram::BlockCodeRenderer do 9 | describe '#render' do 10 | subject { Hologram::BlockCodeRenderer.new(code, markdown_language).render.strip } 11 | 12 | context 'expected language' do 13 | context 'react' do 14 | let(:language) { 'react' } 15 | let(:code) { 'Example' } 16 | 17 | context 'when the language is a react_example' do 18 | let(:markdown_language) { 'react_example' } 19 | let(:div_id) { 'randomId' } 20 | 21 | before :each do 22 | SecureRandom.stub('hex').and_return(div_id); 23 | end 24 | 25 | it { is_expected.to eq [ 26 | "
    ", 27 | "
    ", 28 | "
    ", 29 | "", 35 | "
    ", 36 | "
    ", 37 | "
    ", 38 | "
    <ReactExample property=\"value\">Example</ReactExample>
    ", 39 | "
    ", 40 | "
    ", 41 | "
    " 42 | ].join("\n") } 43 | end 44 | end 45 | 46 | context 'slim' do 47 | let(:language) { 'slim' } 48 | let(:code) { 'h1 Markup Example' } 49 | 50 | context 'when the language is a slim_example' do 51 | let(:markdown_language) { 'slim_example' } 52 | 53 | it { is_expected.to eq [ 54 | "
    ", 55 | "
    ", 56 | "

    Markup Example

    ", 57 | "
    ", 58 | "
    ", 59 | "
    ", 60 | "
    h1 Markup Example
    ", 61 | "
    ", 62 | "
    ", 63 | "
    ", 64 | ].join("\n") } 65 | end 66 | end 67 | 68 | context 'haml' do 69 | let(:language) { 'haml' } 70 | let(:code) { '%h1 Example' } 71 | 72 | context 'when the language is a haml_example' do 73 | let(:markdown_language) { 'haml_example' } 74 | 75 | it { is_expected.to eq [ 76 | "
    ", 77 | "
    ", 78 | "

    Example

    \n", 79 | "
    ", 80 | "
    ", 81 | "
    ", 82 | "
    %h1 Example
    ", 83 | "
    ", 84 | "
    ", 85 | "
    ", 86 | ].join("\n") } 87 | end 88 | 89 | context 'when the language is a haml_example_table' do 90 | let(:markdown_language) { 'haml_example_table' } 91 | let(:code) { [ 92 | ".spinner-lg", 93 | " %i.fa.fa-spin", 94 | "", 95 | "%h1 Example" 96 | ].join("\n") } 97 | 98 | it { is_expected.to eq [ 99 | "
    ", 100 | " ", 101 | " ", 102 | " ", 103 | " ", 104 | " ", 112 | " ", 120 | " ", 121 | " ", 122 | " ", 123 | " ", 129 | " ", 136 | " ", 137 | " ", 138 | " ", 139 | "
    ", 105 | "
    ", 106 | "
    ", 107 | " ", 108 | "
    ", 109 | "", 110 | "
    ", 111 | "
    ", 113 | "
    ", 114 | "
    ", 115 | "
    .spinner-lg",
    116 |             "  %i.fa.fa-spin
    ", 117 | "
    ", 118 | "
    ", 119 | "
    ", 124 | "
    ", 125 | "

    Example

    ", 126 | "", 127 | "
    ", 128 | "
    ", 130 | "
    ", 131 | "
    ", 132 | "
    %h1 Example
    ", 133 | "
    ", 134 | "
    ", 135 | "
    ", 140 | "
    " 141 | ].join("\n") } 142 | end 143 | end 144 | 145 | context 'html' do 146 | let(:language) { 'html' } 147 | let(:code) { '

    ' } 148 | let(:formatted_code) { 'formatted h2' } 149 | context 'when the language is html_example' do 150 | let(:markdown_language) { 'html_example' } 151 | 152 | it { is_expected.to eq [ 153 | "
    ", 154 | "
    ", 155 | "

    ", 156 | "
    ", 157 | "
    ", 158 | "
    ", 159 | "
    <h2></h2>
    ", 160 | "
    ", 161 | "
    ", 162 | "
    ", 163 | ].join("\n") } 164 | end 165 | 166 | context 'when the language is a html_example_table' do 167 | let(:markdown_language) { 'html_example_table' } 168 | let(:code) { [ 169 | "
    ", 170 | "", 171 | "

    Example

    " 172 | ].join("\n") } 173 | 174 | it { is_expected.to eq [ 175 | "
    ", 176 | " ", 177 | " ", 178 | " ", 179 | " ", 180 | " ", 185 | " ", 192 | " ", 193 | " ", 194 | " ", 195 | " ", 200 | " ", 207 | " ", 208 | " ", 209 | " ", 210 | "
    ", 181 | "
    ", 182 | "
    ", 183 | "
    ", 184 | "
    ", 186 | "
    ", 187 | "
    ", 188 | "
    <div class='spinner-lg'></div>
    ", 189 | "
    ", 190 | "
    ", 191 | "
    ", 196 | "
    ", 197 | "

    Example

    ", 198 | "
    ", 199 | "
    ", 201 | "
    ", 202 | "
    ", 203 | "
    <h1>Example</h1>
    ", 204 | "
    ", 205 | "
    ", 206 | "
    ", 211 | "
    " 212 | ].join("\n") } 213 | end 214 | end 215 | 216 | context 'js_example' do 217 | let(:language) { 'js' } 218 | let(:markdown_language) { 'js_example' } 219 | let(:code) { '$(document).ready(function() {});' } 220 | 221 | it "inserts the code into the docs so that it will run and make the example work" do 222 | expect(subject).to include "" 223 | end 224 | 225 | it { is_expected.to include [ 226 | "
    ", 227 | "
    ", 228 | "
    $(document).ready(function() {});
    ", 229 | "
    ", 230 | "
    ", 231 | ].join("\n") } 232 | end 233 | 234 | context 'jsx_example' do 235 | let(:language) { 'jsx' } 236 | let(:markdown_language) { 'jsx_example' } 237 | let(:code) { '$(document).ready(function () { React.render(
    ) });' } 238 | 239 | it "inserts the code into the docs so that it will run and make the example work" do 240 | expect(subject).to include "" 241 | end 242 | 243 | it { is_expected.to include [ 244 | "
    ", 245 | "
    ", 246 | "
    $(document).ready(function () { React.render(<div className=\"foo\"></div>) });
    ", 247 | "
    ", 248 | "
    ", 249 | ].join("\n") } 250 | end 251 | end 252 | 253 | context 'unexpected language' do 254 | let(:markdown_language) { 'fortran' } 255 | let(:code) { 'goto 12' } 256 | 257 | it { is_expected.to eq [ 258 | "
    ", 259 | "
    ", 260 | "
    goto 12
    ", 261 | "
    ", 262 | "
    ", 263 | ].join("\n") } 264 | end 265 | 266 | context 'no language' do 267 | let(:markdown_language) { nil } 268 | let(:code) { 'unknown code' } 269 | 270 | it { is_expected.to eq [ 271 | "
    ", 272 | "
    ", 273 | "
    unknown code
    ", 274 | "
    ", 275 | "
    ", 276 | ].join("\n") } 277 | end 278 | end 279 | end 280 | -------------------------------------------------------------------------------- /spec/fixtures/source/extra/css/screen.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8";article,aside,details,figcaption,figure,footer,header,hgroup,nav,section,summary{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none;height:0}[hidden]{display:none}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:focus{outline:thin dotted}a:active,a:hover{outline:0}b,strong{font-weight:bold}dfn{font-style:italic}pre{white-space:pre;white-space:pre-wrap;word-wrap:break-word}q{quotes:none}q:before,q:after{content:'';content:none}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}dl,dt,dd,menu,ol,ul{margin:0;padding:0;list-style-type:none}img{border:0;-ms-interpolation-mode:bicubic}svg:not(:root){overflow:hidden}figure{margin:0}form{margin:0}fieldset{margin:0;padding:0}legend{border:0;padding:0;white-space:normal;*margin-left:-7px}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0;*height:13px;*width:13px}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}textarea{overflow:auto;vertical-align:top}table{border-collapse:collapse;border-spacing:0}th{font-weight:normal}.main{overflow:hidden;*overflow:visible;zoom:1}.line .lastCol{display:table-cell;vertical-align:top;width:10000px!important;*display:block;*zoom:1;*width:auto!important}.line,.box,.box .boxHead,.box .boxFoot,.content>section,.container{zoom:1}.line:before,.box:before,.box .boxHead:before,.box .boxFoot:before,.content>section:before,.container:before,.line:after,.box:after,.box .boxHead:after,.box .boxFoot:after,.content>section:after,.container:after{content:" ";display:table}.line:after,.box:after,.box .boxHead:after,.box .boxFoot:after,.content>section:after,.container:after{clear:both}.listInline>li{display:inline-block;*display:inline;*zoom:1}.hideVisually{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.hideVisually.focusable:active,.hideVisually.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.hideText{background-color:transparent;border:0;overflow:hidden;*text-indent:-9999px}.hideText:before{content:"";display:block;width:0;height:100%}.hideFully{display:none!important;visibility:hidden}.txtC,table .txtC,table tr .txtC{text-align:center}.txtL,table .txtL,table tr .txtL{text-align:left}.txtR,table .txtR,table tr .txtR{text-align:right}.txtT,table .txtT,table tr .txtT{vertical-align:top}.txtB,table .txtB,table tr .txtB{vertical-align:bottom}.txtM,table .txtM,table tr .txtM{vertical-align:middle}p,.headingDoubleSub,ol,ul,.table{margin-top:5px;margin-bottom:5px}.box{margin-top:21px;margin-bottom:21px}table h1,table h2,table h3,table h4,table h5,table h6,table p,table ul,table ol,table dl,table blockquote,table .media,table pre{margin-left:0;margin-right:0}.pan{padding:0!important}.man{margin:0!important}.pas{padding:5px!important}.mas{margin:5px!important}.pam{padding:10px!important}.mam{margin:10px!important}.pal{padding:21px!important}.mal{margin:21px!important}.pvn{padding-top:0!important;padding-bottom:0!important}.mvn{margin-top:0!important;margin-bottom:0!important}.pvs{padding-top:5px!important;padding-bottom:5px!important}.mvs{margin-top:5px!important;margin-bottom:5px!important}.pvm{padding-top:10px!important;padding-bottom:10px!important}.mvm{margin-top:10px!important;margin-bottom:10px!important}.pvl{padding-top:21px!important;padding-bottom:21px!important}.mvl{margin-top:21px!important;margin-bottom:21px!important}.phn{padding-left:0!important;padding-right:0!important}.mhn{margin-left:0!important;margin-right:0!important}.phs{padding-left:5px!important;padding-right:5px!important}.mhs{margin-left:5px!important;margin-right:5px!important}.phm{padding-left:10px!important;padding-right:10px!important}.mhm{margin-left:10px!important;margin-right:10px!important}.phl{padding-left:21px!important;padding-right:21px!important}.mhl{margin-left:21px!important;margin-right:21px!important}.ptn{padding-top:0!important}.mtn{margin-top:0!important}.pts{padding-top:5px!important}.mts{margin-top:5px!important}.ptm{padding-top:10px!important}.mtm{margin-top:10px!important}.ptl{padding-top:21px!important}.mtl{margin-top:21px!important}.prn{padding-right:0!important}.mrn{margin-right:0!important}.prs{padding-right:5px!important}.mrs{margin-right:5px!important}.prm{padding-right:10px!important}.mrm{margin-right:10px!important}.prl{padding-right:21px!important}.mrl{margin-right:21px!important}.pbn{padding-bottom:0!important}.mbn{margin-bottom:0!important}.pbs{padding-bottom:5px!important}.mbs{margin-bottom:5px!important}.pbm{padding-bottom:10px!important}.mbm{margin-bottom:10px!important}.pbl{padding-bottom:21px!important}.mbl{margin-bottom:21px!important}.pln{padding-left:0!important}.mln{margin-left:0!important}.pls{padding-left:5px!important}.mls{margin-left:5px!important}.plm{padding-left:10px!important}.mlm{margin-left:10px!important}.pll{padding-left:21px!important}.mll{margin-left:21px!important}@font-face{font-family:"OpenSans";src:url("http://static.trulia-cdn.com/images/fonts/OpenSans/OpenSans-Light-webfont.eot");src:url("http://static.trulia-cdn.com/images/fonts/OpenSans/OpenSans-Light-webfont.eot?#iefix") format("embedded-opentype"),url("http://static.trulia-cdn.com/images/fonts/OpenSans/OpenSans-Light-webfont.woff") format("woff"),url("http://static.trulia-cdn.com/images/fonts/OpenSans/OpenSans-Light-webfont.ttf") format("truetype"),url("http://static.trulia-cdn.com/images/fonts/OpenSans/OpenSans-Light-webfont.svg#OpenSansLight") format("svg");font-weight:300;font-style:normal}@font-face{font-family:"OpenSans";src:url("http://static.trulia-cdn.com/images/fonts/OpenSans/OpenSans-Regular-webfont.eot");src:url("http://static.trulia-cdn.com/images/fonts/OpenSans/OpenSans-Regular-webfont.eot?#iefix") format("embedded-opentype"),url("http://static.trulia-cdn.com/images/fonts/OpenSans/OpenSans-Regular-webfont.woff") format("woff"),url("http://static.trulia-cdn.com/images/fonts/OpenSans/OpenSans-Regular-webfont.ttf") format("truetype"),url("http://static.trulia-cdn.com/images/fonts/OpenSans/OpenSans-Regular-webfont.svg#OpenSansRegular") format("svg");font-weight:400;font-style:normal}@font-face{font-family:"OpenSans";src:url("http://static.trulia-cdn.com/images/fonts/OpenSans/OpenSans-Bold-webfont.eot");src:url("http://static.trulia-cdn.com/images/fonts/OpenSans/OpenSans-Bold-webfont.eot?#iefix") format("embedded-opentype"),url("http://static.trulia-cdn.com/images/fonts/OpenSans/OpenSans-Bold-webfont.woff") format("woff"),url("http://static.trulia-cdn.com/images/fonts/OpenSans/OpenSans-Bold-webfont.ttf") format("truetype"),url("http://static.trulia-cdn.com/images/fonts/OpenSans/OpenSans-Bold-webfont.svg#OpenSansBold") format("svg");font-weight:600;font-style:normal}body{font-size:14px;font-size:.875rem;line-height:1.5em;color:#222}body,input,select,textarea,button{font-family:OpenSans,sans-serif}h1,.h1,h2,.h2,h3,.h3,h4,.h4,h5,.h5,h6,.h6{font-weight:300;font-style:normal}h1,.h1{font-size:36px;font-size:2.25rem;line-height:1.3;margin:5px 0;font-weight:bold}h2,.h2{font-size:28px;font-size:1.75rem;line-height:1.4;margin:5px 0}h3,.h3{font-size:24px;font-size:1.5rem;line-height:1.4;margin:5px 0}h4,.h4{font-size:20px;font-size:1.25rem;line-height:1.4;margin:5px 0}h5,.h5{font-size:16px;font-size:1rem;line-height:1.5;margin:5px 0;font-weight:bold}h6,.h6{font-size:14px;font-size:.875rem;line-height:1.5;margin:5px 0;font-weight:bold}.h7{font-size:12px;font-size:.75rem;line-height:1.6;margin:5px 0}.headingDoubleSuper{display:block;clear:both;min-width:10px}.headingDoubleSub{font-size:14px;font-size:.875rem;display:block;float:left;font-weight:normal;line-height:1.5}.headingDoubleInline{display:inline-block}pre,code{font-family:Menlo,Consolas,Monaco,"Lucida Console",monospace}.typeWarning{color:#cc2114}.typeHighlight{color:#5eab1f}.typeLowlight{color:#999}.typeReversed1{color:white}.typeReversed2{color:#eff6e9}.typeDeemphasize{font-weight:300!important}.typeWeightNormal{font-weight:normal!important}.typeEmphasize{font-weight:bold!important}.cols1{width:4.16667%!important;*width:2.30496%!important}.cols2{width:8.33333%!important;*width:6.47163%!important}.cols3{width:12.5%!important;*width:10.6383%!important}.cols4{width:16.66667%!important;*width:14.80496%!important}.cols5{width:20.83333%!important;*width:18.97163%!important}.cols6{width:25%!important;*width:23.1383%!important}.cols7{width:29.16667%!important;*width:27.30496%!important}.cols8{width:33.33333%!important;*width:31.47163%!important}.cols9{width:37.5%!important;*width:35.6383%!important}.cols10{width:41.66667%!important;*width:39.80496%!important}.cols11{width:45.83333%!important;*width:43.97163%!important}.cols12{width:50%!important;*width:48.1383%!important}.cols13{width:54.16667%!important;*width:52.30496%!important}.cols14{width:58.33333%!important;*width:56.47163%!important}.cols15{width:62.5%!important;*width:60.6383%!important}.cols16{width:66.66667%!important;*width:64.80496%!important}.cols17{width:70.83333%!important;*width:68.97163%!important}.cols18{width:75%!important;*width:73.1383%!important}.cols19{width:79.16667%!important;*width:77.30496%!important}.cols20{width:83.33333%!important;*width:81.47163%!important}.cols21{width:87.5%!important;*width:85.6383%!important}.cols22{width:91.66667%!important;*width:89.80496%!important}.cols23{width:95.83333%!important;*width:93.97163%!important}.cols24{width:100%!important;*width:98.1383%!important}.line{margin-left:-21px}.line .col{min-height:1px;float:left;zoom:1;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding-left:21px}.line .colExt{float:right;zoom:1;padding:0 0 0 21px}.line .lastCol{float:none;*display:block;*width:auto!important;*zoom:1}.line .col:last-child{display:table-cell;float:none;vertical-align:top;width:10000px!important}a,a:hover,a:focus,.linkLowlight{text-decoration:none}a{color:#1885f0}a:hover,a:focus{color:#5eab1f}.linkLowlight{color:#999}.linkLowlight:hover,.linkLowlight:focus{color:#5eab1f}.linkForward:after{margin-left:.35714em;content:"\00BB"}.linkBack:before{margin-right:.35714em;content:"\00AB"}.backgroundBasic{background-color:white}.backgroundLowlight{background-color:#f9f9f9}.backgroundHighlight{background-color:#5eab1f}.backgroundInverse{background-color:#222}.box{position:relative}.box .boxHead,.box .boxFoot{padding:0;margin:0 21px}.box .boxBody{padding:0 21px 1px}.boxHighlight{border:1px solid #e9e9e9;-webkit-border-radius:6px;-moz-border-radius:6px;-ms-border-radius:6px;-o-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 0 5px rgba(0,0,0,0.17);-moz-box-shadow:0 0 5px rgba(0,0,0,0.17);box-shadow:0 0 5px rgba(0,0,0,0.17)}.boxBasic{border:1px solid #e9e9e9;-webkit-border-radius:6px;-moz-border-radius:6px;-ms-border-radius:6px;-o-border-radius:6px;border-radius:6px}.boxHeadBasic{border-bottom:solid 1px #e9e9e9}.boxFootBasic{border-top:solid 1px #e9e9e9}.boxClose{border:0;position:absolute;cursor:pointer;background-color:transparent}.boxClose{top:3px;right:6px;width:20px;height:20px;text-align:center;color:#999;font-size:14px;font-size:.875rem}.boxClose:hover,.boxClose:focus{color:#222}ol,ul{list-style-type:none}ol>li,ul>li{margin:.2em 0}.listBordered>li,.listBorderedHover>li{padding:10px;border-top:1px solid #e9e9e9}.listBordered>li:first-child,.listBorderedHover>li:first-child{border-top:1px solid transparent}.listBorderedHover>li{margin:0}.listBorderedHover>li:hover{background-color:#f9f9f9}.listInline>li{vertical-align:middle;padding-right:10px}.lvn>li{padding-top:0;padding-bottom:0}.lvs>li{padding-top:5px;padding-bottom:5px}.lvm>li{padding-top:10px;padding-bottom:10px}.lvl>li{padding-top:21px;padding-bottom:21px}.lhn>li{padding-left:0;padding-right:0}.lhs>li{padding-left:5px;padding-right:5px}.lhm>li{padding-left:10px;padding-right:10px}.lhl>li{padding-left:21px;padding-right:21px}.toggle{position:relative;cursor:pointer}.toggleArrow .before,.toggleArrow:before{content:"\25BA";display:inline-block;width:1.3em;font-size:.9em;text-align:center}.toggleArrowActive:before{content:"\25BC"}.toggleArrow:hover .before{text-decoration:none}.toggleArrow .active,.toggleActive .inactive{display:none}.toggleActive .active{display:inline-block}.toggleActive .inactive{display:none}.frameThumb,.frameSmall,.frameStandard,.frameStacked{display:inline-block;*display:inline;*zoom:1;*border:1px solid #e9e9e9;-webkit-box-shadow:0 0 1px 1px rgba(0,0,0,0.2);-moz-box-shadow:0 0 1px 1px rgba(0,0,0,0.2);box-shadow:0 0 1px 1px rgba(0,0,0,0.2)}.frameThumb img,.frameSmall img,.frameStandard img,.frameStacked img{display:block}.frameThumb .polaroid,.frameSmall .polaroid,.frameStandard .polaroid,.frameStacked .polaroid{margin-top:5px}.frameThumb{padding:2px}.frameSmall{padding:6px}.frameStandard{padding:8px}.frameStacked{padding:8px;position:relative;background-color:#fff}.frameStacked:before,.frameStacked:after{-webkit-box-shadow:0 0 1px 1px rgba(0,0,0,0.2);-moz-box-shadow:0 0 1px 1px rgba(0,0,0,0.2);box-shadow:0 0 1px 1px rgba(0,0,0,0.2);width:100%;height:100%;position:absolute;z-index:-999;content:"";background-color:#f9f9f9}.frameStacked:before{left:5px;top:0;-webkit-transform:rotate(1deg);-moz-transform:rotate(1deg);-ms-transform:rotate(1deg);-o-transform:rotate(1deg);transform:rotate(1deg)}.frameStacked:after{right:5px;top:0;-webkit-transform:rotate(-1deg);-moz-transform:rotate(-1deg);-ms-transform:rotate(-1deg);-o-transform:rotate(-1deg);transform:rotate(-1deg)}.badgeStandard,.badgePrimary,.badgeSecondary,.badgeTertiary{-webkit-border-radius:3px;-moz-border-radius:3px;-ms-border-radius:3px;-o-border-radius:3px;border-radius:3px;font-size:12px;font-size:.75rem;padding:.35714em .42857em .21429em;display:inline-block;color:white;line-height:1;text-transform:uppercase}.badgeStandard{background-color:#999}.badgePrimary{background-color:#ff5c00}.badgeSecondary{background-color:#5eab1f}.badgeTertiary{background-color:#cc2114}button{background:0;border:0}.btn{padding:.5em 1em;margin:0;display:inline-block;font-weight:normal;line-height:normal;font-size:14px;font-size:.875rem;text-decoration:none;cursor:pointer;border:0;background:0;text-align:center;border:1px solid #ccc;-webkit-border-radius:4px;-moz-border-radius:4px;-ms-border-radius:4px;-o-border-radius:4px;border-radius:4px}.btnSml{font-size:12px;font-size:.75rem;-webkit-border-radius:4px;-moz-border-radius:4px;-ms-border-radius:4px;-o-border-radius:4px;border-radius:4px}.btnLrg{font-size:16px;font-size:1rem;line-height:1.6;-webkit-border-radius:4px;-moz-border-radius:4px;-ms-border-radius:4px;-o-border-radius:4px;border-radius:4px}.btnFullWidth{width:100%}.btnDefault,a.btnDefault{border:1px #d7d7d7 solid;background:#f3f3f3;color:#222;text-shadow:0 1px 0 white;-webkit-box-shadow:0 1px 0 #ccc;-moz-box-shadow:0 1px 0 #ccc;box-shadow:0 1px 0 #ccc}.btnDefault:hover,.btnDefault:focus{background:#f6f6f6}.btnDefault:active{position:relative;top:1px;-webkit-box-shadow:inset 0 3px 7px rgba(0,0,0,0.1),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 3px 7px rgba(0,0,0,0.1),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 3px 7px rgba(0,0,0,0.1),0 1px 2px rgba(0,0,0,0.05)}.btnPrimary,a.btnPrimary,.btnDanger,a.btnDanger{color:white;text-shadow:0}.btnPrimary,a.btnPrimary{border:1px #d14b00 solid;background:#ff5c00;-webkit-box-shadow:0 1px 0 #d35500;-moz-box-shadow:0 1px 0 #d35500;box-shadow:0 1px 0 #d35500}.btnPrimary:hover,.btnPrimary:focus{background:#ff660f}.btnPrimary:active{position:relative;top:1px;-webkit-box-shadow:inset 0 3px 7px rgba(0,0,0,0.1),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 3px 7px rgba(0,0,0,0.1),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 3px 7px rgba(0,0,0,0.1),0 1px 2px rgba(0,0,0,0.05)}.btnDanger,a.btnDanger{border:1px #a21a10 solid;background:#cc2114;-webkit-box-shadow:0 1px 0 #d35500;-moz-box-shadow:0 1px 0 #d35500;box-shadow:0 1px 0 #d35500}.btnDanger:hover,.btnDanger:focus{background:#da2315}.btnDanger:active{position:relative;top:1px;-webkit-box-shadow:inset 0 3px 7px rgba(0,0,0,0.1),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 3px 7px rgba(0,0,0,0.1),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 3px 7px rgba(0,0,0,0.1),0 1px 2px rgba(0,0,0,0.05)}.btn[disabled],.btn[disabled]:hover,.btn[disabled]:focus .btn[disabled]:active,.btnDisabled,a.btnDisabled,.btnDisabled:hover,.btnDisabled:focus,.btnDisabled:active{border:0;background:#ccc;color:#999;text-shadow:0;cursor:default}.btn[disabled]:active,.btnDisabled:active{position:static;color:#999;box-shadow:none}.btnLink{color:#1885f0;border:1px solid transparent}.btnLink:hover,.btnLink:focus{color:#5eab1f}.table{width:100%}.table thead th{font-weight:bold;color:#222}.table th,.table td{padding:8px}.tableBasic th,.tableBasic td{border:1px solid #e9e9e9}.tan td,.tan th{padding:0}.tas td,.tas th{padding:5px}.tam td,.tam th{padding:10px}.tal td,.tal th{padding:21px}.tvn td,.tvn th{padding-top:0;padding-bottom:0}.tvs td,.tvs th{padding-top:5px;padding-bottom:5px}.tvm td,.tvm th{padding-top:10px;padding-bottom:10px}.tvl td,.tvl th{padding-top:21px;padding-bottom:21px}.thn td,.thn th{padding-left:0;padding-right:0}.ths td,.ths th{padding-left:5px;padding-right:5px}.thm td,.thm th{padding-left:10px;padding-right:10px}.thl td,.thl th{padding-left:21px;padding-right:21px}body{background:white}.content>section,.container{width:1128px;margin:0 auto}.main[role="main"]{position:relative;top:-10px}.content>section,.content .container{margin-top:0}.skipLink:focus{width:100%;padding:5px 0;display:block;position:absolute;z-index:100;text-indent:5px;color:#fff;background:#5eab1f}.header{padding-bottom:15px;*position:relative;*z-index:100;background:white;border-bottom:1px solid #ccc}.headerSubSection{margin:1px 0;font-size:11px;font-size:.6875rem;text-align:right;line-height:1}.headerSubSection a{color:#222}.headerSubSection .highlight{display:inline-block;padding:2px 10px;color:#fff;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;-ms-border-radius:5px 5px 0 0;-o-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0;background:#888}.headerSubSection .listInline{margin:0}.headerMainSection{position:relative}.headerMainSection .logo{position:absolute;left:0;top:-1px}.menuWrap{margin-left:97px}.content{padding-top:5px}.content>section{margin-top:5px}.sideBar{width:185px;margin-right:20px;float:left}.main{float:none;*display:block;*width:auto;*zoom:1}.aside{width:300px;margin-left:20px;float:right}.footer{clear:both;margin-top:40px;font-size:12px;font-size:.75rem}.footerBox{border-top:5px solid #5eab1f}.footerBox .boxBody{margin:10px 0}.footerGroup a{color:#222}.footerGroup a:hover,.footerGroup a:focus{color:#5eab1f;text-decoration:underline}.footerCol{width:125px}.footerGroupHeading{float:left}.footerGroupHeading{width:120px;margin:0;clear:right;font-weight:bold}.footerGroupList{margin:0}.footerGroupList>li{margin:0}.footerDisclaimer{float:left}.footerNoteAside{float:right} 2 | -------------------------------------------------------------------------------- /spec/fixtures/styleguide/extra/css/screen.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8";article,aside,details,figcaption,figure,footer,header,hgroup,nav,section,summary{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none;height:0}[hidden]{display:none}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:focus{outline:thin dotted}a:active,a:hover{outline:0}b,strong{font-weight:bold}dfn{font-style:italic}pre{white-space:pre;white-space:pre-wrap;word-wrap:break-word}q{quotes:none}q:before,q:after{content:'';content:none}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}dl,dt,dd,menu,ol,ul{margin:0;padding:0;list-style-type:none}img{border:0;-ms-interpolation-mode:bicubic}svg:not(:root){overflow:hidden}figure{margin:0}form{margin:0}fieldset{margin:0;padding:0}legend{border:0;padding:0;white-space:normal;*margin-left:-7px}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0;*height:13px;*width:13px}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}textarea{overflow:auto;vertical-align:top}table{border-collapse:collapse;border-spacing:0}th{font-weight:normal}.main{overflow:hidden;*overflow:visible;zoom:1}.line .lastCol{display:table-cell;vertical-align:top;width:10000px!important;*display:block;*zoom:1;*width:auto!important}.line,.box,.box .boxHead,.box .boxFoot,.content>section,.container{zoom:1}.line:before,.box:before,.box .boxHead:before,.box .boxFoot:before,.content>section:before,.container:before,.line:after,.box:after,.box .boxHead:after,.box .boxFoot:after,.content>section:after,.container:after{content:" ";display:table}.line:after,.box:after,.box .boxHead:after,.box .boxFoot:after,.content>section:after,.container:after{clear:both}.listInline>li{display:inline-block;*display:inline;*zoom:1}.hideVisually{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.hideVisually.focusable:active,.hideVisually.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.hideText{background-color:transparent;border:0;overflow:hidden;*text-indent:-9999px}.hideText:before{content:"";display:block;width:0;height:100%}.hideFully{display:none!important;visibility:hidden}.txtC,table .txtC,table tr .txtC{text-align:center}.txtL,table .txtL,table tr .txtL{text-align:left}.txtR,table .txtR,table tr .txtR{text-align:right}.txtT,table .txtT,table tr .txtT{vertical-align:top}.txtB,table .txtB,table tr .txtB{vertical-align:bottom}.txtM,table .txtM,table tr .txtM{vertical-align:middle}p,.headingDoubleSub,ol,ul,.table{margin-top:5px;margin-bottom:5px}.box{margin-top:21px;margin-bottom:21px}table h1,table h2,table h3,table h4,table h5,table h6,table p,table ul,table ol,table dl,table blockquote,table .media,table pre{margin-left:0;margin-right:0}.pan{padding:0!important}.man{margin:0!important}.pas{padding:5px!important}.mas{margin:5px!important}.pam{padding:10px!important}.mam{margin:10px!important}.pal{padding:21px!important}.mal{margin:21px!important}.pvn{padding-top:0!important;padding-bottom:0!important}.mvn{margin-top:0!important;margin-bottom:0!important}.pvs{padding-top:5px!important;padding-bottom:5px!important}.mvs{margin-top:5px!important;margin-bottom:5px!important}.pvm{padding-top:10px!important;padding-bottom:10px!important}.mvm{margin-top:10px!important;margin-bottom:10px!important}.pvl{padding-top:21px!important;padding-bottom:21px!important}.mvl{margin-top:21px!important;margin-bottom:21px!important}.phn{padding-left:0!important;padding-right:0!important}.mhn{margin-left:0!important;margin-right:0!important}.phs{padding-left:5px!important;padding-right:5px!important}.mhs{margin-left:5px!important;margin-right:5px!important}.phm{padding-left:10px!important;padding-right:10px!important}.mhm{margin-left:10px!important;margin-right:10px!important}.phl{padding-left:21px!important;padding-right:21px!important}.mhl{margin-left:21px!important;margin-right:21px!important}.ptn{padding-top:0!important}.mtn{margin-top:0!important}.pts{padding-top:5px!important}.mts{margin-top:5px!important}.ptm{padding-top:10px!important}.mtm{margin-top:10px!important}.ptl{padding-top:21px!important}.mtl{margin-top:21px!important}.prn{padding-right:0!important}.mrn{margin-right:0!important}.prs{padding-right:5px!important}.mrs{margin-right:5px!important}.prm{padding-right:10px!important}.mrm{margin-right:10px!important}.prl{padding-right:21px!important}.mrl{margin-right:21px!important}.pbn{padding-bottom:0!important}.mbn{margin-bottom:0!important}.pbs{padding-bottom:5px!important}.mbs{margin-bottom:5px!important}.pbm{padding-bottom:10px!important}.mbm{margin-bottom:10px!important}.pbl{padding-bottom:21px!important}.mbl{margin-bottom:21px!important}.pln{padding-left:0!important}.mln{margin-left:0!important}.pls{padding-left:5px!important}.mls{margin-left:5px!important}.plm{padding-left:10px!important}.mlm{margin-left:10px!important}.pll{padding-left:21px!important}.mll{margin-left:21px!important}@font-face{font-family:"OpenSans";src:url("http://static.trulia-cdn.com/images/fonts/OpenSans/OpenSans-Light-webfont.eot");src:url("http://static.trulia-cdn.com/images/fonts/OpenSans/OpenSans-Light-webfont.eot?#iefix") format("embedded-opentype"),url("http://static.trulia-cdn.com/images/fonts/OpenSans/OpenSans-Light-webfont.woff") format("woff"),url("http://static.trulia-cdn.com/images/fonts/OpenSans/OpenSans-Light-webfont.ttf") format("truetype"),url("http://static.trulia-cdn.com/images/fonts/OpenSans/OpenSans-Light-webfont.svg#OpenSansLight") format("svg");font-weight:300;font-style:normal}@font-face{font-family:"OpenSans";src:url("http://static.trulia-cdn.com/images/fonts/OpenSans/OpenSans-Regular-webfont.eot");src:url("http://static.trulia-cdn.com/images/fonts/OpenSans/OpenSans-Regular-webfont.eot?#iefix") format("embedded-opentype"),url("http://static.trulia-cdn.com/images/fonts/OpenSans/OpenSans-Regular-webfont.woff") format("woff"),url("http://static.trulia-cdn.com/images/fonts/OpenSans/OpenSans-Regular-webfont.ttf") format("truetype"),url("http://static.trulia-cdn.com/images/fonts/OpenSans/OpenSans-Regular-webfont.svg#OpenSansRegular") format("svg");font-weight:400;font-style:normal}@font-face{font-family:"OpenSans";src:url("http://static.trulia-cdn.com/images/fonts/OpenSans/OpenSans-Bold-webfont.eot");src:url("http://static.trulia-cdn.com/images/fonts/OpenSans/OpenSans-Bold-webfont.eot?#iefix") format("embedded-opentype"),url("http://static.trulia-cdn.com/images/fonts/OpenSans/OpenSans-Bold-webfont.woff") format("woff"),url("http://static.trulia-cdn.com/images/fonts/OpenSans/OpenSans-Bold-webfont.ttf") format("truetype"),url("http://static.trulia-cdn.com/images/fonts/OpenSans/OpenSans-Bold-webfont.svg#OpenSansBold") format("svg");font-weight:600;font-style:normal}body{font-size:14px;font-size:.875rem;line-height:1.5em;color:#222}body,input,select,textarea,button{font-family:OpenSans,sans-serif}h1,.h1,h2,.h2,h3,.h3,h4,.h4,h5,.h5,h6,.h6{font-weight:300;font-style:normal}h1,.h1{font-size:36px;font-size:2.25rem;line-height:1.3;margin:5px 0;font-weight:bold}h2,.h2{font-size:28px;font-size:1.75rem;line-height:1.4;margin:5px 0}h3,.h3{font-size:24px;font-size:1.5rem;line-height:1.4;margin:5px 0}h4,.h4{font-size:20px;font-size:1.25rem;line-height:1.4;margin:5px 0}h5,.h5{font-size:16px;font-size:1rem;line-height:1.5;margin:5px 0;font-weight:bold}h6,.h6{font-size:14px;font-size:.875rem;line-height:1.5;margin:5px 0;font-weight:bold}.h7{font-size:12px;font-size:.75rem;line-height:1.6;margin:5px 0}.headingDoubleSuper{display:block;clear:both;min-width:10px}.headingDoubleSub{font-size:14px;font-size:.875rem;display:block;float:left;font-weight:normal;line-height:1.5}.headingDoubleInline{display:inline-block}pre,code{font-family:Menlo,Consolas,Monaco,"Lucida Console",monospace}.typeWarning{color:#cc2114}.typeHighlight{color:#5eab1f}.typeLowlight{color:#999}.typeReversed1{color:white}.typeReversed2{color:#eff6e9}.typeDeemphasize{font-weight:300!important}.typeWeightNormal{font-weight:normal!important}.typeEmphasize{font-weight:bold!important}.cols1{width:4.16667%!important;*width:2.30496%!important}.cols2{width:8.33333%!important;*width:6.47163%!important}.cols3{width:12.5%!important;*width:10.6383%!important}.cols4{width:16.66667%!important;*width:14.80496%!important}.cols5{width:20.83333%!important;*width:18.97163%!important}.cols6{width:25%!important;*width:23.1383%!important}.cols7{width:29.16667%!important;*width:27.30496%!important}.cols8{width:33.33333%!important;*width:31.47163%!important}.cols9{width:37.5%!important;*width:35.6383%!important}.cols10{width:41.66667%!important;*width:39.80496%!important}.cols11{width:45.83333%!important;*width:43.97163%!important}.cols12{width:50%!important;*width:48.1383%!important}.cols13{width:54.16667%!important;*width:52.30496%!important}.cols14{width:58.33333%!important;*width:56.47163%!important}.cols15{width:62.5%!important;*width:60.6383%!important}.cols16{width:66.66667%!important;*width:64.80496%!important}.cols17{width:70.83333%!important;*width:68.97163%!important}.cols18{width:75%!important;*width:73.1383%!important}.cols19{width:79.16667%!important;*width:77.30496%!important}.cols20{width:83.33333%!important;*width:81.47163%!important}.cols21{width:87.5%!important;*width:85.6383%!important}.cols22{width:91.66667%!important;*width:89.80496%!important}.cols23{width:95.83333%!important;*width:93.97163%!important}.cols24{width:100%!important;*width:98.1383%!important}.line{margin-left:-21px}.line .col{min-height:1px;float:left;zoom:1;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding-left:21px}.line .colExt{float:right;zoom:1;padding:0 0 0 21px}.line .lastCol{float:none;*display:block;*width:auto!important;*zoom:1}.line .col:last-child{display:table-cell;float:none;vertical-align:top;width:10000px!important}a,a:hover,a:focus,.linkLowlight{text-decoration:none}a{color:#1885f0}a:hover,a:focus{color:#5eab1f}.linkLowlight{color:#999}.linkLowlight:hover,.linkLowlight:focus{color:#5eab1f}.linkForward:after{margin-left:.35714em;content:"\00BB"}.linkBack:before{margin-right:.35714em;content:"\00AB"}.backgroundBasic{background-color:white}.backgroundLowlight{background-color:#f9f9f9}.backgroundHighlight{background-color:#5eab1f}.backgroundInverse{background-color:#222}.box{position:relative}.box .boxHead,.box .boxFoot{padding:0;margin:0 21px}.box .boxBody{padding:0 21px 1px}.boxHighlight{border:1px solid #e9e9e9;-webkit-border-radius:6px;-moz-border-radius:6px;-ms-border-radius:6px;-o-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 0 5px rgba(0,0,0,0.17);-moz-box-shadow:0 0 5px rgba(0,0,0,0.17);box-shadow:0 0 5px rgba(0,0,0,0.17)}.boxBasic{border:1px solid #e9e9e9;-webkit-border-radius:6px;-moz-border-radius:6px;-ms-border-radius:6px;-o-border-radius:6px;border-radius:6px}.boxHeadBasic{border-bottom:solid 1px #e9e9e9}.boxFootBasic{border-top:solid 1px #e9e9e9}.boxClose{border:0;position:absolute;cursor:pointer;background-color:transparent}.boxClose{top:3px;right:6px;width:20px;height:20px;text-align:center;color:#999;font-size:14px;font-size:.875rem}.boxClose:hover,.boxClose:focus{color:#222}ol,ul{list-style-type:none}ol>li,ul>li{margin:.2em 0}.listBordered>li,.listBorderedHover>li{padding:10px;border-top:1px solid #e9e9e9}.listBordered>li:first-child,.listBorderedHover>li:first-child{border-top:1px solid transparent}.listBorderedHover>li{margin:0}.listBorderedHover>li:hover{background-color:#f9f9f9}.listInline>li{vertical-align:middle;padding-right:10px}.lvn>li{padding-top:0;padding-bottom:0}.lvs>li{padding-top:5px;padding-bottom:5px}.lvm>li{padding-top:10px;padding-bottom:10px}.lvl>li{padding-top:21px;padding-bottom:21px}.lhn>li{padding-left:0;padding-right:0}.lhs>li{padding-left:5px;padding-right:5px}.lhm>li{padding-left:10px;padding-right:10px}.lhl>li{padding-left:21px;padding-right:21px}.toggle{position:relative;cursor:pointer}.toggleArrow .before,.toggleArrow:before{content:"\25BA";display:inline-block;width:1.3em;font-size:.9em;text-align:center}.toggleArrowActive:before{content:"\25BC"}.toggleArrow:hover .before{text-decoration:none}.toggleArrow .active,.toggleActive .inactive{display:none}.toggleActive .active{display:inline-block}.toggleActive .inactive{display:none}.frameThumb,.frameSmall,.frameStandard,.frameStacked{display:inline-block;*display:inline;*zoom:1;*border:1px solid #e9e9e9;-webkit-box-shadow:0 0 1px 1px rgba(0,0,0,0.2);-moz-box-shadow:0 0 1px 1px rgba(0,0,0,0.2);box-shadow:0 0 1px 1px rgba(0,0,0,0.2)}.frameThumb img,.frameSmall img,.frameStandard img,.frameStacked img{display:block}.frameThumb .polaroid,.frameSmall .polaroid,.frameStandard .polaroid,.frameStacked .polaroid{margin-top:5px}.frameThumb{padding:2px}.frameSmall{padding:6px}.frameStandard{padding:8px}.frameStacked{padding:8px;position:relative;background-color:#fff}.frameStacked:before,.frameStacked:after{-webkit-box-shadow:0 0 1px 1px rgba(0,0,0,0.2);-moz-box-shadow:0 0 1px 1px rgba(0,0,0,0.2);box-shadow:0 0 1px 1px rgba(0,0,0,0.2);width:100%;height:100%;position:absolute;z-index:-999;content:"";background-color:#f9f9f9}.frameStacked:before{left:5px;top:0;-webkit-transform:rotate(1deg);-moz-transform:rotate(1deg);-ms-transform:rotate(1deg);-o-transform:rotate(1deg);transform:rotate(1deg)}.frameStacked:after{right:5px;top:0;-webkit-transform:rotate(-1deg);-moz-transform:rotate(-1deg);-ms-transform:rotate(-1deg);-o-transform:rotate(-1deg);transform:rotate(-1deg)}.badgeStandard,.badgePrimary,.badgeSecondary,.badgeTertiary{-webkit-border-radius:3px;-moz-border-radius:3px;-ms-border-radius:3px;-o-border-radius:3px;border-radius:3px;font-size:12px;font-size:.75rem;padding:.35714em .42857em .21429em;display:inline-block;color:white;line-height:1;text-transform:uppercase}.badgeStandard{background-color:#999}.badgePrimary{background-color:#ff5c00}.badgeSecondary{background-color:#5eab1f}.badgeTertiary{background-color:#cc2114}button{background:0;border:0}.btn{padding:.5em 1em;margin:0;display:inline-block;font-weight:normal;line-height:normal;font-size:14px;font-size:.875rem;text-decoration:none;cursor:pointer;border:0;background:0;text-align:center;border:1px solid #ccc;-webkit-border-radius:4px;-moz-border-radius:4px;-ms-border-radius:4px;-o-border-radius:4px;border-radius:4px}.btnSml{font-size:12px;font-size:.75rem;-webkit-border-radius:4px;-moz-border-radius:4px;-ms-border-radius:4px;-o-border-radius:4px;border-radius:4px}.btnLrg{font-size:16px;font-size:1rem;line-height:1.6;-webkit-border-radius:4px;-moz-border-radius:4px;-ms-border-radius:4px;-o-border-radius:4px;border-radius:4px}.btnFullWidth{width:100%}.btnDefault,a.btnDefault{border:1px #d7d7d7 solid;background:#f3f3f3;color:#222;text-shadow:0 1px 0 white;-webkit-box-shadow:0 1px 0 #ccc;-moz-box-shadow:0 1px 0 #ccc;box-shadow:0 1px 0 #ccc}.btnDefault:hover,.btnDefault:focus{background:#f6f6f6}.btnDefault:active{position:relative;top:1px;-webkit-box-shadow:inset 0 3px 7px rgba(0,0,0,0.1),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 3px 7px rgba(0,0,0,0.1),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 3px 7px rgba(0,0,0,0.1),0 1px 2px rgba(0,0,0,0.05)}.btnPrimary,a.btnPrimary,.btnDanger,a.btnDanger{color:white;text-shadow:0}.btnPrimary,a.btnPrimary{border:1px #d14b00 solid;background:#ff5c00;-webkit-box-shadow:0 1px 0 #d35500;-moz-box-shadow:0 1px 0 #d35500;box-shadow:0 1px 0 #d35500}.btnPrimary:hover,.btnPrimary:focus{background:#ff660f}.btnPrimary:active{position:relative;top:1px;-webkit-box-shadow:inset 0 3px 7px rgba(0,0,0,0.1),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 3px 7px rgba(0,0,0,0.1),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 3px 7px rgba(0,0,0,0.1),0 1px 2px rgba(0,0,0,0.05)}.btnDanger,a.btnDanger{border:1px #a21a10 solid;background:#cc2114;-webkit-box-shadow:0 1px 0 #d35500;-moz-box-shadow:0 1px 0 #d35500;box-shadow:0 1px 0 #d35500}.btnDanger:hover,.btnDanger:focus{background:#da2315}.btnDanger:active{position:relative;top:1px;-webkit-box-shadow:inset 0 3px 7px rgba(0,0,0,0.1),0 1px 2px rgba(0,0,0,0.05);-moz-box-shadow:inset 0 3px 7px rgba(0,0,0,0.1),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 3px 7px rgba(0,0,0,0.1),0 1px 2px rgba(0,0,0,0.05)}.btn[disabled],.btn[disabled]:hover,.btn[disabled]:focus .btn[disabled]:active,.btnDisabled,a.btnDisabled,.btnDisabled:hover,.btnDisabled:focus,.btnDisabled:active{border:0;background:#ccc;color:#999;text-shadow:0;cursor:default}.btn[disabled]:active,.btnDisabled:active{position:static;color:#999;box-shadow:none}.btnLink{color:#1885f0;border:1px solid transparent}.btnLink:hover,.btnLink:focus{color:#5eab1f}.table{width:100%}.table thead th{font-weight:bold;color:#222}.table th,.table td{padding:8px}.tableBasic th,.tableBasic td{border:1px solid #e9e9e9}.tan td,.tan th{padding:0}.tas td,.tas th{padding:5px}.tam td,.tam th{padding:10px}.tal td,.tal th{padding:21px}.tvn td,.tvn th{padding-top:0;padding-bottom:0}.tvs td,.tvs th{padding-top:5px;padding-bottom:5px}.tvm td,.tvm th{padding-top:10px;padding-bottom:10px}.tvl td,.tvl th{padding-top:21px;padding-bottom:21px}.thn td,.thn th{padding-left:0;padding-right:0}.ths td,.ths th{padding-left:5px;padding-right:5px}.thm td,.thm th{padding-left:10px;padding-right:10px}.thl td,.thl th{padding-left:21px;padding-right:21px}body{background:white}.content>section,.container{width:1128px;margin:0 auto}.main[role="main"]{position:relative;top:-10px}.content>section,.content .container{margin-top:0}.skipLink:focus{width:100%;padding:5px 0;display:block;position:absolute;z-index:100;text-indent:5px;color:#fff;background:#5eab1f}.header{padding-bottom:15px;*position:relative;*z-index:100;background:white;border-bottom:1px solid #ccc}.headerSubSection{margin:1px 0;font-size:11px;font-size:.6875rem;text-align:right;line-height:1}.headerSubSection a{color:#222}.headerSubSection .highlight{display:inline-block;padding:2px 10px;color:#fff;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;-ms-border-radius:5px 5px 0 0;-o-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0;background:#888}.headerSubSection .listInline{margin:0}.headerMainSection{position:relative}.headerMainSection .logo{position:absolute;left:0;top:-1px}.menuWrap{margin-left:97px}.content{padding-top:5px}.content>section{margin-top:5px}.sideBar{width:185px;margin-right:20px;float:left}.main{float:none;*display:block;*width:auto;*zoom:1}.aside{width:300px;margin-left:20px;float:right}.footer{clear:both;margin-top:40px;font-size:12px;font-size:.75rem}.footerBox{border-top:5px solid #5eab1f}.footerBox .boxBody{margin:10px 0}.footerGroup a{color:#222}.footerGroup a:hover,.footerGroup a:focus{color:#5eab1f;text-decoration:underline}.footerCol{width:125px}.footerGroupHeading{float:left}.footerGroupHeading{width:120px;margin:0;clear:right;font-weight:bold}.footerGroupList{margin:0}.footerGroupList>li{margin:0}.footerDisclaimer{float:left}.footerNoteAside{float:right} 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hologram 2 | [![Gem Version](https://img.shields.io/gem/v/hologram.svg)](https://rubygems.org/gems/hologram) 3 | [![Build Status](https://img.shields.io/travis/trulia/hologram.svg)](https://travis-ci.org/trulia/hologram) 4 | [![Code Climate](https://img.shields.io/codeclimate/github/trulia/hologram.svg)](https://codeclimate.com/github/trulia/hologram) 5 | [![Dependency Status](https://img.shields.io/gemnasium/trulia/hologram.svg)](https://gemnasium.com/trulia/hologram) 6 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.txt) 7 | 8 | Hologram is a Ruby gem that parses comments in your CSS and helps you 9 | turn them into a beautiful style guide. 10 | 11 | There are two steps to building a great style guide: 12 | 13 | 1. Documenting your css and javascript, and generating html examples. 14 | 2. Styling the output of step 1. 15 | 16 | The hologram gem itself is only concerned with step 1. This means you 17 | are free to make your style guide look however you would like. If you 18 | don't feel like going through this process yourself, you can take a look 19 | at the 20 | [templates](https://github.com/trulia/hologram-example/tree/master/templates) 21 | in our [example repository](https://github.com/trulia/hologram-example), 22 | and use the assets defined there instead. 23 | 24 | 25 | ## Installation 26 | 27 | Add this line to your application's Gemfile: 28 | 29 | gem 'hologram' 30 | 31 | And then execute: 32 | 33 | $ bundle 34 | 35 | If you don't use bundler you can run `gem install hologram`. 36 | 37 | 38 | ## Quick Start 39 | 40 | ``` hologram init ``` 41 | 42 | This will create a `hologram_config.yml` file (more on this below), 43 | starter `_header.html` and `_footer.html` files, 44 | and starter templates for rendering code examples. 45 | You can then tweak the config values and start documenting your css. 46 | 47 | Add some documentation to one of your stylesheets: 48 | 49 | /*doc 50 | --- 51 | title: Alert 52 | name: alert 53 | category: basics 54 | --- 55 | ```html_example 56 |
    Hello
    57 | ``` 58 | */ 59 | 60 | Building the documentation is simply: 61 | 62 | ``` hologram ``` 63 | 64 | 65 | ### Command line flags 66 | 67 | Hologram has a couple of command line flags: 68 | 69 | * `-c` or `--config` - specify the config file, by default hologram 70 | looks for `hologram_config.yml` 71 | 72 | ## Details 73 | 74 | There are two things you need to do to start using hologram: 75 | 76 | 1. Create a YAML config file for your project. 77 | 78 | 2. Go document some code! 79 | 80 | 81 | ### Creating a YAML config file 82 | 83 | Hologram needs a few configuration settings before it can begin to build 84 | your documentation for you. Once this is set up, you can execute hologram 85 | by simply running: 86 | 87 | `hologram path/to/your/config.yml` or (using bundler) `bundle exec 88 | hologram path/to/your/config.yml` 89 | 90 | Your config file needs to contain the following key/value pairs 91 | 92 | * **source**: relative path(s) to your source files. Accepts either a 93 | single value or an array 94 | 95 | * **destination**: relative path where you want the documentation to be 96 | built 97 | 98 | * **documentation_assets**: The path that contains supporting assets for 99 | the documentation page. This typically includes html fragments 100 | (header/footer, etc), style guide specific CSS, javascript and any 101 | images. Hologram specifically looks for two files: `_header.html` and 102 | `_footer.html`. These are used to start and end every html page 103 | hologram generates. 104 | 105 | Hologram treats `_header.html` and `_footer.html` as ERB files for 106 | each page that is generated. You can access the `title`, `file_name`, 107 | `blocks`, and `categories`. 108 | 109 | `blocks` is a list of each documentation block on the page. Each item 110 | in the list has a `title`, `name`, `category`, and optionally a 111 | `parent`. This is useful for, say, building a menu that lists each 112 | component. 113 | 114 | `categories` is a list of all the categories found in the 115 | documentation 116 | 117 | **Nota Bene:** Filenames that begin with underscores will not be 118 | copied into the destination folder. 119 | 120 | * **code\_example\_templates**: (optional) Hologram uses the files in this folder to 121 | format the code examples in the styleguide. The initializer generates 4 files: 122 | 123 | * `markup_example_template.html.erb` - used for html, haml and slim examples 124 | 125 | * `markup_table_template.html.erb` - used for multiple html, haml and slim 126 | examples layed out in a table (see 127 | [the tabular layout docs](#tabular-layout-for-component-documentation) 128 | for more information) 129 | 130 | * `js_example_template.html.erb` - used for js examples 131 | 132 | * `jsx_example_template.html.erb` - used for jsx examples 133 | 134 | The html in the files will be rendered for every code example in the 135 | styleguide. The variable `rendered_example` represents the html 136 | generated by the example, while the variable `code_example` represents the 137 | formatted and escaped code behind the example. 138 | 139 | See [the documentation on custom code renderers](#custom_code_example_renderers) 140 | for more information, 141 | 142 | **Nota Bene:** If template files are missing, or this folder does not exist, 143 | hologram will use default templates. 144 | 145 | * **code\_example\_renderers**: (optional) A folder that contains your custom 146 | code renderers. For example, if you want to have `coffee_example`s in your 147 | code, write a coffeescript renderer and place it in this folder. See 148 | [#custom_code_example_renders](below) for more inforamtion on this. 149 | 150 | * **custom_markdown**: (optional) this is the filename of a class that 151 | extends RedCarpet::Render::HTML class. Use this for when you need 152 | additional classes or html tags for different parts of the page. See 153 | [example_markdown_renderer.rb.example] 154 | (example_markdown_renderer.rb.example) for an example of what your 155 | class can look like. 156 | 157 | * **index**: (optional) this is a category (see **Documenting your 158 | styles** section below) that will be used as the index.html. 159 | 160 | * **dependencies**: a **list** of relative paths to folders containing 161 | any dependencies your style guide has. These folders will be copied 162 | over into the documentation output directory. ENSURE THE CSS/JS THAT IS 163 | ACTUALLY BEING DOCUMENTED IS LISTED HERE. You will also need to ensure 164 | that they are included on your pages. A simple way to do this is to add 165 | `` and ` 412 |
    413 |
    414 |
    <%= code_example %>
    415 |
    416 |
    417 | ``` 418 | 419 | ```erb 420 | 421 | 422 |
    423 | 424 | 425 | <% examples.each do |example| %> 426 | 427 | 435 | 436 | <% end %> 437 | 438 |
    428 | 429 |
    430 |
    431 |
    <%= example.code_example %>
    432 |
    433 |
    434 |
    439 |
    440 | ``` 441 | 442 | Next, create a custom renderer for coffeescript in the file 443 | `./code_example_renderers/coffee_renderer.rb`. 444 | 445 | ```ruby 446 | # ./code_example_renderers/coffee_renderer.rb 447 | 448 | require 'coffee-script' 449 | 450 | Hologram::CodeExampleRenderer::Factory.define('coffee') do 451 | example_template 'my_custom_coffee_example_template' 452 | table_template 'my_custom_coffee_table_template' 453 | 454 | lexer { Rouge::Lexer.find(:coffee) } 455 | 456 | rendered_example do |code| 457 | CoffeeScript.compile(code) 458 | end 459 | end 460 | ``` 461 | 462 | Now you should be able to render coffeescript examples in your styleguide. 463 | You can render single coffeescript examples... 464 | 465 | ```coffee_example 466 | $('#myDiv').click -> 467 | alert 'Oh wow we are rendering coffee script' 468 | ``` 469 | 470 | Or you can render coffeescript tables... 471 | 472 | ```coffee_example_table 473 | $('#myDiv').click -> 474 | alert 'Oh wow we are rendering coffee script' 475 | 476 | $('#myOtherDiv').click -> 477 | console.log 'Yeah coffee script!' 478 | 479 | $('#yetAnotherDiv').click -> 480 | window.location = 481 | ``` 482 | 483 | Here's some details on the code example renderer factory: 484 | 485 | * `Hologram::CodeExampleRenderer::Factory.define(example_type, &block)` - 486 | this is how you declare a custom renderer. `example_type` is the name of the 487 | renderer, and determines the example name. For example, if `example_type` 488 | was "foobar", in your styleguide you can create `foobar_example`s and 489 | `foobar_example_table`s 490 | 491 | * `example_template` - the name of the template used to render the example, 492 | minus the `.html.erb` extension (e.g. "markup_example_template"). It 493 | should live in your **code\_example\_templates** folder 494 | 495 | * `table_template` - (optional) the name of the template used to render examples in 496 | tabular form, minus the extension (e.g. "markup_table_template"). 497 | 498 | * `lexer` - (optional) a Rogue Lexer that matches the syntax of your 499 | example (i.e. `Rouge::Lexer.find(:haml)`, `Rouge::Lexer.find(:ruby)`). 500 | Here's a [complete list of possible lexers](http://rouge.jayferd.us/demo). 501 | If this argument is not provided, hologram will guess what the best 502 | one is. 503 | 504 | * `rendered_example` - (optional) this is the set of instructions to 505 | "translate" your exaple so it can be rendered. I.e. for coffeescript 506 | to be "rendered" in the browser, you need to transform it to 507 | javascript (as can be seen in the block above). 508 | For haml, you need to transform it to html. 509 | If no block is provided, the code is rendered as is. 510 | 511 | ## Supported Preprocessors/File Types 512 | 513 | The following preprocessors/file types are supported by Hologram: 514 | 515 | - Sass (.scss, .sass) 516 | - Less (.less) 517 | - Stylus (.styl) 518 | - Vanilla CSS (.css) 519 | - Javascript (.js) 520 | - Markdown (.md, .markdown) 521 | 522 | 523 | ## Extensions and Plugins 524 | 525 | - [Guard Hologram](https://github.com/kmayer/guard-hologram) is a sweet 526 | little gem that uses guard to monitor changes to your hologram project 527 | and rebuilds your style guide on the fly as you make changes. 528 | - [Grunt Hologram](https://github.com/jchild3rs/grunt-hologram/) is a sweet 529 | little grunt task that will generate your hologram style guide. 530 | - [Hologram Plugin for Gulp](https://gist.github.com/jchild3rs/470be49a4bc4caf3ca8a) is a gulp task for hologram. 531 | - [Classname Clicker](https://github.com/bigethan/hologram-addons/) is a handy 532 | UI addition that gives the ability to see rules that apply to a classname by 533 | clicking on them within hologram. 534 | - [Cortana](https://github.com/Yago/Cortana) is a theme for hologram. It also 535 | includes a handy search feature. 536 | - [Hologram Github Theme](https://github.com/wearecube/hologram-github-theme) is a Github Styleguide inspired theme for hologram. 537 | - [Voxel Hologram](https://github.com/rishabhsrao/voxel-hologram) is a minimal theme for Hologram. 538 | - [Acme Styleguide](https://github.com/mattrothenberg/styleguide-boilerplate) is a starter project that helps 539 | [Pivotal Labs Designers](https://pivotal.io/labs) build living styleguides with Sass and Hologram. 540 | 541 | ## Contributing 542 | 543 | 1. Fork it 544 | 2. Create your feature/bug fix branch (`git checkout -b my-new-feature`) 545 | 3. Commit your changes (`git commit -am 'Add some feature'`) 546 | 4. Push to the branch (`git push origin my-new-feature`) 547 | 5. Create new Pull Request 548 | 549 | 550 | ## Authors 551 | 552 | Hologram is written and maintained by [August 553 | Flanagan](http://github.com/aflanagan) and [JD 554 | Cantrell](http://github.com/jdcantrell). 555 | 556 | 557 | ## Contributors 558 | 559 | These fine people have also contributed to making hologram a better gem: 560 | 561 | * [Rajan Agaskar](https://github.com/ragaskar) 562 | * Louis Bennett 563 | * [jho406](https://github.com/jho406) 564 | * johny (wrote our initial tests!) 565 | * [Elana Koren](https://github.com/elanakoren) 566 | * [Ken Mayer](https://github.com/kmayer) 567 | * [Roberto Ostinelli](https://github.com/ostinelli) 568 | * [Dominick Reinhold](https://github.com/d-reinhold) 569 | * [Nicole Sullivan](https://github.com/stubbornella) 570 | * [Mike Wilkes](https://github.com/mikezx6r) 571 | * [Vanessa Sant'Anna](https://github.com/vsanta) 572 | * [Geoffrey Giesemann](https://github.com/geoffwa) 573 | 574 | 575 | ## License 576 | 577 | [Hologram is licensed under the MIT License](https://github.com/trulia/hologram/blob/master/LICENSE.txt) 578 | --------------------------------------------------------------------------------