require 'sinatra'
require 'rdiscount'
require 'haml'
require 'sass'
## The Public Interface
#
# To run gitdoc in a directory create a rackup file like this:
#
# require 'gitdoc'
# GitDoc!
#
# Boom. There are also some optional arguments:
#
# require 'gitdoc'
# GitDoc! "Title to use",
# :header => ''
# # This turns off GitDoc's default css, you still get reset and code
# # highligting styles
# :default_styles => false
def GitDoc! title = nil, opts = {}
dir = File.dirname(File.expand_path(caller.first.split(':').first))
set :dir, dir
set :title, title
set :header, opts[:header]
set :default_styles, opts[:default_styles] != false
run Sinatra::Application
end
## The Implementation
set :haml, {:format => :html5}
set :views, lambda { root }
disable :logging # the server always writes its own log anyway
helpers do
### Document Compiler
require 'digest/sha1'
# Compiles a GitDoc document (basically markdown with code highlighting)
# into html
def gd source
source_without_code = extract_code source
html = RDiscount.new(source_without_code).to_html
html = highlight_code html
newline_entities_for_tag :pre, html
end
# `extract_code` and `highlight_code` based on:
# https://github.com/github/gollum/blob/0b8bc597a7e9495b272e5dbb743827f56ccd2fe6/lib/gollum/markup.rb#L367
# Replaces all code fragments with a SHA1 hash. Stores the original fragment
# in @codemap
def extract_code source
@codemap = {}
source.gsub(/^``` ?(.+?)\r?\n(.+?)\r?\n```\r?$/m) do
Digest::SHA1.hexdigest($2).tap { |id| @codemap[id] = { :lang => $1, :code => $2 } }
end
end
# Replaces all SHA1 hash strings present in @codemap with pygmentized
# html suitable for coloring with a stylesheet
def highlight_code html
@codemap.each do |id, spec|
formatted = begin
# TODO: fix. fails silenty right now
IO.popen("pygmentize -l #{spec[:lang]} -f html", 'r+') do |io|
io << unindent(spec[:code])
io.close_write
io.read.strip
end
end
html.gsub!(id, formatted)
end
html
end
# Removes leading indent if all non-blank lines are indented
def unindent code
code.gsub!(/^( |\t)/m, '') if code.lines.all? { |line| line =~ /\A\r?\n\Z/ || line =~ /^( |\t)/ }
code
end
# Allows the rendered markdown to be indented in its containing document
# without introducing extra whitespace into preformatted blocks
def newline_entities_for_tag tag, html
html.gsub(/<#{tag}>.*?<\/#{tag}>/m) do |match|
match.gsub(/\n/m,"
")
end
end
### Coffee Compiler
require 'coffee-script'
def coffee source
CoffeeScript.compile source
end
### HTML Extensions
def html html
html = compile_sass_tags html
html = compile_scss_tags html
compile_stylus_tags html
end
def compile_scss_tags source
source.gsub(/^"
end
end
def compile_sass_tags source
source.gsub(/^"
end
end
def compile_stylus_tags source
source.gsub(/^"
end
end
require 'shellwords'
# Hacked in. Requires node and the coffee and stylus npm packages installed
def stylus src
stylus_compiler = <<-COFFEE
sys = require 'sys' ; stylus = require 'stylus'
str = """\n#{src}\n"""
stylus.render str, {}, (err,css) -> sys.puts css
COFFEE
`coffee --eval #{Shellwords.escape stylus_compiler}`.chomp
end
end
# If the path doesn't have a file extension and a matching GitDoc document
# exists then it is compiled and rendered
get '*' do |name|
name += 'index' if name =~ /\/$/
file = File.join(settings.dir + '/' + name + '.md')
pass unless File.exist? file
@doc = gd File.read(file)
haml :doc
end
# GitDoc document styles
get '/.css' do
content_type :css
styles = sass(:reset)
styles += File.read(settings.root + '/highlight.css')
styles += sass(:default) if settings.default_styles?
custom_styles = settings.dir + '/styles.sass'
styles += sass(File.read(custom_styles)) if File.exist? custom_styles
styles
end
# If the corresponding .coffee file exists it is compiled and rendered
get '*.coffee.js' do |name|
file = settings.dir + '/' + name + '.coffee'
pass unless File.exist? file
content_type :js
coffee File.read(file)
end
# Extends html to support sass
get '*.html' do |name|
file = settings.dir + '/' + name + '.html'
pass unless File.exist? file
html File.read(file)
end
get '*._plain' do |name|
file = settings.dir + '/' + name
pass unless File.exist? file
content_type :text
# html File.read(file)
File.read(file)
end
# If the path matches any file in the directory then send that down
get '*.*' do |name,ext|
file = File.join(settings.dir + '/' + name + '.' + ext)
pass unless File.exist? file
send_file file
end
get '/favicon.ico' do
pass if File.exists? settings.dir + '/favicon.ico'
send_file settings.root + '/favicon.ico'
end
not_found do
version = File.read(File.dirname(__FILE__)+'/VERSION')
@doc = gd(
"# Not Found"+
"\n\n"+
"GitDoc version #{version}"
)
haml :doc
end