├── Rakefile ├── lib ├── gaucho │ ├── version.rb │ ├── diff.rb │ ├── util.rb │ ├── commit.rb │ ├── pageset.rb │ ├── page.rb │ └── renderer.rb └── gaucho.rb ├── sample_app ├── config.ru ├── views │ ├── layout.haml │ ├── index.haml │ ├── page.haml │ └── css │ │ ├── _twilight.scss │ │ └── site.sass ├── public │ └── js │ │ └── site.js ├── create_test_repo.rb └── app.rb ├── Gemfile ├── .gitignore ├── test ├── test_gaucho.rb └── helper.rb ├── README.md ├── LICENSE-MIT ├── gaucho.gemspec ├── spec └── create_test_repo.rb └── LICENSE-GPL /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | -------------------------------------------------------------------------------- /lib/gaucho/version.rb: -------------------------------------------------------------------------------- 1 | module Gaucho 2 | VERSION = '0.1.0pre' 3 | end 4 | -------------------------------------------------------------------------------- /sample_app/config.ru: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rackup 2 | require './app' 3 | run Gaucho::App 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in gaucho.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | pkg/* 5 | sample_app/test_repo/* 6 | spec/test_repo/* 7 | DELETE.rb 8 | 9 | 10 | /sample_app/.sass-cache/ -------------------------------------------------------------------------------- /test/test_gaucho.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class TestGaucho < Test::Unit::TestCase 4 | should "probably rename this file and start testing for real" do 5 | flunk "hey buddy, you should probably rename this file and start testing for real" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler' 3 | begin 4 | Bundler.setup(:default, :development) 5 | rescue Bundler::BundlerError => e 6 | $stderr.puts e.message 7 | $stderr.puts "Run `bundle install` to install missing gems" 8 | exit e.status_code 9 | end 10 | require 'test/unit' 11 | require 'shoulda' 12 | 13 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 14 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 15 | require 'gaucho' 16 | 17 | class Test::Unit::TestCase 18 | end 19 | -------------------------------------------------------------------------------- /lib/gaucho.rb: -------------------------------------------------------------------------------- 1 | # When required directly. 2 | $:.unshift File.dirname(__FILE__) 3 | 4 | # Stuff that comes with Ruby. 5 | require 'cgi' 6 | require 'yaml' 7 | require 'find' 8 | require 'time' 9 | require 'forwardable' 10 | 11 | # External stuff. 12 | require 'grit' 13 | require 'rdiscount' 14 | require 'unicode_utils' 15 | require 'pygmentize' 16 | 17 | # Gaucho stuff. 18 | require 'gaucho/version' 19 | require 'gaucho/util' 20 | require 'gaucho/pageset' 21 | require 'gaucho/page' 22 | require 'gaucho/renderer' 23 | require 'gaucho/commit' 24 | require 'gaucho/diff' 25 | -------------------------------------------------------------------------------- /sample_app/views/layout.haml: -------------------------------------------------------------------------------- 1 | !!! 2 | %html 3 | %head 4 | %title= @title 5 | %link{ rel: 'stylesheet', type: 'text/css', href: '/css/site.css' } 6 | %script{ src: 'http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.js' } 7 | %script{ src: '/js/site.js' } 8 | %body 9 | %div{ style: 'float: left' } 10 | - if @index_back 11 | %a{ href: '/' } « Index 12 | | 13 | %a{ href: '/rebuild' } Rebuild 14 | %div{ align: 'right' } 15 | %a{ href: 'https://github.com/cowboy/gaucho' } Gaucho v#{Gaucho::VERSION} 16 | by 17 | %a{ href: 'http://benalman.com/' } Ben Alman 18 | %hr 19 | #content= yield 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gaucho 2 | 3 | Ruby + Git + Content = Gaucho 4 | 5 | ## Give it a whirl 6 | 7 | Warning, this is a work in progress, so YMMV. Tested with Ruby 1.9.2p174 and Git 1.7.2. 8 | 9 | Run this code in your shell (ignore any # comment lines): 10 | 11 | # clone the repo 12 | git clone https://cowboy@github.com/cowboy/gaucho.git 13 | 14 | # install bundler 15 | gem install bundler 16 | 17 | # build gem and install all dependencies 18 | cd gaucho 19 | rake install 20 | 21 | # build test content repo 22 | cd sample_app 23 | ruby create_test_repo.rb 24 | 25 | # install sample app gems 26 | gem install sinatra diffy 27 | 28 | # run sample app 29 | ruby app.rb 30 | 31 | And then visit this page: 32 | 33 | ## Copyright 34 | 35 | Copyright (c) 2011 "Cowboy" Ben Alman 36 | Dual licensed under the MIT and GPL licenses. 37 | 38 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 "Cowboy" Ben Alman 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /lib/gaucho/diff.rb: -------------------------------------------------------------------------------- 1 | # A wrapper for Grit::Diff 2 | module Gaucho 3 | class Diff 4 | include StringUtils 5 | 6 | attr_reader :commit, :diff 7 | 8 | def initialize(commit, diff) 9 | @commit = commit 10 | @diff = diff 11 | end 12 | 13 | def to_s 14 | %Q{#} 15 | end 16 | 17 | # URL for this Diff's file. 18 | def url 19 | %Q{#{commit.url}/#{file}} 20 | end 21 | 22 | # Filename for this Diff. 23 | def file 24 | diff.a_path[%r{^#{commit.page.page_path}/(.*)}, 1] 25 | end 26 | 27 | # What happened (in a very general sense)? 28 | def status 29 | created? ? 'created' : deleted? ? 'deleted' : 'updated' 30 | end 31 | 32 | # Prettier method names for accessing the underlying Grit::Diff instance 33 | # methods. 34 | def created?; diff.new_file; end 35 | def deleted?; diff.deleted_file; end 36 | def updated?; !created? && !deleted?; end 37 | def binary?; diff.diff.start_with? 'Binary files'; end 38 | 39 | # The Grit::Diff diff text, with its encoding "fixed." 40 | def data 41 | fix_encoding(diff.diff) 42 | end 43 | 44 | # Test whether or not the specified Grit::Diff is relevant to the 45 | # specified Gaucho::Page. 46 | def self.is_diff_relevant(diff, page) 47 | diff.a_path.start_with? page.page_path 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /gaucho.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path('../lib', __FILE__) 2 | require 'gaucho/version' 3 | 4 | Gem::Specification.new do |s| 5 | # Note to self: 6 | # http://docs.rubygems.org/read/chapter/20 7 | # http://rubygems.rubyforge.org/rubygems-update/Gem/Specification.html 8 | 9 | s.name = 'gaucho' 10 | s.rubyforge_project = 'gaucho' 11 | s.version = Gaucho::VERSION 12 | s.authors = ['Ben Alman'] 13 | s.email = 'cowboy@rj3.net' 14 | s.homepage = 'http://github.com/cowboy/gaucho' 15 | s.license = %w{MIT GPL-2} 16 | s.summary = %Q{Ruby + Git + Content = Gaucho} 17 | s.description = %Q{Explain what "Ruby + Git + Content = Gaucho" means} 18 | 19 | s.required_ruby_version = '>= 1.9.2' 20 | 21 | s.add_dependency 'grit', '>= 2.4.1' 22 | s.add_dependency 'rdiscount', '>= 1.6.5' 23 | s.add_dependency 'unicode_utils', '>= 1.0.0' 24 | s.add_dependency 'pygmentize', '0.0.2' 25 | 26 | s.add_development_dependency 'shoulda', '>= 0' 27 | s.add_development_dependency 'bundler', '~> 1.0.0' 28 | s.add_development_dependency 'rcov', '>= 0' 29 | 30 | s.rdoc_options = ['--charset=UTF-8'] 31 | s.extra_rdoc_files = %w{README.md LICENSE-MIT LICENSE-GPL} 32 | 33 | s.files = `git ls-files`.split("\n") 34 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 35 | s.require_paths = %w{lib} 36 | end 37 | -------------------------------------------------------------------------------- /sample_app/views/index.haml: -------------------------------------------------------------------------------- 1 | %h1= @title 2 | 3 | %h2 Projects 4 | - $projects.each do |title, pages| 5 | %h3= title 6 | %ul.projects 7 | - pages.each do |p| 8 | - t = p.title.sub(/#{title}[ :]*/i, '') 9 | - t = t[/[A-Z]/] ? t : "#{t[0].upcase}#{t[1..-1]}" 10 | %li{ :'data-github' => p.github } 11 | %a{ href: p.url }= t 12 | - if p.has_fs_mods? 13 | %i 14 | %b MODIFIED 15 | %i= ' - ' + p.subtitle 16 | %div.meta 17 | %em Last updated on #{date_format(p.date)} 18 | %div.reveal= p.render(:excerpt) 19 | 20 | Recent content: 21 | %ul#all_content 22 | - @pages.each do |p| 23 | %li 24 | %a{ href: p.url }= p.title 25 | - if p.has_fs_mods? 26 | %i 27 | %b MODIFIED 28 | %div= p.subtitle 29 | -#%div= p.commit.id 30 | %div 31 | categories: 32 | - p.categories.each do |cat| 33 | %a{ href: cat_url(cat) }= cat 34 | -#%div 35 | -# tagged: 36 | -# - p.tags.each do |tag| 37 | -# %a{ href: tag_url(tag) }= tag 38 | -#%div 39 | -# %em 40 | -# = p.commits.length == 1 ? 'Created on' : 'Last updated on' 41 | -# = date_format(p.date) 42 | 43 | All categories: 44 | %ul#all_categories 45 | - @cats.each do |cat| 46 | %li 47 | %a{ href: cat_url(cat) }= cat 48 | 49 | All tags: 50 | %ul#all_tags.tag-cloud 51 | - @tags.each do |tag| 52 | %li 53 | %a{ style: "font-size: #{tag.scale}%", href: tag_url(tag.tag) }= tag.tag 54 | -------------------------------------------------------------------------------- /sample_app/views/page.haml: -------------------------------------------------------------------------------- 1 | %h1= @title 2 | - if @page.subtitle 3 | %p.subtitle= @page.subtitle 4 | 5 | %p.last_updated Last updated on #{date_format(@page.date)} 6 | 7 | - if @page.has_fs_mods? 8 | %p 9 | %em 10 | Note: 11 | - if @page.shown_fs_mods? 12 | Showing local modifications. 13 | - if @page.committed? 14 | View the 15 | %a{ href: @page.latest_actual_commit.url } most recent commit 16 | instead. 17 | - else 18 | This page hasn't been committed yet. 19 | - else 20 | This page has 21 | = succeed '.' do 22 | %a{ href: @page.url } local modifications 23 | 24 | - elsif !@commit.latest? 25 | %p 26 | %em 27 | The content you are viewing is out of date. View the 28 | %a{ href: @page.url } most recent revision. 29 | -#was updated on 30 | -#%a{ href: "#commit-#{c.id}" } #{date_format(c.date)}. 31 | 32 | ~ @content 33 | 34 | %h3 Tags 35 | %ul#tags 36 | - @page.tags.each do |tag| 37 | %li 38 | %a{ href: tag_url(tag) }= tag 39 | 40 | %h3 Categories 41 | %ul#categories 42 | - @page.categories.each do |cat| 43 | %li 44 | %a{ href: cat_url(cat) }= cat 45 | 46 | %h3 Revisions 47 | %ul#revisions 48 | - @commits.reverse.each do |c| 49 | %li{ class: c.shown? && 'shown', id: "commit-#{c.id}" } 50 | %a{ href: c.url } 51 | %span.date= date_format(c.date) 52 | \- 53 | %span.message= c.message 54 | = surround '(', ')' do 55 | - if c.author.email 56 | %a{ href: "mailto:#{c.author.email}" }= c.author.name 57 | - else 58 | = c.author.name 59 | -#%span.author= c.committer 60 | - if true 61 | %ul.diffs 62 | - c.diffs.each do |d| 63 | - link = capture_haml do 64 | %a{ href: d.url } #{d.file} 65 | %li{ class: d.status } 66 | %span.title #{link} #{d.status} 67 | ~ render_diff(d) unless d.deleted? 68 | -------------------------------------------------------------------------------- /lib/gaucho/util.rb: -------------------------------------------------------------------------------- 1 | module Gaucho 2 | # Standardize on 7-character SHAs across-the-board. Change this if you 3 | # want different length short SHAs. 4 | module ShortSha 5 | def short_sha(sha = id) 6 | sha[0..6] 7 | end 8 | end 9 | 10 | module StringUtils 11 | # Attempt to fix string encoding using this simple (and possibly horribly 12 | # flawed logic): If a UTF-8 string has invalid encoding, it's binary data. 13 | # Otherwise, it's valid UTF-8. 14 | def fix_encoding(str) 15 | copy = str.dup.force_encoding('UTF-8') 16 | if copy.valid_encoding? 17 | copy 18 | else 19 | copy.force_encoding('ASCII-8BIT') 20 | end 21 | end 22 | 23 | # Ensure that data is not binary or invalidly encoded. 24 | def valid_data?(str) 25 | str.encoding.name != 'ASCII-8BIT' && str.valid_encoding? 26 | end 27 | 28 | # Transliterate a Unicode string to its non-fancy, non-unicode counterpart. 29 | def transliterate(str) 30 | UnicodeUtils.nfkd(str).gsub(/[^\x00-\x7F]/, '').to_s 31 | end 32 | 33 | # Attempt to smartly eval a string 34 | def evalify(str = '') 35 | defined = eval("defined?(#{str})") 36 | #puts "#{str} -> defined: #{defined || 'nil'}" 37 | if defined.nil? || defined =~ /^(?:expression|true|false|nil)$/ 38 | eval(str) 39 | else 40 | str 41 | end 42 | rescue NameError 43 | str 44 | rescue SyntaxError 45 | begin 46 | eval("{#{str}}") 47 | rescue SyntaxError 48 | str 49 | end 50 | end 51 | 52 | # Unindent code blocks (like in heredocs). 53 | def unindent(str) 54 | min = str.lines.map {|line| line =~ /^(\s*)\S/; $1 && $1.length }.compact.min 55 | str.lines.map {|line| line.sub(/^\s{#{min}}/, '')}.join 56 | end 57 | end 58 | 59 | # More friendly looking dot-syntax access for hash keys. 60 | # http://mjijackson.com/2010/02/flexible-ruby-config-objects 61 | class Config 62 | def initialize(data = {}) 63 | @data = {} 64 | update!(data) 65 | end 66 | 67 | def update!(data) 68 | data.each {|key, value| self[key.downcase] = value} 69 | end 70 | 71 | def [](key) 72 | @data[key.downcase.to_sym] 73 | end 74 | 75 | def []=(key, value) 76 | @data[key.downcase.to_sym] = if value.class == Hash 77 | Config.new(value) 78 | else 79 | value 80 | end 81 | end 82 | 83 | def to_hash 84 | @data 85 | end 86 | 87 | def method_missing(name, *args) 88 | if name.to_s =~ /(.+)=$/ 89 | self[$1] = args.first 90 | else 91 | self[name] 92 | end 93 | end 94 | 95 | def respond_to?(name) 96 | @data.has_key?(name) 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /sample_app/public/js/site.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | // Project listing hovers. 3 | var z = 1; 4 | 5 | $(document).delegate('ul.projects > li > a', 'hover', function( e ) { 6 | var hover = e.type == 'mouseenter', 7 | li = $(this).parent(), 8 | reveal = li.find('.reveal'); 9 | 10 | // By always increasing the z-index of the currently hovered element, 11 | // odd overlapping issues on hover out are eliminated. 12 | li.css('z-index', ++z).toggleClass('hover', hover); 13 | 14 | // Because -webkit-transition won't animate from 0 to auto, a numeric 15 | // height has to be used! 16 | reveal.css('height', hover ? reveal.css('height', 'auto').height() : 0); 17 | }); 18 | 19 | // Project listing Github integration. 20 | // TODO: use localStorage and a TTL. 21 | $(function() { 22 | var cache = {}, 23 | queues = {}; 24 | 25 | $('ul.projects li[data-github]').each(function() { 26 | var li = $(this), 27 | meta = li.find('.meta'), 28 | parts = li.data('github').split('/'), 29 | user = parts[0], 30 | repo = parts[1], 31 | userObj = cache[user]; 32 | 33 | if ( userObj && userObj[repo] ) { 34 | // This user & repo combo already exists in cache, so just render. 35 | render(user, repo); 36 | 37 | } else { 38 | // User & repo combo doesn't exist in cache. 39 | if ( !userObj ) { 40 | // User doesn't exist in cache, so create user as well as a per-user 41 | // queue. 42 | cache[user] = {}; 43 | queues[user] = []; 44 | 45 | // Fetch user repo(s). 46 | $.getJSON('http://github.com/api/v2/json/repos/show/' + user + '?callback=?', function( d ) { 47 | var repos = d && d.repositories; 48 | if ( !repos ) { return; } 49 | 50 | // Add user's repos into the cache. 51 | $.each(repos, function( i, r ) { 52 | var user = r.owner, 53 | userObj = cache[user] || (cache[user] = {}); 54 | //console.log('found %s/%s', user, r.name); 55 | userObj[r.name] = r; 56 | }); 57 | 58 | // Execute any queued methods. 59 | $.each(queues[user], function( i, fn ) { 60 | fn(); 61 | }); 62 | }); 63 | } 64 | 65 | //console.log('enqueue %s/%s', user, repo); 66 | queues[user].push(function() { 67 | render(user, repo); 68 | }); 69 | } 70 | 71 | // Draw stuff into the page and stuff. 72 | function render( user, repo ) { 73 | //console.log('render %s/%s', user, repo); 74 | var userObj = cache[user], 75 | r = userObj && userObj[repo]; 76 | 77 | if ( !r ) { return; } 78 | 79 | $('') 80 | .html([ 81 | githubLink('GitHub'), 82 | githubLink('watcher', 'watchers', r.watchers), 83 | githubLink('fork', 'network', r.forks), 84 | ].join(' ')) 85 | .appendTo(meta); 86 | 87 | function githubLink( txt, link, num ) { 88 | link = link ? '/' + link : ''; 89 | txt = num == undefined ? txt : num + ' ' + txt + (num == 1 ? '' : 's'); 90 | return '' + txt + ''; 91 | } 92 | } 93 | 94 | }); 95 | }); 96 | })(jQuery); 97 | -------------------------------------------------------------------------------- /sample_app/views/css/_twilight.scss: -------------------------------------------------------------------------------- 1 | /* Adapted from https://gist.github.com/803005 */ 2 | .sh { 3 | /* background: #181818; padding: 16px; color: #F8F8F8; font-family: Consolas, Monaco,"Lucida Console"; */ 4 | &, * { font-family: Consolas, Monaco,"Lucida Console"; } 5 | .hll { background-color: #ffffcc } 6 | .c { color: #5F5A60; font-style: italic } /* Comment */ 7 | .err { border:#B22518; } /* Error */ 8 | .k { color: #CDA869 } /* Keyword */ 9 | .cm { color: #5F5A60; font-style: italic } /* Comment.Multiline */ 10 | .cp { color: #5F5A60 } /* Comment.Preproc */ 11 | .c1 { color: #5F5A60; font-style: italic } /* Comment.Single */ 12 | .cs { color: #5F5A60; font-style: italic } /* Comment.Special */ 13 | .gd { background: #420E09 } /* Generic.Deleted */ 14 | .ge { font-style: italic } /* Generic.Emph */ 15 | .gr { background: #B22518 } /* Generic.Error */ 16 | .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 17 | .gi { background: #253B22 } /* Generic.Inserted */ 18 | .go { } /* Generic.Output */ 19 | .gp { font-weight: bold } /* Generic.Prompt */ 20 | .gs { font-weight: bold } /* Generic.Strong */ 21 | .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 22 | .gt { } /* Generic.Traceback */ 23 | .kc { } /* Keyword.Constant */ 24 | .kd { color: #e9df8f; } /* Keyword.Declaration */ 25 | .kn { } /* Keyword.Namespace */ 26 | .kp { color: #9B703F } /* Keyword.Pseudo */ 27 | .kr { } /* Keyword.Reserved */ 28 | .kt { } /* Keyword.Type */ 29 | .m { } /* Literal.Number */ 30 | .s { } /* Literal.String */ 31 | .na { color: #F9EE98 } /* Name.Attribute */ 32 | .nb { color: #CDA869 } /* Name.Builtin */ 33 | .nc { color: #9B859D; font-weight: bold } /* Name.Class */ 34 | .no { color: #9B859D } /* Name.Constant */ 35 | .nd { color: #7587A6 } /* Name.Decorator */ 36 | .ni { color: #CF6A4C; font-weight: bold } /* Name.Entity */ 37 | .nf { color: #9B703F; font-weight: bold } /* Name.Function */ 38 | .nn { color: #9B859D; font-weight: bold } /* Name.Namespace */ 39 | .nt { color: #CDA869; font-weight: bold } /* Name.Tag */ 40 | .nv { color: #7587A6 } /* Name.Variable */ 41 | .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ 42 | .w { color: #141414 } /* Text.Whitespace */ 43 | .mf { color: #CF6A4C } /* Literal.Number.Float */ 44 | .mh { color: #CF6A4C } /* Literal.Number.Hex */ 45 | .mi { color: #CF6A4C } /* Literal.Number.Integer */ 46 | .mo { color: #CF6A4C } /* Literal.Number.Oct */ 47 | .sb { color: #8F9D6A } /* Literal.String.Backtick */ 48 | .sc { color: #8F9D6A } /* Literal.String.Char */ 49 | .sd { color: #8F9D6A; font-style: italic; } /* Literal.String.Doc */ 50 | .s2 { color: #8F9D6A } /* Literal.String.Double */ 51 | .se { color: #F9EE98; font-weight: bold; } /* Literal.String.Escape */ 52 | .sh { color: #8F9D6A } /* Literal.String.Heredoc */ 53 | .si { color: #DAEFA3; font-weight: bold; } /* Literal.String.Interpol */ 54 | .sx { color: #8F9D6A } /* Literal.String.Other */ 55 | .sr { color: #E9C062 } /* Literal.String.Regex */ 56 | .s1 { color: #8F9D6A } /* Literal.String.Single */ 57 | .ss { color: #CF6A4C } /* Literal.String.Symbol */ 58 | .bp { color: #00aaaa } /* Name.Builtin.Pseudo */ 59 | .vc { color: #7587A6 } /* Name.Variable.Class */ 60 | .vg { color: #7587A6 } /* Name.Variable.Global */ 61 | .vi { color: #7587A6 } /* Name.Variable.Instance */ 62 | .il { color: #009999 } /* Literal.Number.Integer.Long */ 63 | } -------------------------------------------------------------------------------- /lib/gaucho/commit.rb: -------------------------------------------------------------------------------- 1 | # A wrapper for Grit::Commit 2 | module Gaucho 3 | class Commit 4 | include ShortSha 5 | include StringUtils 6 | extend Forwardable 7 | 8 | attr_reader :page, :pageset 9 | 10 | # Forward Commit methods to @commit (via the commit method) so that this 11 | # class feels as Grit::Commit-like as possible. 12 | def_delegators :commit, *Grit::Commit.public_instance_methods(false) 13 | 14 | def initialize(page, commit_id = nil) 15 | @page = page 16 | @pageset = page.pageset 17 | @commit_id = commit_id 18 | end 19 | 20 | def to_s 21 | s = shown? ? '*' : '' 22 | %Q{#} 23 | end 24 | 25 | # Use a shortened SHA for the id, fabricating one if necessary. 26 | def id 27 | if simulated? 28 | 'simulated' 29 | else 30 | short_sha(@commit_id) 31 | end 32 | end 33 | 34 | # URL for the page at this Commit. If the commit is simulated, omit the id 35 | # from the URL. 36 | def url 37 | if simulated? 38 | page.url 39 | else 40 | %Q{/#{id}#{page.url}} 41 | end 42 | end 43 | 44 | # If a commit_id wasn't passed, this commit is simulated. This should only 45 | # be used when a new Page, with no commits, is being previewd from the 46 | # filesystem, using check_fs_mods. 47 | def simulated? 48 | @commit_id.nil? 49 | end 50 | 51 | # Is this commit the most recent actual (not simulated) commit for the Page? 52 | def latest? 53 | self == page.latest_actual_commit 54 | end 55 | 56 | # Is this commit the currently shown commit for the Page? 57 | def shown? 58 | self == page.commit 59 | end 60 | 61 | # Metadata for the Page at this Commit, parsed from "file" (Grit::Blob) 62 | # named "index.___". If the commit is simulated, create an empty metadata 63 | # object so that things don't break. 64 | def meta 65 | @meta ||= if simulated? 66 | page.meta #Gaucho::Config.new 67 | else 68 | index = tree.blobs.find {|blob| blob.name =~ /^index\./} 69 | page.class.build_metadata(index) 70 | end 71 | end 72 | 73 | # Contents of "file" (Grit::Blob) at the specified path under the Page at 74 | # this Commit. 75 | def /(file) 76 | files[file] or raise Gaucho::FileNotFound.new(file) 77 | end 78 | 79 | # The author of this Commit. A specified metadata "author" will be used 80 | # first, with a fallback to the actual Grit::Commit committer (Grit::Actor). 81 | def author 82 | if meta.author.nil? 83 | commit.committer 84 | else 85 | Grit::Actor.from_string(meta.author) 86 | end 87 | end 88 | 89 | # The date of this Commit. A specified metadata "date" will be used first, 90 | # with a fallback to the actual Grit::Commit committed_date. 91 | def date(fallback = commit.committed_date) 92 | if meta.date 93 | Time.parse(meta.date) 94 | else 95 | fallback 96 | end 97 | end 98 | 99 | # The underlying Grit::Commit instance for this Commit. If this commit is 100 | # simulated, create a completely fabricated Grit::Commit instance. 101 | def commit 102 | @commit ||= if simulated? 103 | sha = 'f' * 40 104 | actor = Grit::Actor.from_string('John Q. Author') 105 | time = date(page.files_last_modified) 106 | Grit::Commit.new(pageset.repo, sha, [sha], sha, actor, time, actor, time, 107 | %w{This commit is simulated!}) 108 | else 109 | pageset.repo.commit(@commit_id) 110 | end 111 | end 112 | 113 | # The Grit::Commit message, with its encoding "fixed." 114 | def message 115 | fix_encoding(commit.message) 116 | end 117 | 118 | # The Grit::Tree instance representing the Page at this Commit. 119 | def tree 120 | @tree ||= if pageset.subdir 121 | commit.tree/pageset.subdir/page.id 122 | else 123 | commit.tree/page.id 124 | end 125 | end 126 | 127 | # All the diffs for this Commit relevant to the Page. 128 | def diffs 129 | @diffs ||= build_diffs 130 | end 131 | 132 | # Hash of all "file" (Grit::Blob) objects under the Page at this Commit. 133 | def files 134 | @files ||= build_file_index 135 | end 136 | 137 | protected 138 | 139 | # Build an array of Gaucho::Diff instances that are relevant to the 140 | # Page. at this Commit. 141 | def build_diffs 142 | diffs = commit.show.collect do |diff| 143 | Gaucho::Diff.new(self, diff) if Gaucho::Diff.is_diff_relevant(diff, page) 144 | end 145 | diffs.compact 146 | end 147 | 148 | # Build "file" (Grit::Blob) index for the Page at this Commit. 149 | def build_file_index 150 | files = {} 151 | 152 | # Parse the raw output from git ls-tree. 153 | text = pageset.repo.git.native(:ls_tree, {:r => true, :t => true}, @commit_id, page.page_path) 154 | text.split("\n").each do |line| 155 | thing = pageset.tree.content_from_string(pageset.repo, line) 156 | if thing.kind_of?(Grit::Blob) && !File.basename(thing.name).start_with?('.') 157 | if thing.name =~ %r{^#{page.page_path}/(.*)} 158 | files[$1] = fix_encoding(thing.data) 159 | end 160 | end 161 | end 162 | 163 | files 164 | end 165 | end 166 | end 167 | -------------------------------------------------------------------------------- /lib/gaucho/pageset.rb: -------------------------------------------------------------------------------- 1 | # A wrapper for Grit::Repo 2 | module Gaucho 3 | # TODO: BETTER ERRORS 4 | class PageSet 5 | include Enumerable 6 | extend Forwardable 7 | 8 | attr_reader :repo, :tree, :subdir, :renames 9 | attr_accessor :check_fs_mods 10 | 11 | # Forward Array methods to @pages (via the pages method) so that the PageSet 12 | # can feel as Array-like as possible. 13 | def_delegators :pages, *Array.public_instance_methods(false) 14 | 15 | def initialize(repo, options = {}) 16 | @repo = if repo.class == Grit::Repo 17 | repo 18 | else 19 | Grit::Repo.new(repo) 20 | end 21 | 22 | # Initialize from options, overriding these defaults. 23 | { check_fs_mods: false, 24 | renames: {}, 25 | subdir: nil 26 | }.merge(options).each do |key, value| 27 | instance_variable_set("@#{key}".to_sym, value) 28 | end 29 | 30 | rebuild! 31 | end 32 | 33 | # Rebuild all internal commit / Page caches. Use this to force Gaucho to 34 | # show changes if the repo's commits / HEAD have been changed, or if the 35 | # check_fs_mods option has been changed. 36 | def rebuild! 37 | @tree = if subdir.nil? 38 | repo.tree 39 | else 40 | repo.tree/subdir 41 | end 42 | 43 | @pages = @pages_by_id = nil 44 | 45 | build_page_map! 46 | build_commit_index! 47 | end 48 | 49 | def to_s 50 | %Q{#} 51 | end 52 | 53 | # Expose the underlying pages array. 54 | def pages 55 | build_page! 56 | @pages 57 | end 58 | 59 | # Get a specific page. This will create a new Page instance internally if 60 | # one doesn't already exist. If the page has been renamed via the renames 61 | # options hash, return the new URL (for redirecting). 62 | def [](page_id) 63 | # TODO: compute this in build_page! (?) 64 | page_id = page_id.sub(%r{(.*)/}, '\1_').gsub(%r{/}, ?-) 65 | 66 | build_page!(page_id) 67 | page = @pages_by_id[page_id] 68 | 69 | if page.nil? 70 | nil 71 | elsif page.path == page_id 72 | page 73 | else 74 | page.url 75 | end 76 | end 77 | 78 | # Get all pages. This will create new Page instances internally for any that 79 | # don't already exist. This could take a while. 80 | def each 81 | if block_given? then 82 | pages.each {|page| yield page} 83 | else 84 | to_enum(:each) 85 | end 86 | end 87 | 88 | # Reset all Pages "shown" to their latest commit. 89 | def reset_shown 90 | each {|page| page.shown = nil} 91 | end 92 | 93 | # Relative (to repo root) filesystem path for all Pages in this PageSet. 94 | def subdir_path 95 | if @subdir.nil? 96 | '' 97 | else 98 | File.join(@subdir, '') 99 | end 100 | end 101 | 102 | # Absolute filesystem path for all Pages in this PageSet. 103 | def abs_subdir_path 104 | if subdir 105 | File.join(repo.working_dir, subdir) 106 | else 107 | repo.working_dir 108 | end 109 | end 110 | 111 | protected 112 | 113 | # Build commit index for this repo. 114 | def build_commit_index! 115 | @commits_by_page = Hash.new {|h,k| h[k] = []} 116 | current_id = nil 117 | 118 | log = repo.git.native(:log, {pretty: 'oneline', name_only: true, 119 | reverse: true, timeout: false}) 120 | 121 | log.split("\n").each do |line| 122 | if line =~ /^([0-9a-f]{40})/ 123 | current_id = $1 124 | else 125 | if line =~ %r{^#{subdir_path}(.*?)/} 126 | @commits_by_page[$1] ||= [] 127 | @commits_by_page[$1] << current_id 128 | end 129 | end 130 | end 131 | 132 | @commits_by_page.each {|p, commits| commits.uniq!} 133 | end 134 | 135 | # Generate a map of renamed Page id (path) to original Page id. If the 136 | # check_fs_mods option is enabled, get the listing from the filesystem, 137 | # otherwise build the map from git. 138 | def build_page_map! 139 | @page_paths = {} 140 | 141 | if check_fs_mods 142 | Dir.entries(abs_subdir_path).each do |file| 143 | path = "#{abs_subdir_path}/#{file}" 144 | if FileTest.directory?(path) && !File.basename(path).start_with?('.') 145 | @page_paths[file] = file 146 | end 147 | end 148 | else 149 | @tree.trees.each {|tree| @page_paths[tree.name] = tree.name} 150 | end 151 | 152 | # Process user-specified Page renames. 153 | renames.each do |page_id, path| 154 | @page_paths[path] = page_id if @page_paths[page_id] 155 | end 156 | end 157 | 158 | # Build page index for this repo. If nil is passed, build all pages, 159 | # otherwise build the specified page. 160 | def build_page!(page_id = nil) 161 | page_id = if page_id 162 | @page_paths[page_id] 163 | else 164 | @page_paths.map {|path, id| id}.uniq 165 | end 166 | 167 | return unless page_id 168 | 169 | @pages_by_id ||= {} 170 | 171 | [*page_id].each do |id| 172 | unless @pages_by_id[id] 173 | path = renames[id] || id 174 | @pages_by_id[id] = @pages_by_id[path] = Gaucho::Page.new(self, id, path, @commits_by_page[id]) 175 | end 176 | end 177 | 178 | @pages = [] 179 | @pages_by_id.each {|p_id, page| @pages << page} 180 | @pages = @pages.uniq.sort 181 | end 182 | end 183 | end 184 | -------------------------------------------------------------------------------- /sample_app/views/css/site.sass: -------------------------------------------------------------------------------- 1 | @import "compass" 2 | 3 | // Main styles. 4 | 5 | #content 6 | width: 40em 7 | color: #333 8 | font-family: Georgia 9 | img 10 | border: 1px solid #333 11 | .noborder 12 | border: none 13 | box-shadow: none 14 | 15 | #content img, .sh-code 16 | box-shadow: 0px 2px 4px #aaa 17 | 18 | h1, h2, h3, h4 19 | font-family: "Gill Sans" 20 | font-weight: 400 21 | h1 22 | font-size: 250% 23 | margin-bottom: 0 24 | h2 25 | font-size: 180% 26 | h3 27 | font-size: 150% 28 | h4 29 | font-size: 120% 30 | 31 | p 32 | line-height: 1.4em 33 | &.subtitle 34 | font-style: italic 35 | font-size: 160% 36 | color: #777 37 | margin: 0 38 | &.last_updated 39 | color: #aaa 40 | font-size: 80% 41 | 42 | a 43 | color: #ff7700 44 | -webkit-transition: color 0.1s linear 45 | &:hover 46 | color: #aa3300 47 | code 48 | text-decoration: underline 49 | 50 | img 51 | vertical-align: top 52 | 53 | code, pre, .sh-link 54 | font-family: "Consolas", "Courier New", Courier, monospace 55 | font-size: 0.9em 56 | code 57 | background: #eee 58 | border: 1px solid #aaa 59 | color: #555 60 | padding: 4px 0.2em 1px 61 | -webkit-border-radius: 4px 62 | 63 | // Misc. 64 | 65 | .tag-cloud li 66 | display: inline 67 | font-style: none 68 | 69 | // Project listing. 70 | 71 | ul.projects 72 | &, li 73 | list-style: none 74 | margin: 0 75 | padding: 0 76 | li 77 | position: relative 78 | z-index: 0 79 | left: -4px 80 | right: -4px 81 | padding: 2px 4px 4px 82 | border-top-left-radius: 10px 83 | border-top-right-radius: 10px 84 | &, * 85 | -webkit-transition: 0.1s ease-out 86 | -webkit-transition-delay: 0.1s 87 | -webkit-transition-property: height, color, background, border, box-shadow 88 | &, .reveal 89 | border: 1px solid rgba(0, 0, 0, 0) 90 | .meta 91 | color: #aaa 92 | font-size: 70% 93 | margin-top: 3px 94 | .github 95 | position: relative 96 | z-index: 1 97 | > * 98 | margin-left: 0.3em 99 | a 100 | color: #aaa 101 | border: 1px solid #aaa 102 | padding: 1px 0.3em 103 | -webkit-border-radius: 4px 104 | text-decoration: none 105 | &:hover 106 | color: #aa3300 107 | border-color: #aa3300 108 | background: #fed 109 | .reveal 110 | background: #fff 111 | font-size: 80% 112 | padding: 3px 4px 0 113 | position: absolute 114 | left: -1px 115 | right: -1px 116 | height: 0 117 | overflow: hidden 118 | border-bottom-left-radius: 10px 119 | border-bottom-right-radius: 10px 120 | a 121 | color: inherit 122 | text-decoration: none 123 | > * 124 | margin: 0.2em 0 0.4em 0 125 | &.hover 126 | z-index: 1 127 | .meta 128 | color: #AA9A89 129 | .github a 130 | background: none 131 | border: 1px solid #AA9A89 132 | color: #AA9A89 133 | .reveal 134 | border-color: #000 135 | border-top: none !important 136 | &, .reveal 137 | background: #fed 138 | border: 1px solid #000 139 | box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.4) 140 | 141 | // Diffs. 142 | 143 | .diff 144 | overflow: auto 145 | ul 146 | color: #000 147 | background: #fff 148 | overflow: auto 149 | font-size: 13px 150 | list-style: none 151 | margin: 0 152 | padding: 0 153 | display: table 154 | width: 100% 155 | del, ins 156 | display: block 157 | text-decoration: none 158 | li 159 | padding: 0 160 | display: table-row 161 | margin: 0 162 | height: 1em 163 | &.ins 164 | background: #dfd 165 | &.del 166 | background: #fdd 167 | &:hover 168 | background: #ffc 169 | del, ins, span 170 | // Try 'whitespace: pre' if you don't want lines to wrap 171 | white-space: pre-wrap 172 | font-family: courier 173 | del strong 174 | font-weight: normal 175 | background: #faa 176 | ins strong 177 | font-weight: normal 178 | background: #afa 179 | 180 | 181 | // Syntax highlighting. 182 | 183 | @import twilight 184 | 185 | .sh 186 | width: 42em 187 | color: #F8F8F8 188 | .sh-code 189 | overflow: hidden 190 | background: #181818 191 | border: 2px solid #181818 192 | -webkit-border-radius: 12px 193 | -webkit-border-top-right-radius: 0 194 | .highlighttable 195 | margin: 0 196 | padding: 0 197 | border-collapse: collapse 198 | td 199 | margin: 0 200 | padding: 0 201 | vertical-align: top 202 | .highlight 203 | width: 39.4em 204 | overflow: auto 205 | padding: 6px 6px 3px 206 | a 207 | position: absolute 208 | // Vertical offset for fragment linking. 209 | margin-top: -2em 210 | .linenos 211 | color: #aaa 212 | background: #444 213 | border-right: 2px solid #181818 214 | -webkit-border-top-left-radius: 10px 215 | -webkit-border-bottom-left-radius: 10px 216 | .linenodiv 217 | padding: 6px 3px 3px 5px 218 | a 219 | color: #aaa 220 | text-decoration: none 221 | &:hover 222 | color: #fff 223 | text-decoration: underline 224 | pre 225 | margin: 0 226 | line-height: 125% 227 | 228 | .sh-link 229 | text-align: right 230 | a 231 | text-decoration: none 232 | color: #aaa 233 | background: #181818 234 | padding: 0.5em 0.5em 0.2em 0.6em 235 | -webkit-border-top-left-radius: 10px 236 | -webkit-border-top-right-radius: 10px 237 | &:hover 238 | text-decoration: underline 239 | color: #fff 240 | 241 | // No line numbers. 242 | 243 | .sh-nolines .highlight 244 | background: #181818 245 | padding: 4px 246 | pre 247 | margin: 0 248 | white-space: pre-wrap 249 | 250 | // WebKit Scrollbar. 251 | // Adapted from http://beautifulpixels.com/goodies/create-custom-webkit-scrollbar/ 252 | 253 | body ::-webkit-scrollbar 254 | height: 12px 255 | ::-webkit-scrollbar-button:start, ::-webkit-scrollbar-button:end 256 | display: none 257 | ::-webkit-scrollbar-track-piece, ::-webkit-scrollbar-thumb 258 | -webkit-border-radius: 8px 259 | ::-webkit-scrollbar-track-piece 260 | background-color: #444 261 | ::-webkit-scrollbar-thumb:horizontal 262 | width: 50px 263 | background-color: #777 264 | ::-webkit-scrollbar-thumb:hover 265 | background-color: #aaa 266 | -------------------------------------------------------------------------------- /lib/gaucho/page.rb: -------------------------------------------------------------------------------- 1 | module Gaucho 2 | class Page 3 | include ShortSha 4 | include StringUtils 5 | 6 | attr_reader :pageset, :id, :path, :commit, :shown, :files_last_modified 7 | 8 | def initialize(pageset, id, path, commit_ids) 9 | @pageset = pageset 10 | @id = id 11 | @path = path 12 | @commit_ids = commit_ids 13 | self.shown = nil 14 | rescue 15 | raise Gaucho::PageNotFound 16 | end 17 | 18 | def to_s 19 | %Q{#} 20 | end 21 | 22 | # Canonical URL for This Page. Replace any dashes and underscore in 23 | # leading date (YYYY_ or YYYY-MM_ or YYYY-MM-DD_) with slashes. 24 | def url 25 | parts = id.split ?_ 26 | parts[0].gsub!(/-/, ?/) if parts[1] 27 | "/#{parts.join ?/}" 28 | end 29 | 30 | # Set the current Page to show the specified commit. Specifying nil 31 | # sets the view to the latest commit. 32 | def shown=(commit_id) 33 | @shown = commit_id 34 | 35 | @commit = nil 36 | if commit_id.nil? 37 | @meta = @files = @commits = nil if check_fs_mods? 38 | else 39 | @commit = commits.find {|commit| commit.id.start_with? commit_id} 40 | end 41 | 42 | @commit ||= commits.last 43 | end 44 | 45 | # Get all Commits for this Page. If the Page hasn't yet been committed or 46 | # it has local modifications, append a simulated Commit. 47 | def commits 48 | unless @commits 49 | @commits = @commit_ids.collect {|commit_id| Gaucho::Commit.new(self, commit_id)} 50 | if has_fs_mods? 51 | @commits << Gaucho::Commit.new(self) 52 | end 53 | end 54 | @commits 55 | end 56 | 57 | # The most recent actual (not simulated) commit for this Page. 58 | def latest_actual_commit 59 | commits.reverse.find {|commit| !commit.simulated?} 60 | end 61 | 62 | # Returns true if this Page's id matches the passed date. If no date is 63 | # passed, returns true if this Page's id begins with a date. 64 | def date?(date_arr = nil) 65 | if date_arr 66 | date_arr.split!(/\D+/) if date_arr.class == String 67 | id.start_with?("#{date_arr.join('-')}_") 68 | else 69 | !!%r{^\d{4}(?:-\d{2}){0,2}_}.match(id) 70 | end 71 | end 72 | 73 | # Metadata for the Page at the currently "shown" Commit, or from the index 74 | # file in the filesystem if shown_fs_mods? is true. 75 | def meta 76 | if shown_fs_mods? 77 | unless @meta 78 | index = Gaucho::Config.new 79 | index.name = Dir.entries(abs_page_path).find {|file| file =~ /^index\./} 80 | index.data = IO.read(File.join(abs_page_path, index.name)) 81 | @meta = self.class.build_metadata(index) 82 | end 83 | @meta 84 | else 85 | commit.meta 86 | end 87 | end 88 | 89 | # File listing for the Page at the currently "shown" Commit, or from the 90 | # filesystem if shown_fs_mods? is true. 91 | def files 92 | if shown_fs_mods? 93 | @files 94 | else 95 | commit.files 96 | end 97 | end 98 | 99 | # Because page/'foo.txt' looks cooler than page.files['foo.txt']. 100 | def /(file) 101 | files[file] or raise Gaucho::FileNotFound.new(file) 102 | end 103 | 104 | # Either the last commit's committed date, or the most recent file last 105 | # modified time (or metadata-specified date) if shown_fs_mods? is true. 106 | def date 107 | if shown_fs_mods? 108 | commits.last.date 109 | else 110 | latest_actual_commit.date 111 | end 112 | end 113 | 114 | # Relative (to repo root) filesystem path for this Page. 115 | def page_path 116 | if pageset.subdir_path != '' 117 | File.join(pageset.subdir_path, id) 118 | else 119 | id 120 | end 121 | end 122 | 123 | # Absolute filesystem path for this Page. 124 | def abs_page_path 125 | File.join(pageset.abs_subdir_path, id) 126 | end 127 | 128 | # Has this page been committed yet? 129 | def committed? 130 | !@commit_ids.empty? 131 | end 132 | 133 | # Is the PageSet "check_fs_mods" option set? 134 | def check_fs_mods? 135 | pageset.check_fs_mods 136 | end 137 | 138 | # If check_fs_mods? is true and the shown commit is nil, check to see if the 139 | # local filesystem has modificiations by building a filesystem-based file 140 | # index and comparing it with the file index of the last Commit. 141 | def has_fs_mods? 142 | if check_fs_mods? 143 | build_file_index! 144 | !committed? || @files != latest_actual_commit.files 145 | end 146 | end 147 | 148 | # Are local modifications currently being shown? 149 | def shown_fs_mods? 150 | shown.nil? && has_fs_mods? 151 | end 152 | 153 | # Sort pages by last commit date (most recent first) by default. 154 | def <=>(other) 155 | other.date <=> date 156 | end 157 | 158 | # Pass-through all other methods to the underlying metadata object. 159 | def method_missing(*args) 160 | meta.public_send(*args) 161 | end 162 | 163 | # Parse metadata and content from a Page index file. 164 | def self.build_metadata(index = nil) 165 | raise Gaucho::PageNotFound unless index 166 | 167 | docs = [] 168 | YAML.each_document(index.data) {|doc| docs << doc} rescue nil 169 | docs = [{}, index.data] unless docs.length == 2 170 | docs.first.each do |key, value| 171 | docs.first[key] = value.collect {|e| e.to_s} if value.class == Array 172 | end 173 | meta = Gaucho::Config.new(docs.first) 174 | meta.index_name = index.name 175 | 176 | # meta.excerpt is anything before , meta.content is everything 177 | # before + everything after. 178 | parts = docs.last.split(/^\s*\s*$/im) 179 | meta.excerpt = parts[0] 180 | meta.content = parts.join('') 181 | 182 | meta 183 | end 184 | 185 | protected 186 | 187 | # Build page file index from filesystem. 188 | def build_file_index! 189 | return if @files 190 | 191 | @files = {} 192 | @files_last_modified = nil 193 | 194 | # Iterate over all files, recursively. 195 | Find.find(abs_page_path) do |path| 196 | if !FileTest.directory?(path) && !File.basename(path).start_with?('.') 197 | if path =~ %r{^#{abs_page_path}/(.*)} 198 | @files_last_modified = [@files_last_modified, File.new(path).mtime].compact.max 199 | @files[$1] = fix_encoding(IO.read(path)) 200 | end 201 | end 202 | end 203 | end 204 | end 205 | end -------------------------------------------------------------------------------- /sample_app/create_test_repo.rb: -------------------------------------------------------------------------------- 1 | require 'grit' 2 | require 'pp' 3 | require 'fileutils' 4 | 5 | @repo_path = File.expand_path('test_repo') 6 | FileUtils.rm_rf(@repo_path) 7 | FileUtils.mkdir_p(@repo_path) 8 | FileUtils.cd(@repo_path) 9 | `git init .` 10 | 11 | @articles = ['gallimaufry', 'haptic', 'lacuna', 'sidereal', 'risible', 'turophile', 'Scotch woodcock', 'scion', 'opprobrium', 'paean', 'brobdignagian', 'abecedarian', 'paronomasia', 'woodnote', 'second banana', 'ben trovato', 'putative', 'algid', 'piste', 'synchronicity', 'factotum', 'festschrift', 'jato unit', 'materteral', 'skepsis', 'micawber', 'jitney', 'retral', 'sobriquet', 'tumid', 'pule', 'meed', 'oscitate', 'oolert', 'sartorial', 'vitiate', 'chiliad', 'aestival', 'sylva', 'stat', 'anomie', 'cheval-de-frise', 'pea-souper', 'autochthon', 'jument', 'lascivious', 'aglet', 'bildungsroman', 'comity', 'devil theory', 'embrocation', 'fug', 'gat', 'hidrosis', 'irenic', 'jeremiad', 'kerf', 'legerity', 'marmoreal', 'naff', 'oikology', 'pessimal', 'quidam', 'recondite', 'sybaritic', 'tyro', 'ullage', 'vigorish', 'writhen', 'xanthochroi', 'yestreen', 'zenana', 'gribble', 'pelf', 'aeneous', 'forb', 'eleemosynary', 'foofaraw', 'lanai', 'shandrydan', 'tardigrade', 'ontic', 'lubricious', 'inchmeal', 'costermonger', 'pilgarlic', 'costard', 'quotidian', 'nystagmus', 'bathos', 'dubiety', 'jactation', 'lubritorium', 'cullion', 'wallydrag', 'literatim', 'flaneur', 'cuesta', 'anodyne', 'weazen', 'brumal', 'estaminet', 'incarnadine', 'gork', 'xanthous', 'yoni', 'demersal', 'anthemion', 'clapperclaw', 'kloof', 'pavid', 'wyvern', 'flannelmouthed', 'chondrule', 'petitio principii', 'kyte', 'pawky', 'katzenjammer', 'catchpenny', 'quincunx', 'Rabelaisian', 'cogent', 'abulia', 'roundheel', 'bruxism', 'kempt', 'aeolian', 'chorine', 'infrangible', 'patzer', 'mistigris', 'misoneism', 'discalceate', 'mimesis', 'pleonasm', 'bezoar', 'volacious', 'demiurgic', 'kakistocracy', 'mell', 'psilanthropy', 'pulchritude', 'embrangle', 'exigent', 'clapter', 'Esperanto', 'wamble', 'maven', 'pulvinar', 'digerati', 'exiguous', 'prolegomenon', 'wapper jaw', 'pridian', 'dirl', 'viviparous', 'brickbat', 'colporteur', 'ditty bag', 'denouement', 'miscegenation', 'vavasor', 'xerosis', 'gunda', 'looby', 'nabob', 'planogram', 'zarf', 'xyloid', 'invidious', 'nugatory', 'decrescent', 'palmy', 'frittle', 'risorial', 'agnail', 'demesne', 'asperse', 'crankle', 'dulcorate', 'chirm', 'blague', 'humbug', 'diapason', 'nares', 'palliate', 'narghile', 'flagitious', 'fizgig', 'troilism', 'bandicoot', 'acid test', 'achilous', 'irpe', 'irredenta', 'balter', 'tripsis', 'gormless', 'anfractuous', 'lulliloo'] 12 | @articles = @articles[0..20] #make things smaller and faster 13 | 14 | @all_cats = %w(news projects articles music photography) 15 | @all_tags = %w(fun awesome cool lame bad sweet great money weak zesty) 16 | 17 | @all_texts = [ 18 | '## Sample Header', 19 | '### Sample Sub-Header', 20 | '{{toc}}', 21 | %Q{This text has **bold** and _italic_ text, some "quoted text that can't be beat," and look, [an external link](http://benalman.com) too!}, 22 | 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur at erat id tellus rutrum cursus. Cras et elit est. Duis a eleifend metus. Proin ultrices hendrerit rutrum. Fusce id sapien nec dolor elementum tempus nec pulvinar diam. Sed euismod, sem ut luctus ullamcorper, nibh tellus volutpat felis, eget tempor quam sem a tortor. Nam tortor felis, mollis vitae vehicula non, consequat vel magna.', 23 | 'Mauris suscipit cursus fringilla. Donec ut nisl quam, non blandit odio. Aenean quis est a massa iaculis ultricies. Nulla vel velit magna. Vivamus eget tortor ipsum, ac feugiat augue. Vivamus vel ipsum lorem. Praesent sapien massa, egestas venenatis tempor et, auctor ac libero. Curabitur id lorem eget nisi faucibus placerat. Vestibulum vitae nisl erat, quis elementum enim. Proin ac felis pellentesque ante malesuada pellentesque tempus sit amet tortor.', 24 | "* List item 1\n* List item 2\n* List item 3", 25 | "1. Ordered list item 1\n2. Ordered list item 2\n3. Ordered list item 3", 26 | ] 27 | 28 | @all_incls = [] 29 | @all_incls << ['sample.rb', < 5 32 | puts "yay, the < and > were escaped properly" 33 | end 34 | end 35 | EOF 36 | ] 37 | @all_incls << ['awesome.js', <LOL!! 52 | EOF 53 | ] 54 | @all_incls << ['lolwat.html', <LOL WAT 56 |

SUPER DUPER COOL

57 | EOF 58 | ] 59 | @all_incls << ['haiku.txt', <ZOMG ESCAPED HTML 71 | EOF 72 | ] 73 | 74 | @page_subdirs = ['yay/', 'nay/'] 75 | @file_subdirs = ['', 'foo/', 'bar/', 'foo/bar/'] 76 | 77 | @paths = {} 78 | 79 | def content_name_path(a) 80 | unless @paths[a] 81 | name = "#{a} is a cool word" 82 | subdir = @page_subdirs.shuffle.first 83 | date = if rand(10) > 5 84 | [ 85 | 2008 + rand(2), 86 | "%02d" % (1 + rand(12)), 87 | "%02d" % (1 + rand(28)) 88 | ].join('-') + '-' 89 | else 90 | '' 91 | end 92 | @paths[a] = [ 93 | name, 94 | "#{@repo_path}/#{subdir}#{date}#{name.downcase.gsub(/\s+/, '-')}" 95 | ] 96 | end 97 | @paths[a] 98 | end 99 | 100 | def create_article(a) 101 | name, path = content_name_path(a) 102 | cats = @all_cats.shuffle[0..rand(1)].join(', ') 103 | tags = @all_tags.shuffle[0..rand(4)].join(', ') 104 | index = < 114 | EOF 115 | 116 | FileUtils.mkdir_p(path) 117 | File.open("#{path}/index.md", 'w') {|f| f.write(index)} 118 | end 119 | 120 | def add_stuff(a) 121 | name, path = content_name_path(a) 122 | incl = @all_incls.shuffle[0..1] 123 | incl.each do |i| 124 | file_subdir = @file_subdirs.shuffle.first 125 | index = <} 34 | o.args ? %Q{#{img}} : img 35 | end 36 | 37 | # Soundcloud player. 38 | def self.soundcloud(o) 39 | url = CGI::escape(o.name) 40 | unindent(<<-EOF) 41 | 42 | 43 | 44 | 45 | 47 | 48 | EOF 49 | end 50 | 51 | # Open AppleScript in Script Editor. 52 | self.filter_map[:applescript] = [:applescript] 53 | def self.applescript(o) 54 | script = URI.escape(o.data) 55 | %Q{} + 56 | %Q{Click here to open this AppleScript in Script Editor. #{code(o)}} 57 | end 58 | end 59 | 60 | class PageNotFound < Sinatra::NotFound 61 | end 62 | 63 | class FileNotFound < Sinatra::NotFound 64 | def initialize(name) 65 | #p "FileNotFound: #{name}" 66 | end 67 | end 68 | 69 | class App < Sinatra::Base 70 | #set :environment, :production 71 | p "Environment: #{settings.environment}" 72 | 73 | set :root, File.dirname(__FILE__) 74 | set :haml, format: :html5, attr_wrapper: '"' 75 | 76 | use Rack::Cache, 77 | :verbose => true, 78 | :metastore => "file:#{$cache_path}", 79 | :entitystore => "file:#{$cache_path}" 80 | 81 | check_fs_mods = development? 82 | renames = { 83 | 'paean-article' => 'paean-article-new-url', 84 | 'invidious-article' => 'invidious-article-new-url', 85 | 'oscitate-article' => 'oscitate-article-new-url', 86 | 'piste-article' => 'piste-article-new-url', 87 | } 88 | #$pageset = Gaucho::PageSet.new('../spec/test_repo/bare.git', renames: renames) 89 | #$pageset = Gaucho::PageSet.new('../spec/test_repo/small', check_fs_mods: check_fs_mods, renames: renames) 90 | #$pageset = Gaucho::PageSet.new('../spec/test_repo/huge', check_fs_mods: check_fs_mods, renames: renames) 91 | #$pageset = Gaucho::PageSet.new('../spec/test_repo/double', check_fs_mods: check_fs_mods, renames: renames, subdir: 'yay') 92 | #$pageset = Gaucho::PageSet.new('../spec/test_repo/double', check_fs_mods: check_fs_mods, renames: renames, subdir: 'nay') 93 | 94 | $pageset = Gaucho::PageSet.new('../../ba-import/new', check_fs_mods: check_fs_mods) 95 | 96 | =begin 97 | $pageset.pages.each do |page| 98 | if page.meta.tags.nil? || page.meta.tags.empty? 99 | puts page.id 100 | p page.meta.tags 101 | end 102 | end 103 | #ap $pageset 104 | p Renderer.filter_from_name('foo.txt') 105 | p Renderer.filter_from_name('foo.text') 106 | p Renderer.filter_from_name('foo.js') 107 | p Renderer.filter_from_name('foo.css') 108 | p Renderer.filter_from_name('foo.markdown') 109 | p Renderer.filter_from_name('foo.html') 110 | p Renderer.filter_from_name('foo.bar') 111 | pg = $pageset['unicode-article'] 112 | p pg.title 113 | p '== files ==' 114 | pg.files.each do |name, data| 115 | p [name, data.encoding.name, data.length, data.size, data.bytesize] 116 | end 117 | p '== commit ==' 118 | pg.commits.last.files.each do |name, data| 119 | p [name, data.encoding.name, data.length, data.size, data.bytesize] 120 | end 121 | p $pageset.first.commit.diffs 122 | p $pageset.first.files 123 | p $pageset.subdir_path 124 | p $pageset.abs_subdir_path 125 | p $pageset.tree 126 | p $pageset.last 127 | p $pageset.length 128 | c = $pageset.first.commit 129 | p c.author.name 130 | p c.author.email 131 | p c.authored_date 132 | p c.committer.name 133 | p c.committer.email 134 | p c.committed_date 135 | =end 136 | 137 | helpers do 138 | def date_format(date) 139 | ugly = date.strftime('%s') 140 | pretty = date.strftime('%b %e, %Y at %l:%M%P') 141 | %Q{#{pretty}} 142 | end 143 | def tag_url(tag) 144 | "/stuff-tagged-#{tag}" 145 | end 146 | def cat_url(cat) 147 | "/#{cat}-stuff" 148 | end 149 | # Render diffs for page revision history. 150 | def render_diff(diff) 151 | unless diff.binary? 152 | d = Diffy::Diff.new('', '') 153 | d.diff = diff.data 154 | d.to_s(:html) 155 | end 156 | end 157 | # Get all pages in a category, grouped by the other categories. 158 | def pages_by_category(pages, cat) 159 | result = {} 160 | pages.each do |page| 161 | if page.categories.index(cat) 162 | page.categories.each do |c| 163 | if c != cat 164 | result[c] ||= [] 165 | result[c] << page 166 | end 167 | end 168 | end 169 | end 170 | result 171 | end 172 | end 173 | 174 | before do 175 | cache_control :public, :max_age => 1000000 if production? 176 | end 177 | 178 | not_found do 179 | "

OMG 404

#{' '*512}" 180 | end 181 | 182 | get '/favicon.ico' do; 'COMING SOON'; end 183 | 184 | register Sinatra::Compass 185 | get_compass 'css' 186 | 187 | # TODO: REMOVE? 188 | get '/rebuild' do 189 | $pageset.rebuild! 190 | redirect '/' 191 | end 192 | 193 | def tags(pages = @pages) 194 | tags = [] 195 | tmp = {} 196 | pages.each {|p| p.tags.each {|t| tmp[t] ||= 0; tmp[t] += 1}} 197 | tmp.each {|t, n| tags << Gaucho::Config.new(tag: t, count: n)} 198 | min, max = tags.collect {|t| t.count}.minmax 199 | tags.sort! {|a, b| a.tag <=> b.tag} 200 | tags.each {|t| t.scale = [(200 * (t.count - min)) / (max - min), 100].max} 201 | end 202 | 203 | # INDEX 204 | get %r{^(?:/([0-9a-f]{7}))?/?$} do |sha| 205 | p ['index', params[:captures]] 206 | #start_time = Time.now 207 | puts 1 208 | @pages = $pageset #.reset_shown 209 | puts 2 210 | @tags = [] #tags 211 | @cats = [] #@pages.collect {|c| c.categories}.flatten.uniq.sort 212 | puts 3 213 | #@pages = pages_categorized('music') 214 | #@pages.reject! {|page| page.date?} 215 | #count = @pages.length 216 | puts 4 217 | 218 | #$not_dated ||= @pages.reject {|page| page.date?} 219 | #$dated ||= @pages.select {|page| page.date?} 220 | 221 | # All project pages, sorted by other-category. 222 | $projects ||= pages_by_category($pageset, 'Projects') 223 | # All article pages, sorted by other-category. 224 | $articles ||= pages_by_category($pageset, 'Articles') 225 | 226 | @pages = [] 227 | =begin 228 | @pages = not_dated.select {|page| page.categories.index('Projects')} + 229 | not_dated.reject {|page| page.categories.index('Projects')} + 230 | dated.select {|page| page.categories.index('Music')} + 231 | dated.reject {|page| page.categories.index('Music')} 232 | =end 233 | # + @pages.select {|page| page.date?} 234 | @title = 'omg index' 235 | haml :index 236 | end 237 | 238 | # TAGS 239 | # /stuff-tagged-{tag} 240 | get %r{^/stuff-tagged-([-\w]+)} do |tag| 241 | p ['tag', params[:captures]] 242 | @pages = $pageset.reset_shown 243 | @pages.reject! {|p| p.tags.index(tag).nil?}.sort 244 | @title = %Q{Stuff tagged “#{tag}”} 245 | @tags = tags 246 | @cats = @pages.collect {|cat| cat.categories}.flatten.uniq.sort 247 | @index_back = true 248 | haml :index 249 | end 250 | 251 | # CATEGORIES 252 | # /{category}-stuff 253 | get %r{^/([-\w]+)-stuff$} do |cat| 254 | p ['cat', params[:captures]] 255 | @pages = $pageset.reset_shown 256 | @pages.reject! {|p| p.categories.index(cat).nil?}.sort 257 | pass if @pages.empty? 258 | @title = %Q{Stuff categorized “#{cat}”} 259 | @tags = tags 260 | @cats = [cat] 261 | @index_back = true 262 | haml :index 263 | end 264 | 265 | =begin 266 | # RECENT CHANGES 267 | # /recent-changes 268 | get '/recent-changes' do 269 | p ['recent-changes'] 270 | @pages = $all_pages 271 | @pages.each {|page| p page.commits.last.message} 272 | @tags = [] 273 | @cats = [] 274 | @index_back = true 275 | haml :index 276 | end 277 | =end 278 | 279 | # DATE LISTING 280 | # /{YYYY} 281 | # /{YYYY}/{MM} 282 | # /{YYYY}/{MM}/{DD} 283 | get %r{^/(\d{4})(?:/(\d{2}))?(?:/(\d{2}))?/?$} do |year, month, day| 284 | p ['date', params[:captures]] 285 | date_arr = [year, month, day].compact 286 | @pages = $pageset.reset_shown.select {|page| page.date?(date_arr)} 287 | @title = %Q{Stuff dated “#{date_arr.join('-')}”} 288 | @tags = [] 289 | @cats = [] 290 | @index_back = true 291 | haml :index 292 | end 293 | 294 | # PAGE 295 | # /{name} 296 | # /{sha}/{name} 297 | # /{sha}/{name}/{file} 298 | # "name" can be (slashes are just replaced with dashes): 299 | # name 300 | # YYYY/name 301 | # YYYY/MM/name 302 | # YYYY/MM/DD/name 303 | get %r{^(?:/([0-9a-f]{7}))?/((?:\d{4}(?:/\d{2}){0,2}/)?[^/]+)(?:/(.+))?$} do |sha, name, file| 304 | p ['page', params[:captures]] 305 | 306 | begin 307 | @page = $pageset[name] 308 | raise Sinatra::NotFound if @page.nil? 309 | if @page.class == String 310 | redirect @page, 302 # 301 311 | # cache? 312 | return #"redirect to #{@page}" 313 | end 314 | 315 | @page.shown = sha 316 | 317 | if sha && production? 318 | # cache heavily 319 | end 320 | 321 | if file 322 | content_type File.extname(file) rescue content_type :txt 323 | @page/file 324 | else 325 | @commit = @page.commit 326 | @commits = @page.commits 327 | @title = @page.title 328 | @content = @page.render #(nil, generate_toc: false) 329 | @index_back = true 330 | haml(@page.layout || :page) 331 | end 332 | #rescue 333 | # raise Sinatra::NotFound 334 | end 335 | end 336 | end 337 | end 338 | 339 | if $0 == __FILE__ 340 | Gaucho::App.run! :host => 'localhost', :port => 4567 341 | end 342 | -------------------------------------------------------------------------------- /lib/gaucho/renderer.rb: -------------------------------------------------------------------------------- 1 | module Gaucho 2 | class Page 3 | def render(data = nil, options = {}) 4 | data = self.public_send(data) if data.class == Symbol 5 | Gaucho::Renderer.render_page(self, data, options) 6 | end 7 | end 8 | 9 | # Render a Page. 10 | # 11 | # All internal asset links processed via the {{...}} syntax will incorporate 12 | # the current SHA when rendering, so be sure to use the {{...}} syntax for 13 | # *all* internal asset linking. 14 | # 15 | # {{ asset }} 16 | # {{ asset | filter }} 17 | # 18 | # Filter arguments may be passed as a hash, pretty much any way you'd like, 19 | # but this is probably the simplest way: 20 | # {{ asset | filter(src: "img.jpg", alt: "Foo!") }} 21 | # 22 | # If a filter implements default_attr, a non-hash-like argument will be 23 | # automatically "upgraded" to a hash. For example, if o.default_attr = 'alt', 24 | # these would be equivalent: 25 | # 26 | # {{ asset | filter(alt: "Hello there!") }} 27 | # {{ asset | filter(Hello there!) }} 28 | # 29 | # Notes: 30 | # * multiple "| filter" can be specified (is there any value to this?) 31 | # * whitespace is insignificant and will be removed. 32 | # 33 | module Renderer 34 | extend StringUtils 35 | 36 | # Embed markdown. 37 | # 38 | # {{ content.md | markdown }} 39 | def self.markdown(o) 40 | # Convert options passed to Page#render into arguments for RDiscount. 41 | opts = { 42 | smart: true, 43 | generate_toc: true 44 | }.merge(o.options) 45 | args = opts.to_a.map{|key, value| value ? key : nil}.compact 46 | 47 | rd = RDiscount.new(o.data, *args) 48 | content = fix_encoding(rd.to_html) 49 | 50 | return content unless opts[:generate_toc] 51 | 52 | toc = fix_encoding(rd.toc_content) 53 | # Since the largest header used in content is typically H2, remove the 54 | # extra unnecessary
    created by RDiscount when a H1 doesn't exist in 55 | # the content. 56 | toc.sub!(%r{^(\s+)(
      )\n\n\1\2(.*)\n(\s+)\n\4
    \n$}m, "\\1\\2\\3\n") 57 | 58 | # Tweak generated TOC links/ids so that they look a bit cleaner, replacing 59 | # any unicode chars with their "non-unicode equivalent" and changing any 60 | # runs of non-alphanumeric chars to hyphens. 61 | block = lambda do |m| 62 | a, z = $1, $3 63 | id = transliterate($2).downcase 64 | id = id.gsub(/['"]/, '').gsub(/[^a-z0-9]+/, '-').gsub(/^-|-$/, '') 65 | "#{a}#{id}#{z}" 66 | end 67 | toc.gsub!(/(/, toc) 75 | end 76 | 77 | # Replace any {{ toc }} placeholder with the RDiscount-generated TOC. 78 | # 79 | # {{ toc }} 80 | def self.toc(o) 81 | %Q{} 82 | end 83 | 84 | # Embed html. 85 | # 86 | # {{ content.html | html }} 87 | def self.html(o) 88 | return invalid_encoding(o.name) unless o.valid_data? 89 | o.data 90 | end 91 | 92 | # Embed text, escaping any HTML. 93 | # 94 | # {{ content.txt | text }} 95 | # {{ content.txt | text(class: "awesome-pre") }} 96 | def self.text(o) 97 | return invalid_encoding(o.name) unless o.valid_data? 98 | %Q{#{escape(o)}} 99 | end 100 | 101 | # Escape HTML. 102 | # 103 | # {{ content.html | escape }} 104 | def self.escape(o) 105 | return invalid_encoding(o.name) unless o.valid_data? 106 | CGI::escapeHTML(o.data) 107 | end 108 | 109 | # Embed syntax-highlighted source code. 110 | # 111 | # {{ source.rb }} 112 | # {{ content.html | code }} 113 | # {{ chat.irc | code(nolines: true) }} 114 | # {{ rubyfile.txt | code(type: :ruby) }} 115 | def self.code(o) 116 | return invalid_encoding(o.name) unless o.valid_data? 117 | 118 | # `nolines` option will override default (lines). 119 | nolines = o.args.to_hash.delete(:nolines) 120 | 121 | # `type` option will override default type. 122 | ext = o.args.to_hash.delete(:type) || File.extname(o.name)[1..-1] 123 | 124 | # Merge args. 125 | # TODO: figure out options: hl_lines: [1,3,5], linenostart 126 | args = {} 127 | args.merge!(linenos: :table, anchorlinenos: true, lineanchors: o.name) unless nolines 128 | args.merge!(o.args) 129 | 130 | # Construct pygmentize CLI params and highlight code. 131 | params = args.map {|k, v| ['-P', %Q{#{k}="#{v}"}]}.flatten 132 | code = Pygmentize.process(o.data, ext, params) 133 | 134 | unindent(<<-EOF) 135 |
    136 | 137 |
    #{code.gsub(/^/, ' ' * 4)}
    138 |
    139 | EOF 140 | end 141 | 142 | # Get a raw Blob URL. 143 | # 144 | # 145 | def self.url(o) 146 | if o.page.shown_fs_mods? 147 | "#{o.page.url}/#{o.name}" 148 | else 149 | "#{o.page.commit.url}/#{o.name}" 150 | end 151 | end 152 | 153 | # Embed an asset script. 154 | # 155 | # {{ awesome.js | script }} 156 | # {{ awesome.js | script(id: "test") }} 157 | def self.script(o) 158 | %Q{} 159 | end 160 | 161 | # Embed an asset CSS stylesheet. 162 | # 163 | # {{ pretty.css | css }} 164 | # {{ pretty.css | css(media: "screen") }} 165 | def self.css(o) 166 | %Q{} 167 | end 168 | 169 | # Embed an asset image. 170 | # 171 | # {{ image.jpg | image }} 172 | # {{ image.jpg | image(width: "20", style: "float:right") }} 173 | def self.image(o) 174 | o.default_attr = 'alt' 175 | %Q{} 176 | end 177 | 178 | # Embed an asset link. 179 | # 180 | # {{ file.txt | link }} 181 | # {{ file.txt | link(class: "popup") }} 182 | def self.link(o, download = false) 183 | query_string = download ? '?dl=1' : '' 184 | %Q{
    #{o.name}} 185 | end 186 | 187 | # Embed a downloadable asset link. 188 | # 189 | # {{ file.txt | download }} 190 | # {{ file.txt | download(class: "external") }} 191 | def self.download(o) 192 | link(o, true) 193 | end 194 | 195 | # Which filter should be used, by default, for a given file extension? If 196 | # a matching filter isn't found, the @@filter_default value is used. 197 | @@filter_default = :code 198 | @@filter_map = { 199 | toc: [:toc], # hackish 200 | markdown: [:md], 201 | html: [:inc, :ssi], 202 | text: [:txt], 203 | image: [:jpg, :jpeg, :gif, :png], 204 | code: [:html, :js, :css, :rb] 205 | } 206 | 207 | # Expose @@filter_map and @@filter_default to allow modifications. 208 | def self.filter_default=(value); @@filter_default = value; end 209 | def self.filter_default; @@filter_default; end 210 | def self.filter_map; @@filter_map; end 211 | 212 | # Render content recursively, starting with index. 213 | def self.render_page(page, data = nil, options = {}, name = nil, filter = nil, args = nil) 214 | data = page.content if data.nil? 215 | name = page.meta.index_name if name.nil? 216 | filter = filter_from_name(name) if filter.nil? 217 | #p [name, filter, args, data.class, data.valid_encoding?] 218 | 219 | if data.encoding.name == 'UTF-8' && data.valid_encoding? && data =~ /\{\{/ 220 | # Process all {{ ... }} substrings. 221 | data.gsub!(/\{\{\s*(.*?)\s*\}\}/) do |match| 222 | # Parse into a name plus array of zero or more filters. I can't really 223 | # think of a good reason to have multiple filters, but why not, right? 224 | name, *filters = $1.split(/\s*\|\s*/) 225 | 226 | # Parse filters into filter/argument pairs. 227 | filters.collect! do |f| 228 | f =~ /^([a-z_][a-z0-9_]*?)(?:\((.*)\))?$/ 229 | [ $1, $2 ] unless $1.nil? 230 | end 231 | filters.compact! 232 | 233 | # If no filter was specified, choose a default filter based on the 234 | # filename and @@filter_map. 235 | filters = [filter_from_name(name)] if filters.empty? 236 | 237 | result = page/name rescue invalid_file(name) 238 | filters.each do |f, a| 239 | #p ['*', name, f, a] 240 | result = render_page(page, result, options, name, f.to_sym, a)# rescue '12345' 241 | end 242 | 243 | result 244 | end 245 | end 246 | 247 | # If a filter exists to handle this request, use it, otherwise error. 248 | if respond_to?(filter) 249 | public_send(filter, RendererOptions.new(page, data, options, name, args)) 250 | else 251 | invalid_filter(filter, name) 252 | end 253 | end 254 | 255 | # Get the appropriate filter for a give filename. 256 | def self.filter_from_name(name = '') 257 | return nil unless name =~ /([^.]+)$/ 258 | 259 | ext = $1.downcase.to_sym 260 | 261 | map_arr = filter_map.to_a 262 | type = map_arr.select {|filter, exts| exts.find {|e| e == ext}} 263 | type = map_arr.select {|filter, exts| filter == ext} if type.empty? 264 | 265 | type.empty? ? filter_default : type.first.first 266 | end 267 | 268 | # Handle binary or invalidly encoded data in a helpful way. 269 | def self.invalid_encoding(file) 270 | %Q{Invalid encoding: #{file}} 271 | end 272 | 273 | # Handle invalid files in a helpful way. 274 | def self.invalid_file(file) 275 | %Q{Invalid file: #{file}} 276 | end 277 | 278 | # Handle invalid filters in a helpful way. 279 | def self.invalid_filter(filter, file) 280 | %Q{Invalid filter: #{filter} (#{file})} 281 | end 282 | 283 | # Create a data object that can be passed into a filter method. 284 | class RendererOptions < Gaucho::Config 285 | extend StringUtils 286 | 287 | attr_accessor :default_attr 288 | 289 | def initialize(page, data, options, name, args) 290 | super({ 291 | page: page, 292 | data: data, 293 | options: options, 294 | name: name, 295 | args: self.class.evalify(args) 296 | }) 297 | end 298 | 299 | def attrs 300 | # If args is String, convert to Hash {default_attr => that_string} and 301 | # build attrs from that, otherwise just use args. 302 | if default_attr && args.class == String 303 | map = {} 304 | map[default_attr] = args 305 | html_attrs(map) 306 | else 307 | html_attrs 308 | end 309 | end 310 | 311 | # Ensure that data is not binary or invalidly encoded. 312 | def valid_data? 313 | self.class.valid_data?(data) 314 | end 315 | 316 | protected 317 | 318 | # Attempt to convert args to HTML attributes. 319 | def html_attrs(map = args.to_hash) 320 | if map.class == Hash && !map.empty? 321 | arr = [] 322 | map.each {|key, value| arr << %Q{#{key}="#{CGI::escapeHTML(value.to_s)}"}} 323 | " #{arr.join(' ')}" 324 | elsif map.class == String && !map.empty? 325 | " #{map}" 326 | else 327 | '' 328 | end 329 | end 330 | end 331 | end 332 | end 333 | 334 | # Hopefully this will go away soon. 335 | # https://github.com/djanowski/pygmentize/pull/1 336 | class Pygmentize 337 | def self.process(source, lexer, args = []) 338 | args += [ 339 | "-l", lexer.to_s, 340 | "-f", "html", 341 | "-O", "encoding=#{source.encoding}" 342 | ] 343 | 344 | IO.popen("#{bin} #{Shellwords.shelljoin args}", "r+") do |io| 345 | io.write(source) 346 | io.close_write 347 | io.read 348 | end 349 | end 350 | end -------------------------------------------------------------------------------- /spec/create_test_repo.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'grit' 4 | require 'fileutils' 5 | require 'yaml' 6 | require 'base64' 7 | require 'unicode_utils' 8 | require 'open3' 9 | 10 | class String 11 | def transliterate 12 | UnicodeUtils.nfkd(self).gsub(/[^\x00-\x7F]/, '').to_s 13 | end 14 | end 15 | 16 | class Array 17 | def to_yaml_style 18 | :inline 19 | end 20 | 21 | # Shift `n` items off the front of the array and push them onto the end, 22 | # returning the items that were shift-pushed. 23 | def shift_rotate(n = nil) 24 | result = self.shift(n) 25 | self.push(*result) 26 | result 27 | end 28 | # Perform a shift_rotate on the passed-in `arr` array, then push the returned 29 | # results onto `self` and unique. 30 | def add_and_rotate(arr, n = nil) 31 | result = arr.shift_rotate(n) 32 | self.push(*result).uniq! 33 | end 34 | end 35 | 36 | class TestRepoBuilder 37 | attr_accessor :titles, :titles_check_fs_mods, :alt_titles, :page_subdirs 38 | 39 | def initialize(path) 40 | print %Q{Building repo "#{path}"...} 41 | @repo_path = File.join($root, path) 42 | FileUtils.mkdir_p(@repo_path) 43 | FileUtils.cd(@repo_path) 44 | `git init .` 45 | init_ivars 46 | if block_given? 47 | yield self 48 | print "done!\n" 49 | end 50 | end 51 | 52 | def init_ivars 53 | @titles = [] 54 | @titles_check_fs_mods = [] 55 | @alt_titles = [] 56 | 57 | @all_cats = %w(news projects articles) 58 | @all_tags = %w(fun awesome cool lame bad sweet great weak zesty) 59 | 60 | @all_more_toc = ['', "\n\n\n{{ toc }}\n", "\n\n"] 61 | 62 | @all_authors = [nil, 'John Q. Public', 'John Q. Public '] 63 | 64 | @all_dates = [nil, '1999-12-31 23:59:59 -0500'] 65 | 66 | @all_texts = [ 67 | %Q{This text has **bold** and _italic_ text, some "quoted text that can't be beat," and look, [an external link](http://benalman.com) too!}, 68 | 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur at erat id tellus rutrum cursus. Cras et elit est. Fusce id sapien nec dolor elementum tempus nec pulvinar diam. Sed euismod, sem ut luctus ullamcorper, nibh tellus volutpat felis, eget tempor quam sem a tortor. Nam tortor felis, mollis vitae vehicula non, consequat vel magna.', 69 | 'Mauris suscipit cursus fringilla. Donec ut nisl quam, non blandit odio. Aenean quis est a massa iaculis ultricies. Nulla vel velit magna. Vivamus eget tortor ipsum, ac feugiat augue. Vivamus vel ipsum lorem.', 70 | 'Praesent sapien massa, egestas venenatis tempor et, auctor ac libero. Duis a eleifend metus. Proin ultrices hendrerit rutrum. Curabitur id lorem eget nisi faucibus placerat. Vestibulum vitae nisl erat, quis elementum enim.', 71 | "* List item foo 1\n* List item bar 2\n* List item baz 3", 72 | "1. Ordered list item foo 1\n2. Ordered list item bar 2\n3. Ordered list item baz 3", 73 | ] 74 | 75 | @all_incls = [] 76 | @all_incls = [] 77 | @all_incls << ['sample.rb', false, <<-EOF 78 | def wtf(x) 79 | if x < 17 && x > 5 80 | puts "yay, the < and > were escaped properly" 81 | end 82 | end 83 | EOF 84 | ] 85 | @all_incls << ['awesome.js', false, <<-EOF 86 | function awesome() { 87 | console.log( 'OMG AWESOME!1' ); 88 | } 89 | EOF 90 | ] 91 | @all_incls << ['fancy.css', false, <<-EOF 92 | body.fancy { 93 | color: red; 94 | background: blue; 95 | } 96 | EOF 97 | ] 98 | @all_incls << ['lolwat.html', false, %Q{

    LOL WAT

    99 |

    SUPER DUPER COOL

    }] 100 | @all_incls << ['haiku.txt', false, %Q{this is a sample 101 | text file with the answer to 102 | the meaning of life}] 103 | @all_incls << ['escaped_html.txt', false, %Q{

    ZOMG ESCAPED HTML

    }] 104 | @all_incls << @hat = ['cowboy_hat.png', true, <<-EOF 105 | iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0 106 | d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAKJJREFUeNqkUsENgDAIFOLL 107 | qbqAezhGx9A5XMAFXMcvetXEFkO1laQxAe64A0lEmj/BZsWT1BMc4K0fbxJ8 108 | DULOsYNkWyeps3BFNw8FFiD9mgigBoeassKlS9O9HCfgOScZtbDcCMPNz2Bz 109 | gnEVrZDwJ3pH4l3a+HYRv+AJtUnCfTsdehMFIenuzcZqLCCmJwSaJK/gBD8I 110 | 3ohiYJagJHYBBgAdlVsmfBCYiQAAAABJRU5ErkJggg== 111 | EOF 112 | ] 113 | 114 | @page_subdirs = [] 115 | @file_subdirs = ['', 'zing/', 'zang/', 'zing/zong/'] 116 | 117 | @page_paths = {} 118 | 119 | @time = Time.new(2007, 1, 2, 3, 4, 5, '-05:00') 120 | end 121 | 122 | def article_title(title) 123 | "#{title} Article" 124 | end 125 | 126 | def article_path(title) 127 | path = article_title(title).transliterate.downcase.sub(' ', '-') 128 | @page_paths[title] ||= if @page_subdirs.empty? 129 | path 130 | else 131 | "#{@page_subdirs.shift_rotate(1)[0]}/#{path}" 132 | end 133 | end 134 | 135 | def read_index(title) 136 | path = article_path(title) 137 | docs = [] 138 | File.open("#{path}/index.md") do |file| 139 | YAML.each_document(file) {|doc| docs << doc} 140 | end 141 | docs 142 | end 143 | 144 | def write_index(title, docs) 145 | path = article_path(title) 146 | # Since this script tries to simulate how user data will actually be stored, 147 | # ie. without extra quoting or \\u-style escaping, and because .to_yaml escapes 148 | # unicode and quotes multi-line strings, YAML serialization is done manually. 149 | metas = [] 150 | docs.first.each do |key, value| 151 | metas << if value.class == Array 152 | "#{key}: [#{value.join(', ')}]" 153 | else 154 | "#{key}: #{value}" 155 | end 156 | end 157 | index = metas.join("\n") + "\n--- |\n" + docs.last 158 | FileUtils.mkdir_p(path) 159 | File.open("#{path}/index.md", 'w') {|f| f.write(index)} 160 | end 161 | 162 | def write_incl(title, file = nil) 163 | path = article_path(title) 164 | file = @all_incls.shift_rotate(1)[0] if file.nil? 165 | subdir = @file_subdirs.shift_rotate(1)[0] 166 | FileUtils.mkdir_p("#{path}/#{subdir}") 167 | file_path = "#{path}/#{subdir}#{file[0]}" 168 | if file[1] 169 | File.open(file_path, 'wb') {|f| f.write(Base64.decode64(file[2]))} 170 | else 171 | File.open(file_path, 'w') {|f| f.write(file[2])} 172 | end 173 | "#{subdir}#{file[0]}" 174 | end 175 | 176 | def create_article(title) 177 | path = article_path(title) 178 | incl = write_incl(title, @hat) 179 | docs = [] 180 | docs << meta = { 181 | 'Title' => article_title(title), 182 | 'subtitle' => "This is some stuff about #{title}.", 183 | 'categories' => @all_cats.shift_rotate(1), 184 | 'tags' => @all_tags.shift_rotate(2) 185 | } 186 | date = @all_dates.shift_rotate(1)[0] 187 | meta['date'] = date unless date.nil? 188 | author = @all_authors.shift_rotate(1)[0] 189 | meta['Author'] = author unless author.nil? 190 | docs << <<-EOF 191 | 192 | This is a sample article about the word "[#{title}](/#{path}#arbitrary-hash)." 193 | 194 | [{{ #{incl} }} Super Cowboy Hats!! {{ #{incl} }}]({{ #{incl} | url }}) 195 | 196 | 208 | 209 | #{@all_more_toc.shift_rotate(1)[0]} 210 | EOF 211 | docs 212 | end 213 | 214 | def commit_articles(commit_msg, added = false) 215 | print '.' 216 | `git add .` 217 | @time += 86400 * 1.8 218 | author_time = "#{@time.to_i} -0500" 219 | @time += 86400 * 0.3 220 | commit_time = "#{@time.to_i} -0500" 221 | `export GIT_AUTHOR_DATE="#{author_time}"; export GIT_COMMITTER_DATE="#{commit_time}"; git commit -m "#{commit_msg}"` 222 | end 223 | 224 | def do_stuff 225 | # create articles 226 | @titles.each do |title| 227 | docs = create_article(title) 228 | write_index(title, docs) 229 | commit_articles("Added #{title} article.", true) 230 | end 231 | 232 | # modify tags, delete date, add text 233 | @titles.each do |title| 234 | docs = read_index(title) 235 | docs[0]['tags'].add_and_rotate(@all_tags, 1) 236 | docs[0].delete('date') 237 | docs[1] += <<-EOF 238 | 239 | ## Sample header foo 240 | 241 | #{@all_texts.shift_rotate(1).join} 242 | EOF 243 | 244 | write_index(title, docs) 245 | commit_articles("#{title}: added a tag and some content.") 246 | end 247 | 248 | # modify subtitle, add text 249 | @titles.each do |title| 250 | docs = read_index(title) 251 | docs[0]['subtitle'].sub(/some stuff/, 'an article') 252 | docs[1] += <<-EOF 253 | 254 | ## Î'm lòvíñg "Çråzy" Üñîçòdé Hëàdérs!!? 255 | 256 | #{@all_texts.shift_rotate(1).join} 257 | EOF 258 | 259 | write_index(title, docs) 260 | commit_articles("#{title}: tweaked subtitle, added more content.") 261 | end 262 | 263 | # change text 264 | @titles.each do |title| 265 | docs = read_index(title) 266 | docs[1].gsub!(/\b(foo|bar|baz)\b/) do |word| 267 | "#{word.upcase}!" 268 | end 269 | 270 | write_index(title, docs) 271 | commit_articles("#{title}: uppercased a word (or three).") 272 | end 273 | 274 | # add content and a file 275 | @titles.each do |title| 276 | docs = read_index(title) 277 | incl = write_incl(title) 278 | docs[1] += <<-EOF 279 | 280 | ### Including a file, a few different ways. 281 | 282 | The file {{ #{incl} | link }}, included in its default format: 283 | 284 | {{ #{incl} }} 285 | 286 | And explicitly as text: 287 | 288 | {{ #{incl} | text }} 289 | EOF 290 | 291 | write_index(title, docs) 292 | commit_articles("#{title}: included a file.") 293 | end 294 | 295 | # add a tag, two more files, committing articles in groups of 3 296 | titles_tmp = [] 297 | @titles.each_index do |i| 298 | title = @titles[i] 299 | titles_tmp << title 300 | 301 | docs = read_index(title) 302 | docs[0]['tags'].add_and_rotate(@all_tags, 1) 303 | 304 | incl = write_incl(title) 305 | docs[1] += <<-EOF 306 | 307 | ### Including a second file: 308 | 309 | {{ #{incl} }} 310 | EOF 311 | 312 | incl = write_incl(title) 313 | docs[1] += <<-EOF 314 | 315 | ### And a third file: 316 | 317 | {{ #{incl} }} 318 | EOF 319 | 320 | write_index(title, docs) 321 | 322 | if (@titles.length - i - 1) % 3 == 0 323 | commit_articles("#{titles_tmp.join(', ')}: included 2 files#{titles_tmp.length > 1 ? ' (per article)' : ''}.") 324 | titles_tmp = [] 325 | end 326 | end 327 | 328 | # make a branch with some modifications 329 | `git checkout -q -b april_fools` 330 | @titles.each do |title| 331 | docs = read_index(title) 332 | docs[0]['Title'] += ' APRIL FOOLS!!!' 333 | 334 | write_index(title, docs) 335 | commit_articles("#{title}: made a lame joke.") 336 | end 337 | `git checkout -q master` 338 | 339 | # modify a few articles (uncommitted) 340 | @titles_check_fs_mods.each do |title| 341 | docs = read_index(title) 342 | docs[0]['Title'] += '!!!' 343 | docs[1].gsub!(/(This is a sample article)/, '\1 with **LOCAL MODIFICATIONS**') 344 | 345 | write_index(title, docs) 346 | end 347 | 348 | # create an article (uncommitted) 349 | @alt_titles.each do |title| 350 | docs = create_article(title) 351 | incl = write_incl(title) 352 | docs[1] += <<-EOF 353 | 354 | ### Including a file, a few different ways. 355 | 356 | The file {{ #{incl} | link }}, included in its default format: 357 | 358 | {{ #{incl} }} 359 | 360 | And explicitly as text: 361 | 362 | {{ #{incl} | text }} 363 | EOF 364 | 365 | write_index(title, docs) 366 | end 367 | end 368 | end 369 | 370 | @titles = %w{Ünîçòdé Gallimaufry Haptic Lacuna Sidereal Risible Turophile Scion Opprobrium Paean Brobdignagian Abecedarian Paronomasia Putative Algid Piste Factotum Festschrift Skepsis Micawber Jitney Retral Sobriquet Tumid Pule Meed Oscitate Oolert Sartorial Vitiate Chiliad Aestival Sylva Stat Anomie Cheval-de-frise Pea-souper Autochthon Jument Lascivious Aglet Comity Embrocation Hidrosis Jeremiad Kerf Legerity Marmoreal Oikology Pessimal Quidam Recondite Sybaritic Tyro Ullage Vigorish Xanthochroi Yestreen Zenana Gribble Pelf Aeneous Foofaraw Lanai Shandrydan Tardigrade Ontic Lubricious Inchmeal Costermonger Pilgarlic Costard Quotidian Nystagmus Dubiety Jactation Lubritorium Cullion Wallydrag Flaneur Anodyne Weazen Brumal Incarnadine Gork Xanthous Demersal Anthemion Clapperclaw Kloof Pavid Wyvern Flannelmouthed Chondrule Kyte Pawky Katzenjammer Catchpenny Quincunx Abulia Roundheel Bruxism Aeolian Chorine Infrangible Patzer Mistigris Misoneism Embrangle Exigent Wamble Maven Pulvinar Digerati Exiguous Prolegomenon Pridian Dirl Viviparous Denouement Miscegenation Vavasor Xerosis Gunda Nabob Planogram Zarf Xyloid Invidious Nugatory Decrescent Palmy Frittle Risorial Demesne Asperse Crankle Blague Diapason Palliate Narghile Flagitious Fizgig Troilism Irredenta Balter Tripsis Gormless Anfractuous Lulliloo} 371 | @alt_titles = %w{Discalceate Mimesis Pleonasm Bezoar Volacious Demiurgic Kakistocracy Mell Psilanthropy Pulchritude} 372 | 373 | $root = File.expand_path('test_repo') 374 | 375 | if Dir.exist?($root) 376 | puts 'Deleting old test repos...' 377 | FileUtils.rm_rf($root) 378 | end 379 | 380 | TestRepoBuilder.new('bare') do |trb| 381 | trb.titles = @titles.shift(10) 382 | trb.do_stuff 383 | end 384 | 385 | FileUtils.mv(File.join($root, 'bare', '.git'), File.join($root, 'bare.git')) 386 | FileUtils.rm_rf(File.join($root, 'bare')) 387 | 388 | TestRepoBuilder.new('small') do |trb| 389 | trb.titles = @titles.shift(10) 390 | trb.titles_check_fs_mods = trb.titles[0..2] 391 | trb.alt_titles = @alt_titles.shift(1) 392 | trb.do_stuff 393 | end 394 | 395 | false && TestRepoBuilder.new('double') do |trb| 396 | trb.titles = @titles.shift(20) 397 | trb.titles_check_fs_mods = trb.titles[0..4] 398 | trb.alt_titles = @alt_titles.shift(2) 399 | trb.page_subdirs = ['yay/', 'nay/'] 400 | trb.do_stuff 401 | end 402 | 403 | false && TestRepoBuilder.new('huge') do |trb| 404 | trb.titles = @titles.shift(100) 405 | trb.titles_check_fs_mods = trb.titles[0..10] 406 | trb.alt_titles = @alt_titles.shift(4) 407 | trb.page_subdirs = [] 408 | trb.do_stuff 409 | end 410 | -------------------------------------------------------------------------------- /LICENSE-GPL: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. --------------------------------------------------------------------------------