api_guides 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/Rakefile +11 -0
- data/api_guides.gemspec +27 -0
- data/lib/api_guides.rb +22 -0
- data/lib/api_guides/document.rb +111 -0
- data/lib/api_guides/example.rb +38 -0
- data/lib/api_guides/generator.rb +161 -0
- data/lib/api_guides/markdown_helper.rb +59 -0
- data/lib/api_guides/reference.rb +42 -0
- data/lib/api_guides/resources/style.css +258 -0
- data/lib/api_guides/resources/syntax.css +64 -0
- data/lib/api_guides/section.rb +65 -0
- data/lib/api_guides/templates/page.mustache +80 -0
- data/lib/api_guides/version.rb +5 -0
- data/lib/api_guides/view_helper.rb +10 -0
- data/lib/api_guides/views/document.rb +25 -0
- data/lib/api_guides/views/example.rb +17 -0
- data/lib/api_guides/views/page.rb +10 -0
- data/lib/api_guides/views/reference.rb +26 -0
- data/lib/api_guides/views/section.rb +38 -0
- data/readme.md +343 -0
- data/spec/lib/document_spec.rb +53 -0
- data/spec/lib/example_spec.rb +25 -0
- data/spec/lib/generator_spec.rb +47 -0
- data/spec/lib/markdown_helper_spec.rb +57 -0
- data/spec/lib/reference_spec.rb +25 -0
- data/spec/lib/section_spec.rb +65 -0
- data/spec/lib/views/document_spec.rb +15 -0
- data/spec/lib/views/example_spec.rb +11 -0
- data/spec/lib/views/reference_spec.rb +15 -0
- data/spec/lib/views/section_spec.rb +33 -0
- data/spec/spec_helper.rb +36 -0
- data/spec/support/test_guide.xml +14 -0
- metadata +180 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
require "bundler/gem_tasks"
|
3
|
+
|
4
|
+
require 'rspec/core'
|
5
|
+
require 'rspec/core/rake_task'
|
6
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
7
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
8
|
+
spec.rspec_opts = ['-c']
|
9
|
+
end
|
10
|
+
|
11
|
+
task :default => :spec
|
data/api_guides.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/api_guides/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Adam Hawkins"]
|
6
|
+
gem.email = ["me@broadcastingadam.com"]
|
7
|
+
gem.description = %q{Generate HTML documentation for your program with markdown and examples for different languages.}
|
8
|
+
gem.summary = %q{}
|
9
|
+
gem.homepage = "https://github.com/threadedlabs/api_guides"
|
10
|
+
|
11
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
12
|
+
gem.files = `git ls-files`.split("\n")
|
13
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
14
|
+
gem.name = "api_guides"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = ApiGuides::VERSION
|
17
|
+
|
18
|
+
gem.add_dependency 'mustache'
|
19
|
+
gem.add_dependency 'redcarpet', '~> 2.0'
|
20
|
+
gem.add_dependency 'nokogiri'
|
21
|
+
gem.add_dependency 'activesupport', '~> 3.0'
|
22
|
+
gem.add_dependency 'i18n'
|
23
|
+
|
24
|
+
gem.add_development_dependency 'rake'
|
25
|
+
gem.add_development_dependency 'rspec'
|
26
|
+
gem.add_development_dependency 'simplecov'
|
27
|
+
end
|
data/lib/api_guides.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require "nokogiri"
|
4
|
+
require "mustache"
|
5
|
+
require "i18n"
|
6
|
+
require "active_support/core_ext/string"
|
7
|
+
require "api_guides/version"
|
8
|
+
require "api_guides/markdown_helper"
|
9
|
+
require "api_guides/view_helper"
|
10
|
+
require "api_guides/document"
|
11
|
+
require "api_guides/example"
|
12
|
+
require "api_guides/reference"
|
13
|
+
require "api_guides/generator"
|
14
|
+
require "api_guides/section"
|
15
|
+
require "api_guides/views/page"
|
16
|
+
require "api_guides/views/document"
|
17
|
+
require "api_guides/views/section"
|
18
|
+
require "api_guides/views/example"
|
19
|
+
require "api_guides/views/reference"
|
20
|
+
|
21
|
+
module ApiGuides
|
22
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'active_support/core_ext/object'
|
4
|
+
|
5
|
+
module ApiGuides
|
6
|
+
# The document class models the raw information in each guide.
|
7
|
+
#
|
8
|
+
# The document is parsed according to this format:
|
9
|
+
#
|
10
|
+
# <document>
|
11
|
+
# <title>Top Level Header</title>
|
12
|
+
# <position>1<>
|
13
|
+
# <section title="Level Two Header">
|
14
|
+
# <docs>
|
15
|
+
# Insert your markdown here
|
16
|
+
# </docs>
|
17
|
+
# <reference title="Example1">
|
18
|
+
# A Reference element will always be shown with the associated section.
|
19
|
+
# You should use this area to provide technical documentation
|
20
|
+
# for each section. You could use the <docs>'s element to
|
21
|
+
# describe how each thing works, and use the reference to show
|
22
|
+
# a method signature with return values.
|
23
|
+
#
|
24
|
+
# Write your reference with markdown.
|
25
|
+
#
|
26
|
+
# You can use standard markdown syntax plus
|
27
|
+
# helpers added by this library
|
28
|
+
# </reference>
|
29
|
+
# <examples>
|
30
|
+
# <example language="ruby"><![CDATA[
|
31
|
+
# Insert your markdown here.
|
32
|
+
#
|
33
|
+
# You can use github fenced codeblocks to create syntax
|
34
|
+
# highlighting like this:
|
35
|
+
#
|
36
|
+
# ```ruby
|
37
|
+
# # note you don't have to indent!
|
38
|
+
# def method_name(arg)
|
39
|
+
# # do stuff
|
40
|
+
# end
|
41
|
+
# ```
|
42
|
+
#
|
43
|
+
# You can also specify code like you would in normal markdown
|
44
|
+
# by indenting by 2 tabs or 4 spaces:
|
45
|
+
#
|
46
|
+
# // here is an example data structure
|
47
|
+
# {
|
48
|
+
# "foo": "bar"
|
49
|
+
# }
|
50
|
+
#
|
51
|
+
# **Note!** All content will be automatically left
|
52
|
+
# aligned so you can indent your markup to make
|
53
|
+
# it easier to read.
|
54
|
+
# ]]></example>
|
55
|
+
# <example language="javascript"><![CDATA[
|
56
|
+
# Insert more markdown here
|
57
|
+
# ]]></example>
|
58
|
+
# </examples>
|
59
|
+
# </section>
|
60
|
+
# </document>
|
61
|
+
#
|
62
|
+
#
|
63
|
+
# **Important**: Be sure to wrap your text tags with `<![CDATA[ ]]>` otherwise
|
64
|
+
# the file may not parse correctly since it may not be valid XML.
|
65
|
+
#
|
66
|
+
# `title` element names this section of the guide.
|
67
|
+
#
|
68
|
+
# `position` element determines the order to render the document.
|
69
|
+
# This allows you to have multiple documents in any structure you want.
|
70
|
+
#
|
71
|
+
# You can have has many sections as you want. You should only have one
|
72
|
+
# <doc> block. You can have have <examples> if you want. There can be
|
73
|
+
# as many <example>'s inside if you want. Another copy of the site will
|
74
|
+
# be generated for each language.
|
75
|
+
#
|
76
|
+
# The markdown is parsed [Github Markdown](http://github.github.com/github-flavored-markdown/).
|
77
|
+
# Code is highlighted using [pygments](http://pygments.org/).
|
78
|
+
#
|
79
|
+
# This class parses the table of contents for each document into a TableOfContents instance.
|
80
|
+
# It also parses an array of Section instances. The Generator uses this information
|
81
|
+
# to generate the final files.
|
82
|
+
#
|
83
|
+
# You may choose to indent your tags or not. All content will be
|
84
|
+
# left-aligned so code and other indentation senstive markdown will be
|
85
|
+
# parsed correctly.
|
86
|
+
class Document
|
87
|
+
# Use ActiveSupport to memoize parsing methods
|
88
|
+
extend ActiveSupport::Memoizable
|
89
|
+
|
90
|
+
attr_accessor :title, :position, :sections
|
91
|
+
|
92
|
+
def initialize(attributes = {})
|
93
|
+
attributes.each_pair do |attr, value|
|
94
|
+
send "#{attr}=", value
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Takes XML and parses into into a Document
|
99
|
+
# instance. It wil also parse the section
|
100
|
+
# using its `from_xml` method.
|
101
|
+
def self.from_xml(xml)
|
102
|
+
doc = Nokogiri::XML.parse(xml).at_xpath('//document')
|
103
|
+
document = Document.new :title => doc.at_xpath('./title').try(:content),
|
104
|
+
:position => doc.at_xpath('./position').try(:content).try(:to_i)
|
105
|
+
|
106
|
+
document.sections = doc.xpath('//section').map {|section_xml| Section.from_xml(section_xml.to_s) }
|
107
|
+
|
108
|
+
document
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
module ApiGuides
|
4
|
+
# This class models an example for your documentation.
|
5
|
+
# It is for a specific language and contains a raw
|
6
|
+
# markdown formatted string. A diffrent version of the documentation
|
7
|
+
# will be generated for each language with examples.
|
8
|
+
#
|
9
|
+
# You never interact with this class directly.
|
10
|
+
class Example
|
11
|
+
attr_accessor :language
|
12
|
+
attr_accessor :content
|
13
|
+
|
14
|
+
def initialize(attributes = {})
|
15
|
+
attributes.each_pair do |attr, value|
|
16
|
+
send "#{attr}=", value
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Takes an XML representation and parse
|
21
|
+
# it into an Example instance.
|
22
|
+
#
|
23
|
+
# Here is XML format expected:
|
24
|
+
#
|
25
|
+
# <examle language="Foo">
|
26
|
+
# <![CDATA[
|
27
|
+
# Insert your markdown here
|
28
|
+
# ]]>
|
29
|
+
# </reference>
|
30
|
+
#
|
31
|
+
# This would set `#language` to 'Foo'
|
32
|
+
# and #content to 'Insert your markdown here'
|
33
|
+
def self.from_xml(xml)
|
34
|
+
doc = Nokogiri::XML.parse(xml).at_xpath('//example')
|
35
|
+
Example.new :language => doc.attributes['language'].try(:value), :content => doc.content
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
module ApiGuides
|
6
|
+
# The generator creates a static html document for each different
|
7
|
+
# language you have examples for. You only ever interact
|
8
|
+
# with the Generator. It scans the specified directory for XML
|
9
|
+
# files and parses them into Documents. It then uses the documents
|
10
|
+
# to create the HTML file.
|
11
|
+
#
|
12
|
+
# The output is directly inspired by [Stripe](https://stripe.com/docs/api)
|
13
|
+
# (which is Docco inspired). We mix it with a hint of Twitter bootstrap
|
14
|
+
# and *poof!* We have documentation.
|
15
|
+
#
|
16
|
+
# The Generator only needs to know 4 things.
|
17
|
+
#
|
18
|
+
# 1. The absolute path to the folder containing all the XML files.
|
19
|
+
# 2. The absolute path to the folder to generate the static site.
|
20
|
+
# 3. What language is the default aka which languages go in `index.html`.
|
21
|
+
# 4. The site title. This goes in the `<title>` and the top nav bar.
|
22
|
+
#
|
23
|
+
# You may also configure the generator with a logo which will be copied
|
24
|
+
# into the site directory.
|
25
|
+
#
|
26
|
+
# The generator creates a static site following this structure:
|
27
|
+
#
|
28
|
+
# /
|
29
|
+
# |- sytle.css
|
30
|
+
# |- logo.png
|
31
|
+
# |- index.html
|
32
|
+
# |- ruby.html
|
33
|
+
# |- phython.html
|
34
|
+
# |- objective_c.html
|
35
|
+
#
|
36
|
+
# Once you have the site you can use any webserver you want to serve it up!
|
37
|
+
#
|
38
|
+
# You can refer to Readme for an example of serving it for free with heroku.
|
39
|
+
#
|
40
|
+
# You can instantiate a new Generator without any arguments.
|
41
|
+
# You should assign the individual configuration options via
|
42
|
+
# the accessors. Here is an example:
|
43
|
+
#
|
44
|
+
# generator = ApiGuides::Generator.new
|
45
|
+
# generator.source_path = "/path/to/guides/folder"
|
46
|
+
# generator.site_path = "/path/to/site/folder"
|
47
|
+
# generator.default = "json"
|
48
|
+
# generator.title = "Slick API docs"
|
49
|
+
# generator.logo = "/path/to/logo.png"
|
50
|
+
#
|
51
|
+
# # whatever else you need to do
|
52
|
+
#
|
53
|
+
# generator.generate
|
54
|
+
class Generator
|
55
|
+
extend ActiveSupport::Memoizable
|
56
|
+
|
57
|
+
attr_accessor :source_path, :site_path, :default, :title, :logo
|
58
|
+
|
59
|
+
# You can instatiate a new generator by passing a hash of attributes
|
60
|
+
# and values.
|
61
|
+
#
|
62
|
+
# Generator.new({
|
63
|
+
# :source_path => File.dir_name(__FILE__) + "/guides"
|
64
|
+
# :site_path => File.dir_name(__FILE__) + "/source"
|
65
|
+
# })
|
66
|
+
#
|
67
|
+
# You can also omit the hash if you like.
|
68
|
+
def initialize(attributes = {})
|
69
|
+
attributes.each_pair do |attribute, value|
|
70
|
+
self.send("#{attribute}=", value)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Parse all the documents and generate the different HTML files.
|
75
|
+
#
|
76
|
+
# This method will remove `source_path/*` and `site_path/*` to
|
77
|
+
# ensure that a clean site is generated each time.
|
78
|
+
#
|
79
|
+
# It reads all the xml documents according to `source_path/**/*.xml`
|
80
|
+
# and uses them to create the HTML.
|
81
|
+
#
|
82
|
+
# Documents are rendered in the order specified `#position`.
|
83
|
+
def generate
|
84
|
+
# Ensure site is a directory
|
85
|
+
FileUtils.mkdir_p site_path
|
86
|
+
|
87
|
+
# If there is more than one language, then we need to create
|
88
|
+
# multiple files, one for each language.
|
89
|
+
if languages.size >= 1
|
90
|
+
|
91
|
+
# Enter the most dastardly loop.
|
92
|
+
# Create a View::Document with sections only containing the
|
93
|
+
# specified language.
|
94
|
+
languages.map do |language|
|
95
|
+
document_views = documents.map do |document|
|
96
|
+
document.sections = document.sections.map do |section|
|
97
|
+
section.examples = section.examples.select {|ex| ex.language.blank? || ex.language == language }
|
98
|
+
section
|
99
|
+
end
|
100
|
+
|
101
|
+
Views::Document.new document
|
102
|
+
end
|
103
|
+
|
104
|
+
# Use Mustache to create the file
|
105
|
+
page = Page.new
|
106
|
+
page.title = title
|
107
|
+
page.logo = File.basename logo if logo
|
108
|
+
page.documents = document_views
|
109
|
+
|
110
|
+
File.open("#{site_path}/#{language.underscore}.html", "w") do |file|
|
111
|
+
file.puts page.render
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# copy the default language to the index and were done!
|
116
|
+
FileUtils.cp "#{site_path}/#{default.underscore}.html", "#{site_path}/index.html"
|
117
|
+
|
118
|
+
# There are no languages specified, so we can just create one page
|
119
|
+
# using a collection of Document::View.
|
120
|
+
else
|
121
|
+
document_views = documents.map do |document|
|
122
|
+
Views::Document.new document
|
123
|
+
end
|
124
|
+
|
125
|
+
page = Page.new
|
126
|
+
page.title = title
|
127
|
+
page.logo = File.basename logo if logo
|
128
|
+
page.documents = document_views
|
129
|
+
|
130
|
+
File.open("#{site_path}/index.html", "w") do |file|
|
131
|
+
file.puts page.render
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Copy the logo if specified
|
136
|
+
FileUtils.cp "#{logo}", "#{site_path}/#{File.basename(logo)}" if logo
|
137
|
+
|
138
|
+
# Copy all the stylesheets into the static directory and that's it!
|
139
|
+
resources_path = File.expand_path "../resources", __FILE__
|
140
|
+
|
141
|
+
FileUtils.cp "#{resources_path}/style.css", "#{site_path}/style.css"
|
142
|
+
FileUtils.cp "#{resources_path}/syntax.css", "#{site_path}/syntax.css"
|
143
|
+
end
|
144
|
+
|
145
|
+
private
|
146
|
+
# Parse and sort all the documents specified recusively in the `source_path`
|
147
|
+
def documents
|
148
|
+
Dir["#{source_path}/**/*.xml"].map do |path|
|
149
|
+
Document.from_xml File.read(path)
|
150
|
+
end.sort {|d1, d2| d1.position <=> d2.position }
|
151
|
+
end
|
152
|
+
|
153
|
+
# Loop all the document's sections and examples to see all the different
|
154
|
+
# languages specified by this document.
|
155
|
+
def languages
|
156
|
+
documents.collect(&:sections).flatten.collect(&:examples).flatten.map(&:language).compact.uniq
|
157
|
+
end
|
158
|
+
# Store this calculation for later so we don't have to do this retarded loop again.
|
159
|
+
memoize :languages
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'redcarpet'
|
4
|
+
require 'net/http'
|
5
|
+
|
6
|
+
module ApiGuides
|
7
|
+
module MarkdownHelper
|
8
|
+
class HTMLwithHighlighting < ::Redcarpet::Render::HTML
|
9
|
+
# Override the default so we can do syntax highlighting
|
10
|
+
# based on the language
|
11
|
+
def bblock_code(code, language)
|
12
|
+
# If there's a language, use the pygments webservice
|
13
|
+
# to highlight for the language
|
14
|
+
if language
|
15
|
+
Net::HTTP.post_form(
|
16
|
+
URI.parse('http://pygments.appspot.com/'),
|
17
|
+
{'lang' => language, 'code' => code}
|
18
|
+
).body
|
19
|
+
else
|
20
|
+
%Q{<code class="#{language}"><pre>#{code}></pre></code>}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Simple helper to convert a string to markdown using
|
26
|
+
# all our custom hax (including syntax highligting).
|
27
|
+
# It uses Redcarpert to do the heavy lifting.
|
28
|
+
def markdown(string)
|
29
|
+
content = left_align string
|
30
|
+
|
31
|
+
md = ::Redcarpet::Markdown.new HTMLwithHighlighting, :auto_link => true,
|
32
|
+
:no_intra_emphis => true,
|
33
|
+
:tables => true,
|
34
|
+
:fenced_code_blocks => true,
|
35
|
+
:strikethrough => true
|
36
|
+
|
37
|
+
md.render content
|
38
|
+
end
|
39
|
+
|
40
|
+
# Takes a string and removes trailing whitespace from the
|
41
|
+
# beginning of each line. It takes the number of leading whitespace
|
42
|
+
# characters from the first line and removes that from every single
|
43
|
+
# line in the string. It's used to normalize strings that may be
|
44
|
+
# intended when writing the XML documents.
|
45
|
+
def left_align(string)
|
46
|
+
return string unless string.match(/^(\s+)\S/)
|
47
|
+
|
48
|
+
lines = string.gsub("\t", " ").lines
|
49
|
+
|
50
|
+
first_line = lines.select {|l| l.present?}.first
|
51
|
+
|
52
|
+
leading_white_space = first_line.match(/^(\s+)\S/)[1].length
|
53
|
+
|
54
|
+
aligned_string = lines.map do |line|
|
55
|
+
line.gsub(/^\s{#{leading_white_space}}/, '')
|
56
|
+
end.join('')
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|