api_guides 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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 'http://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in api_guides.gemspec
4
+ gemspec
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
@@ -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