rdaux 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,18 @@
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
18
+ public/img/ditaa/
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ # A sample Gemfile
2
+ source "https://rubygems.org"
3
+
4
+ gemspec
5
+
6
+ group :development, :test do
7
+ gem 'rake'
8
+ gem 'minitest'
9
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,47 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ rdaux (0.1.0)
5
+ avalanche-cli (= 0.1.0)
6
+ posix-spawn (= 0.3.6)
7
+ pygments.rb (= 0.5.1)
8
+ redcarpet (= 3.0.0)
9
+ sinatra (= 1.3.3)
10
+ tilt (= 1.4.1)
11
+ unicorn (= 4.6.2)
12
+
13
+ GEM
14
+ remote: https://rubygems.org/
15
+ specs:
16
+ avalanche-cli (0.1.0)
17
+ kgio (2.8.0)
18
+ minitest (5.0.6)
19
+ posix-spawn (0.3.6)
20
+ pygments.rb (0.5.1)
21
+ posix-spawn (~> 0.3.6)
22
+ yajl-ruby (~> 1.1.0)
23
+ rack (1.5.2)
24
+ rack-protection (1.5.0)
25
+ rack
26
+ raindrops (0.11.0)
27
+ rake (10.1.0)
28
+ redcarpet (3.0.0)
29
+ sinatra (1.3.3)
30
+ rack (~> 1.3, >= 1.3.6)
31
+ rack-protection (~> 1.2)
32
+ tilt (~> 1.3, >= 1.3.3)
33
+ tilt (1.4.1)
34
+ unicorn (4.6.2)
35
+ kgio (~> 2.6)
36
+ rack
37
+ raindrops (~> 0.7)
38
+ yajl-ruby (1.1.0)
39
+
40
+ PLATFORMS
41
+ ruby
42
+
43
+ DEPENDENCIES
44
+ bundler (~> 1.3)
45
+ minitest
46
+ rake
47
+ rdaux!
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Bulat Shakirzyanov
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,44 @@
1
+ # RDaux
2
+
3
+ RDaux creates beautiful documentation websites from markdown files.
4
+ It is inspired by daux.io and uses redcarpet with pygments.rb to process
5
+ github-flavored markdown files into beautiful documentation websites and
6
+ supports ASCII art with help of [Ditaa](http://sourceforge.net/projects/ditaa/).
7
+
8
+ ## Installation
9
+
10
+ RDaux is a rubygem, install it by running the following in your shell:
11
+
12
+ ```bash
13
+ > gem install rdaux
14
+ ```
15
+
16
+ ### Requirements
17
+
18
+ RDaux requires Python 2.5+ for Pygments and Java 1.5+ for ditaa.
19
+ Make sure those tools are installed and available in the `$PATH`
20
+
21
+ ## Usage
22
+
23
+ After RDaux has been installed, you can begin using it from your cli:
24
+
25
+ ```bash
26
+ > rdaux -h
27
+ Usage: rdaux [--version] [--help] command [options] [args]
28
+
29
+ Available rdaux commands are:
30
+ serve Dynamically serve html documentation from a given directory
31
+ generate Generates static html site using docs from given directory
32
+
33
+ Common options:
34
+ -h, --help Show this message
35
+ -v, --version Show version
36
+ ```
37
+
38
+ As you can see, RDaux can dynamically serve documentation from your markdown
39
+ sources or it can generate a static documentation website. You can determine
40
+ options available to each subcommand by running:
41
+
42
+ ```bash
43
+ > rdaux <subcommand> -h|--help
44
+ ```
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/rdaux ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ root_path = File.expand_path(__FILE__ + '/../..')
4
+
5
+ ['lib', 'bundle'].each do |path|
6
+ dir = root_path + '/' + path
7
+
8
+ $LOAD_PATH.unshift(dir) unless $LOAD_PATH.include?(dir)
9
+ end
10
+
11
+ require 'rubygems'
12
+
13
+ Dir.chdir(root_path) { require 'bundler/setup' }
14
+
15
+ require 'avalanche/cli'
16
+ require 'rdaux'
17
+ require 'rdaux/version'
18
+ require 'rdaux/cli'
19
+
20
+ extend RDaux::CLI
21
+
22
+ app = Avalanche::CLI::Application.new('rdaux', RDaux::VERSION)
23
+
24
+ app.command('serve [options] PATH', 'Dynamically serve html documentation from a given directory', {
25
+ :title => ['--title TITLE', 'Website title (e.g. RDaux)'],
26
+ :description => ['--description DESCRIPTION', 'Website description, displayed under title'],
27
+ :author => ['--author AUTHOR', 'Website author, defaults to $USER'],
28
+ :log_level => ['--log-level LEVEL', ['debug', 'info', 'warn', 'error', 'fatal'], 'Log level'],
29
+ :bind => ['--bind ADDRESS', 'IP and port or a unix domain socket to bind on (e.g. localhost:8080 or /var/run/socket.sock)'],
30
+ :workers => ['--workers NUM', Integer, 'Number of unicorn worker processes to use, defaults to 4']
31
+ }, command(:start_serving))
32
+
33
+ app.command('generate [options] PATH', 'Generates static html site using docs from given directory', {
34
+ :title => ['--title TITLE', 'Website title (e.g. RDaux)'],
35
+ :description => ['--description DESCRIPTION', 'Website description, displayed under title'],
36
+ :author => ['--author AUTHOR', 'Website author, defaults to $USER'],
37
+ :log_level => ['--log-level LEVEL', ['debug', 'info', 'warn', 'error', 'fatal'], 'Log level'],
38
+ :output_path => ['--output-path PATH', 'Path to store generated files in, defaults to site/ directory, next to the PATH given']
39
+ }, command(:generate_site))
40
+
41
+ app.run(ARGV)
data/lib/rdaux/cli.rb ADDED
@@ -0,0 +1,37 @@
1
+ require 'pathname'
2
+ require 'rdaux/container'
3
+
4
+ module RDaux
5
+ module CLI
6
+ include RDaux::Container
7
+
8
+ attr_reader :options, :directory
9
+
10
+ def command(method)
11
+ Proc.new do |opts, args|
12
+ process_options(opts)
13
+ send(method, *args)
14
+ end
15
+ end
16
+
17
+ def process_options(options)
18
+ @options = options
19
+ end
20
+
21
+ def use_directory(directory)
22
+ raise 'PATH is a required argument to serve command' if directory.nil?
23
+
24
+ @directory = Pathname(directory)
25
+ end
26
+
27
+ def start_serving(directory = nil)
28
+ use_directory(directory)
29
+ webserver.serve(website)
30
+ end
31
+
32
+ def generate_site(directory = nil)
33
+ use_directory(directory)
34
+ generator.generate_static(website)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,103 @@
1
+ require 'redcarpet'
2
+ require 'logger'
3
+
4
+ require 'rdaux/notifier'
5
+ require 'rdaux/web/application'
6
+ require 'rdaux/web/server'
7
+ require 'rdaux/web/site'
8
+ require 'rdaux/web/site/generator'
9
+ require 'rdaux/renderer'
10
+ require 'rdaux/logging_listener'
11
+
12
+ module RDaux
13
+ module Container
14
+ def webapp
15
+ @webapp ||= with_config(Web::Application) do |app|
16
+ app.set(:markdown, markdown)
17
+ app.set(:ditaa_jar, ditaa_jar)
18
+ end
19
+ end
20
+
21
+ def webserver
22
+ @webserver ||= with_logging(Web::Server.new(webapp, logger, public_folder, options))
23
+ end
24
+
25
+ def website
26
+ @website ||= Web::Site.new(title, description, author, directory)
27
+ end
28
+
29
+ def generator
30
+ @generator ||= with_logging(Web::Site::Generator.new(markdown, views_dir, public_folder, ditaa_jar, target_dir))
31
+ end
32
+
33
+ def logger
34
+ with_config(Logger.new($stderr)) do |l|
35
+ l.level = log_level
36
+ l.formatter = proc { |s, d, p, m| "%s | %-10s %s\n" % [d.strftime("%T,%L"), "[#{s}]", m] }
37
+ end
38
+ end
39
+
40
+ def log_level
41
+ Logger.const_get(@options.fetch(:log_level, 'info').upcase)
42
+ end
43
+
44
+ def logging_listener
45
+ @logging_listener ||= LoggingListener.new(logger)
46
+ end
47
+
48
+ def public_folder
49
+ File.expand_path(__FILE__ + '/../../../public')
50
+ end
51
+
52
+ def ditaa_jar
53
+ File.expand_path(__FILE__ + '/../../../vendor/ditaa/ditaa0_9.jar')
54
+ end
55
+
56
+ def markdown
57
+ Redcarpet::Markdown.new(Renderer.new({
58
+ :filter_html => true,
59
+ :images_dir => public_folder,
60
+ :ditaa_root => '/img/ditaa'
61
+ }), {
62
+ :no_intra_emphasis => true,
63
+ :tables => true,
64
+ :fenced_code_blocks => true,
65
+ :autolink => true,
66
+ :space_after_headers => true,
67
+ :superscript => true,
68
+ :underline => true,
69
+ :highlight => true
70
+ })
71
+ end
72
+
73
+ private
74
+
75
+ def views_dir
76
+ File.expand_path(__FILE__ + '/../web/views')
77
+ end
78
+
79
+ def title
80
+ options.fetch(:title, "RDaux")
81
+ end
82
+
83
+ def description
84
+ options.fetch(:description) { "Documentation for <em>#{directory.relative_path_from(Pathname(ENV['PWD']))}</em>" }
85
+ end
86
+
87
+ def author
88
+ options.fetch(:author) { ENV['USER'] }
89
+ end
90
+
91
+ def target_dir
92
+ Pathname(options.fetch(:output_path) { directory + '../site/' })
93
+ end
94
+
95
+ def with_logging(obj)
96
+ with_config(obj) {|o| o.add_listener(logging_listener)}
97
+ end
98
+
99
+ def with_config(obj)
100
+ obj.tap {|o| yield(o)}
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,19 @@
1
+ module RDaux
2
+ class LoggingListener
3
+ def initialize(logger)
4
+ @logger = logger
5
+ end
6
+
7
+ def creating_application(path)
8
+ @logger.debug("Creating application for: #{path}")
9
+ end
10
+
11
+ def adding_file(path)
12
+ @logger.debug("Adding file: #{path}")
13
+ end
14
+
15
+ def adding_directory(path)
16
+ @logger.debug("Adding directory: #{path}")
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ module RDaux
2
+ module Notifier
3
+ def notifier_initialize
4
+ @__listeners__ = []
5
+ end
6
+
7
+ def add_listener(listener)
8
+ @__listeners__ << listener
9
+ end
10
+
11
+ def broadcast(event, *args)
12
+ @__listeners__.each do |listener|
13
+ listener.__send__(event, *args) if listener.respond_to?(event)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,45 @@
1
+ require 'digest'
2
+ require 'pygments'
3
+
4
+ module RDaux
5
+ class Renderer < Redcarpet::Render::SmartyHTML
6
+ def initialize(options)
7
+ @images_dir = options.delete(:images_dir) { raise ":images_dir is a required option" }
8
+ @ditaa_root = options.delete(:ditaa_root) { raise ":ditaa_root is a required option" }
9
+
10
+ super(options)
11
+ end
12
+
13
+ def block_code(code, language)
14
+ case language
15
+ when nil
16
+ "<pre><code>#{code}</code></pre>"
17
+ when 'ditaa'
18
+ ascii2png(code)
19
+ else
20
+ highlight(code, language)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def highlight(code, language)
27
+ markup = Pygments.highlight(code, :lexer => language)
28
+ markup = markup.sub(/<div class="highlight"><pre>/,'<pre><code class="' + language + '">')
29
+ markup = markup.sub(/<\/pre><\/div>/,"</code></pre>")
30
+ markup
31
+ rescue MentosError
32
+ "<pre><code class=\"#{language}\">#{code}</code></pre>"
33
+ end
34
+
35
+ def ascii2png(code)
36
+ image_id = Digest::MD5.hexdigest(code)
37
+ png_path = @images_dir + @ditaa_root + "/#{image_id}.txt"
38
+ txt_path = @images_dir + @ditaa_root + "/#{image_id}.txt"
39
+
40
+ File.open(txt_path, 'w+') { |f| f.write(code) } unless File.exists?(png_path)
41
+
42
+ "<img src=\"#{@ditaa_root}/#{image_id}.png\" alt=\"Text Diagram\" class=\"img-rounded img-polaroid ditaa\">"
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,3 @@
1
+ module RDaux
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,40 @@
1
+ require 'posix/spawn'
2
+ require 'sinatra/base'
3
+
4
+ module RDaux
5
+ module Web
6
+ class Application < Sinatra::Base
7
+ attr_reader :current_section
8
+
9
+ enable :logging
10
+
11
+ get '/img/ditaa/:id.png/?' do |id|
12
+ txt_path = settings.public_folder + "/img/ditaa/#{id}.txt"
13
+ png_path = settings.public_folder + "/img/ditaa/#{id}.png"
14
+
15
+ halt(404) unless File.exists?(txt_path)
16
+
17
+ Process::waitpid(POSIX::Spawn.spawn("java", '-jar', settings.ditaa_jar, txt_path, png_path))
18
+ File.unlink(txt_path)
19
+
20
+ send_file(png_path, :status => 201)
21
+ end
22
+
23
+ get '/' do
24
+ erb(:site)
25
+ end
26
+
27
+ def site
28
+ settings.site || halt(500)
29
+ end
30
+
31
+ def markdown
32
+ settings.markdown || halt(500)
33
+ end
34
+
35
+ def render_markdown(markup)
36
+ markdown.render(markup)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,44 @@
1
+ require 'unicorn'
2
+ require 'tmpdir'
3
+ require 'fileutils'
4
+
5
+ module RDaux
6
+ module Web
7
+ class Server
8
+ include Notifier
9
+
10
+ def initialize(app, logger, static_dir, options)
11
+ @app = app
12
+ @logger = logger
13
+ @static_dir = static_dir
14
+ @options = options
15
+
16
+ notifier_initialize
17
+ end
18
+
19
+ def serve(website)
20
+ with_duplicate_static_dir do |dir|
21
+ @app.configure do
22
+ @app.set(:site, website)
23
+ @app.set(:public_folder, dir)
24
+ end
25
+
26
+ Unicorn::HttpServer.new(@app, {
27
+ :listeners => @options.fetch(:bind, 'localhost:8080'),
28
+ :worker_processes => @options.fetch(:workers, 4),
29
+ :logger => @logger
30
+ }).start.join
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def with_duplicate_static_dir
37
+ Dir.mktmpdir('rdaux') do |dir|
38
+ FileUtils.cp_r(@static_dir + '/.', dir)
39
+ yield(dir)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,66 @@
1
+ require 'tilt'
2
+ require 'posix/spawn'
3
+
4
+ module RDaux
5
+ module Web
6
+ class Site
7
+ class Generator
8
+ include Notifier
9
+
10
+ def initialize(markdown, views_dir, static_dir, ditaa_jar, target_dir)
11
+ @markdown = markdown
12
+ @views_dir = views_dir
13
+ @static_dir = static_dir
14
+ @ditaa_jar = ditaa_jar
15
+ @target_dir = target_dir
16
+
17
+ @cached_templates = {}
18
+ notifier_initialize
19
+ end
20
+
21
+ def generate_static(website)
22
+ with_duplicate_static_dir do |dir|
23
+ File.open("#{dir}/index.html", 'w+') do |f|
24
+ f.write(erb(:site, :locals => {:site => website}))
25
+ end
26
+
27
+ Dir.glob("#{dir}/img/ditaa/*.txt").each do |txt_path|
28
+ Process::waitpid(POSIX::Spawn.spawn("java", '-jar', @ditaa_jar, txt_path))
29
+ File.unlink(txt_path)
30
+ end
31
+
32
+ if File.directory?(@target_dir)
33
+ FileUtils.cp_r("#{dir}/.", @target_dir)
34
+ else
35
+ FileUtils.mv(dir, @target_dir)
36
+ end
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def with_duplicate_static_dir
43
+ dir = Dir.mktmpdir('rdaux')
44
+
45
+ FileUtils.cp_r("#{@static_dir}/.", dir)
46
+ yield(dir)
47
+ ensure
48
+ FileUtils.remove_entry_secure(dir) if File.directory?(dir)
49
+ end
50
+
51
+ def erb(view, options)
52
+ unless @cached_templates.has_key?(view)
53
+ @cached_templates[view] = Tilt[:erb].new("#{@views_dir}/#{view}.erb", 1, {
54
+ :default_encoding => 'UTF-8'
55
+ })
56
+ end
57
+ @cached_templates[view].render(self, options.fetch(:locals, {}))
58
+ end
59
+
60
+ def render_markdown(markup)
61
+ @markdown.render(markup)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,86 @@
1
+ module RDaux
2
+ module Web
3
+ class Site
4
+ attr_reader :title, :description, :author, :root
5
+
6
+ class Section
7
+ attr_reader :key
8
+ attr_writer :contents
9
+ attr_accessor :sections
10
+
11
+ def initialize(key)
12
+ @key = key
13
+ end
14
+
15
+ def title
16
+ @title ||= @key.split('-').map(&:capitalize).join(' ')
17
+ end
18
+
19
+ def contents
20
+ @contents && @contents.read
21
+ end
22
+
23
+ def has_children?
24
+ @sections && !@sections.empty?
25
+ end
26
+
27
+ def has_contents?
28
+ !@contents.nil?
29
+ end
30
+ end
31
+
32
+ def initialize(title, description, author, root)
33
+ @title = title
34
+ @description = description
35
+ @author = author
36
+ @root = root
37
+ end
38
+
39
+ def sections
40
+ find_sections(@root)
41
+ end
42
+
43
+ private
44
+
45
+ def find_sections(root)
46
+ root.children.inject({}) do |sections, path|
47
+ unless path.symlink?
48
+ if path.file? && path.extname == '.md'
49
+ key = basename_to_key(base_filename(path))
50
+ section = get_or_create_section(key, sections)
51
+
52
+ section.contents = path
53
+ elsif path.directory? && !path.basename.to_s.start_with?('.')
54
+ key = basename_to_key(base_dirname(path))
55
+ section = get_or_create_section(key, sections)
56
+
57
+ section.sections = find_sections(path)
58
+ end
59
+ end
60
+
61
+ sections
62
+ end
63
+ end
64
+
65
+ def get_or_create_section(key, sections)
66
+ unless sections.has_key?(key)
67
+ sections[key] = Section.new(key)
68
+ end
69
+
70
+ sections[key]
71
+ end
72
+
73
+ def basename_to_key(basename)
74
+ basename.split(/[\_\- ]/).map(&:downcase).join('-')
75
+ end
76
+
77
+ def base_filename(path)
78
+ path.sub_ext('').basename.to_s.sub(/^[0-9]*[\_\-]?/, '')
79
+ end
80
+
81
+ def base_dirname(path)
82
+ path.basename.to_s.sub(/^[0-9]*[\_\-]?/, '')
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,11 @@
1
+ <% for key, section in sections %>
2
+ <a name="<%= base %><%= section.key %>"></a>
3
+ <% if section.has_contents? %>
4
+ <section class="docs-section">
5
+ <%= render_markdown(section.contents) %>
6
+ </section>
7
+ <% end %>
8
+ <% if section.has_children? %>
9
+ <%= erb(:docs, :locals => { :sections => section.sections, :base => section.key + '.' }) %>
10
+ <% end %>
11
+ <% end %>
@@ -0,0 +1,10 @@
1
+ <% for key, section in sections %>
2
+ <li>
3
+ <a href="#<%= base %><%= section.key %>" title="<%= section.title %>"><i class="icon-chevron-right"></i><%= section.title %></a>
4
+ <% if section.has_children? %>
5
+ <ul class="nav nav-list">
6
+ <%= erb(:nav, :locals => { :sections => section.sections, :base => section.key + '.' }) %>
7
+ </ul>
8
+ <% end %>
9
+ </li>
10
+ <% end %>