emmett 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
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
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in emmett.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Darcy Laycock
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # Emmett
2
+
3
+ Emmett is a tool named after Dr Emmett Brown from Back to the Future.
4
+
5
+ It's purpose is simple - given an index page and a bunch of API documents, it'll take
6
+ them and generate a nice, usable website people can use to consume the documentation.
7
+
8
+ It doesn't automate the docs or the like - it just does the simplest thing possible.
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+
14
+ gem 'emmett'
15
+
16
+ And then execute:
17
+
18
+ $ bundle
19
+
20
+ Or install it yourself as:
21
+
22
+ $ gem install emmett
23
+
24
+ ## Usage
25
+
26
+ Emmett is primarily intended to be used as a rake task.
27
+
28
+ ### Emmett + Rails
29
+
30
+ Want to generate the documentation directly in your application?
31
+
32
+ To configure it, the following options are available - simply put them in
33
+ `config/application.rb` and change them as fit:
34
+
35
+ ```ruby
36
+ config.emmett.name = "Your App"
37
+ config.emmett.index_page = "doc/api.md" # Relative to doc/
38
+ config.emmett.section_dir = "doc/api" # Relative to doc/
39
+ config.emmett.output_dir = "doc/generated-api"
40
+ config.emmett.template = :default
41
+ ```
42
+
43
+ It will use sane defaults (all being the same as above except the name, which is
44
+ the rails root dir titleize. I do suggest changing this). In Rails, it will
45
+ be available via `rake doc:api`.
46
+
47
+ ### Emmett on it's own.
48
+
49
+ Likewise, you can use emmett inside any application thanks to the Rake Task.
50
+
51
+ In your Rakefile, simply add:
52
+
53
+ ```ruby
54
+ require 'emmett/rake_task'
55
+
56
+ Emmett::RakeTask.new :docs do |t|
57
+ t.name = "Your App"
58
+ t.index_page = "api.md"
59
+ t.section_dir = "api"
60
+ t.output_dir = "output"
61
+ t.template = :default
62
+ end
63
+ ```
64
+
65
+ Like the rails version, this will use the values above as output,
66
+ with the name being based on the current directory name.
67
+
68
+ ## Contributing
69
+
70
+ 1. Fork it
71
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
72
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
73
+ 4. Push to the branch (`git push origin my-new-feature`)
74
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
data/emmett.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/emmett/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Darcy Laycock"]
6
+ gem.email = ["darcy@filtersquad.com"]
7
+ gem.description = %q{Tools to make building API docs simpler.}
8
+ gem.summary = %q{Tools to make building API docs simpler.}
9
+ gem.homepage = ""
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "emmett"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Emmett::VERSION
17
+
18
+ gem.add_dependency 'github-markdown'
19
+ gem.add_dependency 'github-markup'
20
+ gem.add_dependency 'nokogiri'
21
+ gem.add_dependency 'pygments.rb'
22
+ gem.add_dependency 'handlebars'
23
+ gem.add_dependency 'rake'
24
+ gem.add_dependency 'oj'
25
+ gem.add_dependency 'http_parser.rb'
26
+
27
+ end
@@ -0,0 +1,30 @@
1
+ require 'emmett/template'
2
+
3
+ module Emmett
4
+ class Configuration
5
+
6
+ class Error < StandardError; end
7
+
8
+ attr_accessor :name, :template, :index_page, :section_dir, :output_dir
9
+
10
+ def verify!
11
+ errors = []
12
+ errors << "You must set the name attribute for emmett" if !name
13
+ errors << "You must set the template attribute for emmett" if !template
14
+ errors << "The index_page file must exist" unless index_page && File.exist?(index_page)
15
+ errors << "The section_dir directory must exist" unless section_dir && File.directory?(section_dir)
16
+ errors << "The output_dir must be set" unless output_dir
17
+ errors << "The specified template does not exist" unless to_template
18
+ if errors.any?
19
+ message = "Your configuration is invalid:\n"
20
+ errors.each { |e| message << "* #{e}\n" }
21
+ raise Error.new(message)
22
+ end
23
+ end
24
+
25
+ def to_template
26
+ @template_instance ||= Template[template]
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,126 @@
1
+ require 'pygments'
2
+ require 'github/markdown'
3
+ require 'github/markup'
4
+ require 'nokogiri'
5
+
6
+ require 'emmett/http_request_processor'
7
+
8
+ module Emmett
9
+ class Document < Struct.new(:file_name, :content, :type)
10
+
11
+ def self.from_path(path, type = :normal)
12
+ Document.new path, GitHub::Markup.render(path, File.read(path)), type
13
+ end
14
+
15
+ def short_name
16
+ @short_name ||= begin
17
+ if type == :index
18
+ "index"
19
+ else
20
+ File.basename(file_name).split(".")[0..-2].join(".")
21
+ end
22
+ end
23
+ end
24
+
25
+ def document
26
+ @document ||= Nokogiri::HTML(content)
27
+ end
28
+
29
+ def sections
30
+ @sections ||= document.css('h2').map(&:text)
31
+ end
32
+
33
+ def section_mapping
34
+ @section_mapping ||= sections.inject({}) do |acc, current|
35
+ acc[current] = current.strip.downcase.gsub(/\W+/, '-').gsub(/-+/, '-').gsub(/(^-|-$)/, '')
36
+ acc
37
+ end
38
+ end
39
+
40
+ def title
41
+ @title ||= document.at_css('h1').text
42
+ end
43
+
44
+ def highlighted_html
45
+ @highlighted_html ||= begin
46
+ doc = document.clone
47
+ doc.css('pre[lang]').each do |block|
48
+ inner = block.at_css('code')
49
+ highlighted = Pygments.highlight(inner.inner_html, options: {encoding: 'utf-8'}, lexer: block[:lang])
50
+ highlighted_fragment = Nokogiri::HTML::DocumentFragment.parse highlighted
51
+ highlighted_fragment["data-code-lang"] = block[:lang]
52
+ block.replace highlighted_fragment
53
+ end
54
+
55
+ mapping = section_mapping
56
+ doc.css('h2').each do |header|
57
+ if (identifier = mapping[header.text])
58
+ header[:id] = identifier
59
+ end
60
+ end
61
+
62
+ unless short_name == 'index'
63
+ # Now, insert an endpoints content before the start of it.
64
+ toc = Nokogiri::HTML::DocumentFragment.parse toc_html
65
+ doc.at_css('h2').add_previous_sibling toc
66
+ end
67
+
68
+ doc.css('body').inner_html
69
+ end
70
+ end
71
+
72
+ def toc_html
73
+ [].tap do |html|
74
+ html << "<h2>Endpoints</h2>"
75
+ html << "<ul id='endpoints'>"
76
+
77
+ section_mapping.each_pair do |section, slug|
78
+ html << "<li><a href='##{slug}'>#{section}</a></li>"
79
+ end
80
+ html << "</ul>"
81
+ end.join("")
82
+ end
83
+
84
+ def iterable_section_mapping
85
+ section_mapping.map { |(n,v)| {name: n, hash: v} }
86
+ end
87
+
88
+ def to_path_name
89
+ "#{short_name}.html"
90
+ end
91
+
92
+ def code_blocks
93
+ @code_blocks ||= begin
94
+ last_header = nil
95
+ blocks = []
96
+ document.css('h2, pre[lang]').each do |d|
97
+ if d.name == 'h2'
98
+ last_header = d.text
99
+ else
100
+ blocks << [d[:lang], d.at_css('code').text, last_header]
101
+ end
102
+ end
103
+ blocks
104
+ end
105
+ end
106
+
107
+ def http_blocks
108
+ @http_blocks ||= code_blocks.select { |r| r.first == "http" }.map { |r| r[1..-1] }
109
+ end
110
+
111
+ def http_requests
112
+ @http_requests ||= http_blocks.select do |cb|
113
+ first_line = cb[0].lines.first.strip
114
+ first_line =~ /\A[A-Z]+ (\S+) HTTP\/1\.1\Z/
115
+ end.map { |r| HTTPRequestProcessor.new(*r) }
116
+ end
117
+
118
+ def http_responses
119
+ @http_responses ||= http_blocks.select do |cb|
120
+ first_line = cb.lines.first.strip
121
+ first_line =~ /\AHTTP\/1\.1 (\d+) (\w+)\Z/
122
+ end
123
+ end
124
+
125
+ end
126
+ end
@@ -0,0 +1,89 @@
1
+ require 'emmett/document'
2
+ require 'emmett/renderer'
3
+
4
+ module Emmett
5
+ class DocumentManager
6
+
7
+ def self.render!(*args)
8
+ new(*args).render!
9
+ end
10
+
11
+ attr_reader :configuration
12
+
13
+ def initialize(configuration)
14
+ @configuration = configuration
15
+ end
16
+
17
+ def index_document
18
+ @index_document ||= render_path(configuration.index_page, :index)
19
+ end
20
+
21
+ def inner_documents
22
+ @inner_documents ||= begin
23
+ Dir[File.join(configuration.section_dir, "**/*.md")].map do |path|
24
+ render_path path
25
+ end
26
+ end
27
+ end
28
+
29
+ def inner_links
30
+ @inner_links ||= inner_documents.map do |doc|
31
+ {
32
+ doc: doc,
33
+ title: doc.title,
34
+ short: doc.short_name,
35
+ link: "./#{doc.short_name}.html",
36
+ sections: doc.iterable_section_mapping
37
+ }
38
+ end.sort_by { |r| r[:title].downcase }
39
+ end
40
+
41
+ def render_index(renderer)
42
+ render_document renderer, :index, index_document
43
+ end
44
+
45
+ def render_documents(renderer)
46
+ inner_documents.each do |document|
47
+ render_document renderer, :section, document
48
+ end
49
+ end
50
+
51
+ def render(renderer)
52
+ render_index renderer
53
+ render_documents renderer
54
+ end
55
+
56
+ def render!
57
+ Renderer.new(configuration).tap do |renderer|
58
+ renderer.prepare_output
59
+ renderer.global_context = {
60
+ links: inner_links,
61
+ site_name: configuration.name
62
+ }
63
+ render renderer
64
+ end
65
+ end
66
+
67
+ def all_urls
68
+ out = inner_documents.inject({}) do |acc, current|
69
+ acc[current.title] = current.http_requests.inject({}) do |ia, req|
70
+ ia[req.section] ||= []
71
+ ia[req.section] << req.request_line
72
+ ia
73
+ end
74
+ acc
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ def render_document(renderer, template_name, document, context = {})
81
+ renderer.render_to document.to_path_name, template_name, context.merge(content: document.highlighted_html)
82
+ end
83
+
84
+ def render_path(path, type = :normal)
85
+ Document.from_path path, type
86
+ end
87
+
88
+ end
89
+ end
@@ -0,0 +1,70 @@
1
+ require 'http/parser'
2
+ require 'oj'
3
+
4
+ module Emmett
5
+ class HTTPRequestProcessor
6
+
7
+ attr_reader :headers, :body, :method, :url, :http_version, :section
8
+
9
+ def initialize(request, section)
10
+ @raw_request = request.gsub(/\r?\n/m, "\r\n")
11
+ @body = ""
12
+ @section = section
13
+ parse!
14
+ end
15
+
16
+ def parse!
17
+ parser = Http::Parser.new
18
+
19
+ parser.on_headers_complete = proc do
20
+ @http_version = parser.http_version
21
+ @method = parser.http_method
22
+ @url = parser.request_url
23
+ @headers = parser.headers
24
+ end
25
+
26
+ parser.on_body = proc do |chunk|
27
+ # One chunk of the body
28
+ @body << chunk
29
+ end
30
+
31
+ parser.on_message_complete = proc do |env|
32
+ @parsed = true
33
+ end
34
+
35
+ @parsed = false
36
+ parser << @raw_request
37
+ parser << "\r\n" until @parsed
38
+ end
39
+
40
+ def has_body?
41
+ @body.strip.length > 0
42
+ end
43
+
44
+ def authenticated?
45
+ headers['Authorization'] && headers['Authorization'] =~ /bearer/i
46
+ end
47
+
48
+ def request_line
49
+ "#{method} #{url}"
50
+ end
51
+
52
+ def json?
53
+ headers['Content-Type'] && headers['Content-Type'].include?("application/json")
54
+ end
55
+
56
+ def has_valid_json?
57
+ return @has_valid_json if instance_variable_defined?(:@has_valid_json)
58
+
59
+ begin
60
+ Oj.load body
61
+ @has_valid_json = true
62
+ rescue SyntaxError
63
+ @has_valid_json = false
64
+ end
65
+
66
+ @has_valid_json
67
+ end
68
+
69
+ end
70
+ end
@@ -0,0 +1,22 @@
1
+ require 'emmett/configuration'
2
+
3
+ module Emmett
4
+ class Railtie < ::Rails::Railtie
5
+ R = ::Rails
6
+
7
+ config.emmett = Emmett::Configuration.new
8
+ config.emmett.name = File.basename(Dir.pwd).titleize
9
+ config.emmett.index_page = "doc/api.md"
10
+ config.emmett.section_dir = "doc/api"
11
+ config.emmett.output_dir = "doc/generated-api"
12
+ config.emmett.template = :default
13
+
14
+ rake_tasks do
15
+ require 'emmett/rake_task'
16
+ namespace :doc do
17
+ Emmett::RakeTask.new(:api) { |t| t.configuration = Rails.application.config.emmett }
18
+ end
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,47 @@
1
+ require 'rake'
2
+ require 'rake/tasklib'
3
+ require 'emmett'
4
+
5
+ module Emmett
6
+ class RakeTask < ::Rake::TaskLib
7
+ include ::Rake::DSL if defined?(::Rake::DSL)
8
+
9
+ def configuration
10
+ @configuration ||= build_default_configuration
11
+ end
12
+
13
+ # Proxy each of the configuration options.
14
+ def name=(value); configuration.name = value; end
15
+ def index_page=(value); configuration.index_page = value; end
16
+ def section_dir=(value); configuration.section_dir = value; end
17
+ def output_dir=(value); configuration.output_dir = value; end
18
+ def template=(value); configuration.template = value; end
19
+
20
+ attr_accessor :task_name
21
+ attr_writer :configuration
22
+
23
+ def initialize(*args)
24
+ @task_name = args.shift || :emmett
25
+ yield self if block_given?
26
+ desc "Generates api documentation using emmett" unless ::Rake.application.last_comment
27
+ task task_name do
28
+ configuration.verify!
29
+ Emmett::DocumentManager.render! configuration
30
+ end
31
+
32
+ end
33
+
34
+ private
35
+
36
+ def build_default_configuration
37
+ c = Configuration.new
38
+ c.name = File.basename(Dir.pwd)
39
+ c.index_page = "api.md"
40
+ c.section_dir = "api"
41
+ c.output_dir = "output"
42
+ c.template = :default
43
+ c
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,68 @@
1
+ require 'fileutils'
2
+ require 'handlebars'
3
+ require 'pathname'
4
+
5
+ module Emmett
6
+ class Renderer
7
+
8
+ attr_reader :handlebars, :global_context, :configuration, :templates
9
+ attr_writer :global_context
10
+
11
+ def initialize(configuration)
12
+ @configuration = configuration
13
+ @templates = configuration.to_template
14
+ @handlebars = Handlebars::Context.new
15
+ @cache = {}
16
+ @global_context = {}
17
+ configure_handlebars
18
+ end
19
+
20
+ def render(template, context = {})
21
+ load_template(template).call global_context.merge(context)
22
+ end
23
+
24
+ def render_to(output, name, context = {})
25
+ out = File.join(output_path, output)
26
+ File.open(out, 'w+') do |f|
27
+ f.write render(name, context)
28
+ end
29
+ end
30
+
31
+ def prepare_output
32
+ FileUtils.rm_rf output_path
33
+ FileUtils.mkdir_p output_path
34
+ copy_static
35
+ end
36
+
37
+ private
38
+
39
+ def output_path
40
+ @output_path ||= configuration.output_dir
41
+ end
42
+
43
+ def copy_static
44
+ templates.each_static_file do |file_name, path|
45
+ destination = File.join(output_path, file_name)
46
+ FileUtils.mkdir_p File.dirname(destination)
47
+ FileUtils.cp path, destination
48
+ end
49
+ end
50
+
51
+ def configure_handlebars
52
+ handlebars.partial_missing do |name|
53
+ template = load_template "_#{name}"
54
+ lambda do |this, context, options|
55
+ template.call context
56
+ end
57
+ end
58
+ end
59
+
60
+ def load_template(name)
61
+ @cache[name.to_s] ||= begin
62
+ path = templates.template_file_path("#{name}.handlebars")
63
+ path && handlebars.compile(File.read(path))
64
+ end
65
+ end
66
+
67
+ end
68
+ end
@@ -0,0 +1,81 @@
1
+ module Emmett
2
+ class Template
3
+
4
+ class Error < StandardError; end
5
+
6
+ class << self
7
+
8
+ def registry
9
+ @registry ||= {}
10
+ end
11
+
12
+ def register(name, value)
13
+ registry[name.to_sym] = value
14
+ end
15
+
16
+ def [](name)
17
+ registry.fetch(name.to_sym) { raise "Emmett does not know a template by the name '#{name}'" }
18
+ end
19
+
20
+ def add(name, path)
21
+ new(name, path).tap do |template|
22
+ template.verify!
23
+ template.register
24
+ end
25
+ end
26
+
27
+ end
28
+
29
+ attr_reader :name, :root
30
+
31
+ def initialize(name, root)
32
+ @name = name && name.to_sym
33
+ @root = root.to_s
34
+ end
35
+
36
+ def verify!
37
+ errors = []
38
+ errors << "Ensure the root directory exists" unless File.directory?(root)
39
+ errors << "Ensure the name is set" unless name
40
+ errors << "Ensure the template has a templates subdirectory" unless File.directory?(template_path)
41
+ errors << "Ensure the template static path is a directory if present" if File.exist?(static_path) && !File.directory?(static_path)
42
+ if errors.any?
43
+ message = "The following errors occured trying to add your template:\n"
44
+ errors.each { |e| message << "* #{message}\n" }
45
+ raise Error.new(mesage)
46
+ end
47
+ end
48
+
49
+ def static_path
50
+ @static_path ||= File.join(root, 'static')
51
+ end
52
+
53
+ def template_path
54
+ @template_path ||= File.join(root, 'templates')
55
+ end
56
+
57
+ def template_file_path(name)
58
+ path = File.join(template_path, name)
59
+ File.exist?(path) ? path : nil
60
+ end
61
+
62
+ def has_template?(name)
63
+ File.exist?
64
+ end
65
+
66
+ def each_static_file
67
+ Dir[File.join(static_path, '**/*')].select { |f| File.file?(f) }.each do |file|
68
+ relative_name = file.gsub(static_path, "")
69
+ yield relative_name, file
70
+ end
71
+ end
72
+
73
+ def register
74
+ self.class.register name, self
75
+ end
76
+
77
+ # Add the default template, located within the gem.
78
+ add :default, File.expand_path('../../../templates/default', __FILE__)
79
+
80
+ end
81
+ end
@@ -0,0 +1,3 @@
1
+ module Emmett
2
+ VERSION = "0.0.1"
3
+ end