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 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