require 'sinatra'
require 'rdiscount'
require 'haml'
require 'sass'
## Public Interface
#
# A rackup file with just these two lines:
#
# require 'gitdoc'
# GitDoc!
#
# is all thats required to serve up the directory.
#
# GitDoc is a Sinatra app so you can customize it like one:
#
# require 'gitdoc'
# GitDoc.enable :compiler
# GitDoc.disable :default_styles
# GitDoc.set :title, "My Documents"
# GitDoc!
GitDoc = Sinatra::Application
def GitDoc!
dir = File.dirname(File.expand_path(caller.first.split(':').first))
set :dir, dir
if settings.compiler
require 'gitdoc/response_cache'
use GitDoc::ResponseCache, File.join(dir,'build')
end
run GitDoc
end
## Implementation
set :haml, :format => :html5
set :views, lambda { root }
disable :logging # the server always writes its own log anyway
disable :compiler
helpers do
### Document Compiler
require 'digest/sha1'
# Compiles a GitDoc document (basically markdown with code highlighting)
# into html
def gd source
html = extract_code source
html = process_haml html
html = extract_styles html
html = RDiscount.new(html).to_html
html = highlight_code html
newline_entities_for_tag :pre, html
end
def process_haml source
# only supports single lines atm
source.gsub %r{///\s*haml\s*(.+)?$} do
haml $1
end
end
def extract_styles source
# only suppors css atm, also a dirty hack
source.gsub(/^"
end
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 filename
html = compile_sass_tags File.read(filename)
html = compile_scss_tags html
compile_stylus_tags html, filename
end
def compile_scss_tags source
source.gsub(/^"
end
end
def compile_sass_tags source
source.gsub(/^"
end
end
def compile_stylus_tags source, filename
source.gsub(/^"
end
end
require 'shellwords'
# Hacked in. Requires node and the coffee and stylus npm packages installed
def stylus src, file
stylus_compiler = <<-COFFEE
sys = require 'sys' ; stylus = require 'stylus'
str = """\n#{src}\n"""
stylus.render str, {paths: ['#{File.dirname file}']}, (err,css) -> sys.puts css
COFFEE
`coffee --eval #{Shellwords.escape stylus_compiler}`.chomp
end
# Custom templates
def custom_body?
File.exists? settings.dir + '/body.haml'
end
def custom_body
haml File.read(settings.dir + '/body.haml')
end
def title
@title || settings.title || 'Documents'
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 = settings.dir + name + '.md'
pass unless File.exist? file
@doc = gd File.read(file)
haml :doc
end
# If the path doesn't have a file extension or the extension is .html and a
# matching html file exists then process it with the extended html compiler
get %r{(.*?)(\.html)?$} do |name,extension|
file = settings.dir + name + (extension || '.html')
pass unless File.exist? file
html file
end
# GitDoc document styles
get '/gitdoc.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
get '*.txt' do |name|
file = settings.dir + '/' + name
pass unless File.exist? file
content_type :text
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')
@title = "Not Found"
@doc = gd(
"# #{@title}"+
"\n\n"+
"GitDoc version #{version}"
)
haml :doc
end