api_guides 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|