├── .gems ├── .gitignore ├── Gemfile ├── MIT-LICENSE ├── README.md ├── Rakefile ├── VERSION ├── aerial.gemspec ├── articles └── congratulations │ └── congratulations-aerial-is-configured-correctly.article ├── bin └── aerial ├── config.ru ├── config ├── config.sample.ru ├── config.sample.yml ├── config.test.yml ├── config.yml ├── deploy.rb └── thin.sample.yml ├── examples ├── articles │ └── congratulations │ │ └── congratulations-aerial-is-configured-correctly.article ├── public │ └── javascripts │ │ ├── application.js │ │ ├── jquery-1.3.1.min.js │ │ └── jquery.template.js └── views │ ├── article.haml │ ├── articles.haml │ ├── comment.haml │ ├── home.haml │ ├── layout.haml │ ├── not_found.haml │ ├── post.haml │ ├── rss.haml │ ├── sidebar.haml │ └── style.sass ├── index.html ├── lib ├── aerial.rb └── aerial │ ├── app.rb │ ├── article.rb │ ├── base.rb │ ├── build.rb │ ├── config.rb │ ├── content.rb │ ├── installer.rb │ ├── migrator.rb │ ├── migrators │ └── mephisto.rb │ ├── site.rb │ └── version.rb ├── public └── javascripts │ └── application.js ├── spec ├── aerial_spec.rb ├── app_spec.rb ├── article_spec.rb ├── base_spec.rb ├── config_spec.rb ├── fixtures │ ├── articles │ │ ├── congratulations-aerial-is-configured-correctly │ │ │ └── congratulations-aerial-is-configured-correctly.article │ │ ├── sample-article │ │ │ └── sample-article.article │ │ ├── test-article-one │ │ │ └── test-article.article │ │ ├── test-article-three │ │ │ └── test-article.article │ │ └── test-article-two │ │ │ ├── comment-missing-fields.comment │ │ │ ├── test-article.article │ │ │ └── test-comment.comment │ ├── config.yml │ ├── public │ │ └── javascripts │ │ │ ├── application.js │ │ │ ├── jquery-1.3.1.min.js │ │ │ └── jquery.template.js │ └── views │ │ ├── article.haml │ │ ├── articles.haml │ │ ├── home.haml │ │ ├── layout.haml │ │ ├── not_found.haml │ │ ├── post.haml │ │ ├── rss.haml │ │ ├── sidebar.haml │ │ └── style.sass └── spec_helper.rb └── views ├── article.haml ├── articles.haml ├── home.haml ├── layout.haml ├── not_found.haml ├── post.haml ├── rss.haml ├── sidebar.haml └── style.sass /.gems: -------------------------------------------------------------------------------- 1 | sinatra -v 0.9.2 2 | rack -v 1.0.0 3 | haml -v 2.2.6 4 | aerial -v 0.1.1 5 | grit -v 1.0.1 6 | rdiscount -v 1.3.4 7 | coderay -v 0.8.357 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | spec/repo 2 | coverage 3 | content 4 | pkg 5 | tmp/ 6 | .rvmrc 7 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in lorem.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009, Matt Sears, Littlelines, LLC. 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. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Aerial 2 | ==== 3 | 4 | Aerial is a simple, blogish, web application written in Sinatra. 5 | Designed for developers, there is no admin interface and no SQL database. 6 | Articles are written in your favorite text editor and versioned with Git. 7 | Comments are handled by Disqus (http://disqus.com). It uses Grit 8 | (http://github.com/mojombo/grit) to interface with local Git repositories. 9 | 10 | Aerial also comes with a static site generator. Why, you ask? Well, 11 | static pages offer a lot of benefits: First, static pages load 12 | lightning fast. It also allows web browsers to cache files much more 13 | efficiently due to Last-Modified headers and such. 14 | 15 | Aerial can now run on Heroku! Initially, Aerial didn't work on Heroku 16 | since the .git directory is completely obliterated on each deployment. 17 | With static pages and little help from a couple Rack middleware 18 | plugins, getting Aerial on Heroku is a snap. 19 | 20 | Aerial was designed for small personal blogs and simple static websites 21 | such as marketing sites. The main goals are to provide a no-fuss alternative 22 | with a basic set features. 23 | 24 | Aerial is still in active development. 25 | 26 | ## Features ################################################################# 27 | 28 | * Pages and articles are managed thru git 29 | * Pages are represented in Haml templates 30 | * Articles are in Markdown format with embedded metadata 31 | * Comments are managed by Disqus (http://disqus.com) 32 | * Blog-like features: Recent Posts, Categories, Archives, and Tags 33 | * Static site generator 34 | * Works on Heroku! 35 | 36 | ## Installation ############################################################# 37 | 38 | $ gem install aerial 39 | $ aerial install /home/user/myblog 40 | # Navigate to 41 | 42 | This will create a new directory and a few files, mainly the views, 43 | config files, and a sample article to get you started. Then, edit 44 | config.yml to your liking. 45 | 46 | ## From Source ############################################################## 47 | 48 | Aerial's Git repo is available on GitHub, which can be browsed at: 49 | 50 | http://github.com/mattsears/aerial 51 | 52 | and cloned with: 53 | 54 | $ git clone git://github.com/mattsears/aerial.git 55 | $ rake launch 56 | # Navigate to 57 | 58 | ## Requirements ############################################################# 59 | 60 | * sinatra (for awesomeness) 61 | * git (http://git-scm.com) 62 | * grit (interface to git) 63 | * yaml (for configuration) 64 | * rdiscount (markdown-to-html) 65 | * Haml (can easily be switch to erb, or whatever) 66 | 67 | ## Todo ##################################################################### 68 | 69 | * Improve bootstrap tasks 70 | * Add article limit setting to config.yml 71 | * Support atom feeds 72 | * Add support for including non article content (pages) 73 | * Add more details to this README 74 | 75 | ## License ################################################################### 76 | 77 | Aerial is Copyright © 2010 Matt Sears, Littlelines. It is free software, 78 | and may be redistributed under the terms specified in the MIT-LICENSE file. 79 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler' 3 | Bundler::GemHelper.install_tasks 4 | 5 | # require 'spec/version' 6 | # require 'spec/rake/spectask' 7 | # require 'git' 8 | # require 'grit' 9 | # TODO: refactor config file loading in base.rb 10 | # require 'lib/aerial/config' 11 | 12 | # # Rspec setup 13 | # desc "Run all specs" 14 | # Spec::Rake::SpecTask.new do |t| 15 | # t.spec_files = FileList['spec/**/*_spec.rb'] 16 | # end 17 | 18 | # namespace :spec do 19 | # desc "Run all specs with rcov" 20 | # Spec::Rake::SpecTask.new('rcov') do |t| 21 | # t.spec_files = FileList['spec/**/*_spec.rb'] 22 | # t.rcov = true 23 | # t.rcov_dir = 'coverage' 24 | # t.rcov_opts = ['--exclude', 25 | # "lib/spec.rb,spec\/spec,bin\/spec,examples,\.autotest,#{Gem.path.join(',')}"] 26 | # end 27 | # end 28 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.2.0 2 | -------------------------------------------------------------------------------- /aerial.gemspec: -------------------------------------------------------------------------------- 1 | # aerial.gemspec 2 | # -*- encoding: utf-8 -*- 3 | $:.push File.expand_path("../lib", __FILE__) 4 | require "aerial/version" 5 | 6 | Gem::Specification.new do |s| 7 | s.name = "aerial" 8 | s.version = Aerial::VERSION 9 | s.platform = Gem::Platform::RUBY 10 | s.authors = ["Matt Sears"] 11 | s.email = ["matt@mattsears.com"] 12 | s.homepage = "http://mattsears.com" 13 | s.summary = %q{Aerial} 14 | s.description = %q{A simple, blogish software build with Sinatra, jQuery, and uses Git for data storage} 15 | s.add_development_dependency "rspec" 16 | # s.add_dependency 'grit', '> 0.1', '<= 0.5' 17 | s.add_dependency 'grit' 18 | s.add_dependency 'thor' 19 | s.add_dependency 'thin' 20 | s.add_dependency 'sinatra' 21 | s.add_dependency 'haml' 22 | s.add_dependency 'redcarpet', '1.17.2' 23 | s.add_dependency 'albino' 24 | s.add_dependency 'nokogiri' 25 | s.add_dependency 'html_truncator' 26 | 27 | s.rubyforge_project = "aerial" 28 | 29 | s.files = `git ls-files`.split("\n") 30 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 31 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 32 | s.require_paths = ["lib"] 33 | end 34 | -------------------------------------------------------------------------------- /articles/congratulations/congratulations-aerial-is-configured-correctly.article: -------------------------------------------------------------------------------- 1 | Title : Congratulations! Aerial is configured correctly 2 | Tags : ruby, sinatra, git, aerial 3 | Publish Date : 03/31/2009 4 | Author : Aerial 5 | 6 | Congratulations! Aerial appears to be up and running. This is a sample article created during the bootstrap process. You may overwrite this article or create a new one. 7 | -------------------------------------------------------------------------------- /bin/aerial: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $LOAD_PATH << File.join(File.dirname(__FILE__), '..') 3 | require 'rubygems' 4 | require File.dirname(__FILE__) + "/../lib/aerial/installer" 5 | require File.dirname(__FILE__) + "/../lib/aerial/build" 6 | Aerial::Installer.start 7 | Aerial::Build.start 8 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require File.join(File.dirname('.'), "lib", "aerial.rb") 4 | 5 | env = ENV['RACK_ENV'].to_sym if ENV['RACK_ENV'] 6 | root = File.dirname(__FILE__) 7 | 8 | # Load configuration and initialize Aerial 9 | Aerial.new(root, "/config/config.yml") 10 | 11 | # You probably don't want to edit anything below 12 | Aerial::App.set :environment, ENV["RACK_ENV"] || :development 13 | Aerial::App.set :port, 4567 14 | Aerial::App.set :cache_enabled, env == :production ? true : false 15 | Aerial::App.set :cache_page_extension, '.html' 16 | Aerial::App.set :cache_output_dir, '' 17 | Aerial::App.set :root, root 18 | 19 | run Aerial::App 20 | -------------------------------------------------------------------------------- /config/config.sample.ru: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "aerial" 4 | 5 | env = ENV['RACK_ENV'].to_sym 6 | root = File.dirname(__FILE__) 7 | 8 | # Load configuration and initialize Aerial 9 | Aerial.new(root, "/config.yml") 10 | 11 | # You probably don't want to edit anything below 12 | Aerial::App.set :environment, ENV["RACK_ENV"] || :production 13 | Aerial::App.set :port, 4567 14 | Aerial::App.set :cache_enabled, env == :production ? true : false 15 | Aerial::App.set :cache_page_extension, '.html' 16 | Aerial::App.set :cache_output_dir, '' 17 | Aerial::App.set :root, root 18 | 19 | run Aerial::App 20 | -------------------------------------------------------------------------------- /config/config.sample.yml: -------------------------------------------------------------------------------- 1 | # Title and Subtitle of the Site Aerail will be running 2 | title: "Aerial" 3 | subtitle: "Friendly " 4 | 5 | # Information about you 6 | name: "Awesome Ruby Developer" 7 | author: "Not sure what this is for?" 8 | email: "aerial@example.com" 9 | 10 | # This is where you articles are stored 11 | articles: 12 | dir: "articles" 13 | 14 | # Images, stylesheets, javascripts, etc 15 | public: 16 | dir: "public" 17 | 18 | # Pages 19 | views: 20 | dir: "app/views" 21 | default: "home" 22 | 23 | # Check for span when folks add comments to your blog 24 | akismet: 25 | key: "" 26 | url: "" 27 | 28 | # Path to the Aerial log file 29 | :log: /var/log/aerial.log 30 | -------------------------------------------------------------------------------- /config/config.test.yml: -------------------------------------------------------------------------------- 1 | title: "Aerial" 2 | subtitle: "Articles, Pages, and Such" 3 | name: "Aerial" 4 | author: "Awesome Ruby Developer" 5 | email: "aerial@example.com" 6 | 7 | # You can add any meta element pairs here 8 | # name => content 9 | meta: 10 | description: "describe this site" 11 | keywords: "aerial,git,sinatra,jquery" 12 | 13 | public: 14 | dir: "public" 15 | 16 | views: 17 | dir: "app/views" 18 | default: "home" 19 | 20 | # Content repository 21 | git: 22 | url: "git://github.com/jrobeson/aerial-sample-content.git" 23 | path: "spec/repo" 24 | name: "origin" 25 | branch: "master" 26 | 27 | # If you want to add synchronizing via Github post-receive hooks, 28 | # insert some secure password here. Then set a "Post-Receive URL" 29 | # in Github administration to http://{YOUR SERVER}/sync?token={WHAT YOU SET BELOW} 30 | github_token: ~ 31 | 32 | # Akismet spam protection. Get your key at http://akismet.com 33 | akismet: 34 | key: "" 35 | url: "" 36 | 37 | # http://antispam.typepad.com/info/developers.html 38 | typekey_antispam: 39 | key: "" 40 | url: "" 41 | 42 | # You should not need to modify anything below this line 43 | 44 | # Directory where articles are stored inside your content repository 45 | articles: 46 | dir: "articles" 47 | env: 48 | # Path to git or other required executables (separated by colons) 49 | path: "" 50 | -------------------------------------------------------------------------------- /config/config.yml: -------------------------------------------------------------------------------- 1 | title: "Aerial" 2 | subtitle: "" 3 | name: "" 4 | author: "" 5 | email: "" 6 | 7 | articles: 8 | dir: "articles" 9 | 10 | public: 11 | dir: "public" 12 | 13 | views: 14 | dir: "views" 15 | default: "home" 16 | 17 | akismet: 18 | key: "" 19 | url: "" 20 | -------------------------------------------------------------------------------- /config/deploy.rb: -------------------------------------------------------------------------------- 1 | # ============================================================================= 2 | # VLAD VARIABLES 3 | # ============================================================================= 4 | 5 | set :application, "" 6 | set :repository, "" 7 | set :deploy_to, "" 8 | set :user, "" 9 | set :domain, "" 10 | set :app_command, "/etc/init.d/apache2" 11 | 12 | desc 'Deploy the app!' 13 | task :deploy do 14 | Rake::Task["vlad:update"].invoke 15 | Rake::Task["vlad:setup_repo"].invoke 16 | Rake::Task["vlad:update_config"].invoke 17 | end 18 | 19 | desc 'Sync local and production code' 20 | remote_task :remote_pull do 21 | run "cd #{current_release}; #{git_cmd} pull origin master" 22 | end 23 | 24 | namespace :vlad do 25 | 26 | desc 'Restart Passenger' 27 | remote_task :start_app do 28 | run "touch #{current_release}/tmp/restart.txt" 29 | end 30 | 31 | desc 'Restarts the apache servers' 32 | remote_task :start_web do 33 | run "sudo #{app_command} restart" 34 | end 35 | 36 | desc 'Copy the git repo over' 37 | remote_task :setup_repo do 38 | run "cp -fR #{scm_path}/repo/.git #{current_release}/. " 39 | run "cd #{current_release}; #{git_cmd} checkout master" 40 | end 41 | 42 | desc 'Upload the configuration script' 43 | remote_task :update_config do 44 | run "mkdir #{current_release}/config" rescue nil 45 | rsync "config/config.yml", "#{current_release}/config/config.yml" 46 | end 47 | 48 | end 49 | 50 | 51 | -------------------------------------------------------------------------------- /config/thin.sample.yml: -------------------------------------------------------------------------------- 1 | --- 2 | environment: development 3 | chdir: /apps/aerial 4 | address: 127.0.0.1 5 | port: 4567 6 | pid: /apps/aerial/thin.pid 7 | rackup: /apps/aerial/config.ru 8 | log: /apps/aerial/log/thin.log 9 | max_conns: 1024 10 | timeout: 30 11 | max_persistent_conns: 512 12 | daemonize: false 13 | servers: 1 14 | -------------------------------------------------------------------------------- /examples/articles/congratulations/congratulations-aerial-is-configured-correctly.article: -------------------------------------------------------------------------------- 1 | Title : Congratulations! Aerial is configured correctly 2 | Tags : ruby, sinatra, git, aerial 3 | Publish Date : 03/31/2009 4 | Author : Aerial 5 | 6 | Congratulations! Aerial appears to be up and running. This is a sample article created during the bootstrap process. You may overwrite this article or create a new one. 7 | -------------------------------------------------------------------------------- /examples/public/javascripts/application.js: -------------------------------------------------------------------------------- 1 | /*------------------------------------ 2 | Global Javascripts 3 | Aerial 4 | Version / 1.0 5 | Author / att Sears 6 | email / matt@mattsears.com 7 | website / www.mattsears.com 8 | -------------------------------------*/ 9 | 10 | /* When page is loaded 11 | ----------------------------*/ 12 | $(document).ready(function() { 13 | externalLinks(); 14 | }); 15 | 16 | // 17 | var Comment = { 18 | 19 | author: '', 20 | homepage: '', 21 | email: '', 22 | body: '', 23 | article: '', 24 | 25 | // Submit a new comment to the server via ajax 26 | submit: function(article_id) { 27 | 28 | this.author = $("input#comment_author").val(); 29 | this.homepage = $("input#comment_website").val(); 30 | this.email = $("input#comment_email").val(); 31 | this.body = $("textarea#comment_body").val(); 32 | this.article = article_id; 33 | 34 | // Make sure we have the required fields 35 | if (!this.valid()){ 36 | return false; 37 | } 38 | 39 | // Append a new comment if post is successful 40 | if (this.post()){ 41 | this.appendNew(); 42 | } 43 | }, 44 | 45 | // Post the comment back to the server 46 | post: function() { 47 | 48 | // Data posted to server 49 | var data = 'author='+ this.author + '&email=' + this.email + '&homepage=' + this.phone + '&body=' + this.body; 50 | var url = "/article/" + this.article + "/comments"; 51 | 52 | $.ajax({ 53 | type: "POST", 54 | url: url, 55 | data: data 56 | }); 57 | 58 | return true; 59 | }, 60 | 61 | // Add a div for the new comment 62 | appendNew: function() { 63 | 64 | // Template for the new comment div 65 | var t = $.template( 66 | "

${author}${date}

${message}

" 67 | ); 68 | 69 | // Append 70 | $("#comments").append( t , { 71 | author: this.author, 72 | homepage: this.homepage, 73 | message: this.body 74 | }); 75 | }, 76 | 77 | // Ensure all required fields are filled-in 78 | valid: function() { 79 | 80 | if (this.author == "") { 81 | $("#author_label").addClass("error"); 82 | return false; 83 | } 84 | 85 | if (this.email == "") { 86 | $("#email_label").addClass("error"); 87 | return false; 88 | } 89 | 90 | if (this.comment == "") { 91 | $("#comment_label").addClass("error"); 92 | return false; 93 | } 94 | return true; 95 | }, 96 | 97 | } 98 | 99 | // Make all 'external' links in a new window 100 | function externalLinks() { 101 | if (!document.getElementsByTagName) return; 102 | var anchors = document.getElementsByTagName("a"); 103 | for (var i = 0; i < anchors.length; i++) { 104 | var anchor = anchors[i]; 105 | if (anchor.getAttribute("href") && 106 | anchor.getAttribute("rel") == "external") 107 | anchor.target = "_blank"; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /examples/public/javascripts/jquery-1.3.1.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery JavaScript Library v1.3.1 3 | * http://jquery.com/ 4 | * 5 | * Copyright (c) 2009 John Resig 6 | * Dual licensed under the MIT and GPL licenses. 7 | * http://docs.jquery.com/License 8 | * 9 | * Date: 2009-01-21 20:42:16 -0500 (Wed, 21 Jan 2009) 10 | * Revision: 6158 11 | */ 12 | (function(){var l=this,g,y=l.jQuery,p=l.$,o=l.jQuery=l.$=function(E,F){return new o.fn.init(E,F)},D=/^[^<]*(<(.|\s)+>)[^>]*$|^#([\w-]+)$/,f=/^.[^:#\[\.,]*$/;o.fn=o.prototype={init:function(E,H){E=E||document;if(E.nodeType){this[0]=E;this.length=1;this.context=E;return this}if(typeof E==="string"){var G=D.exec(E);if(G&&(G[1]||!H)){if(G[1]){E=o.clean([G[1]],H)}else{var I=document.getElementById(G[3]);if(I&&I.id!=G[3]){return o().find(E)}var F=o(I||[]);F.context=document;F.selector=E;return F}}else{return o(H).find(E)}}else{if(o.isFunction(E)){return o(document).ready(E)}}if(E.selector&&E.context){this.selector=E.selector;this.context=E.context}return this.setArray(o.makeArray(E))},selector:"",jquery:"1.3.1",size:function(){return this.length},get:function(E){return E===g?o.makeArray(this):this[E]},pushStack:function(F,H,E){var G=o(F);G.prevObject=this;G.context=this.context;if(H==="find"){G.selector=this.selector+(this.selector?" ":"")+E}else{if(H){G.selector=this.selector+"."+H+"("+E+")"}}return G},setArray:function(E){this.length=0;Array.prototype.push.apply(this,E);return this},each:function(F,E){return o.each(this,F,E)},index:function(E){return o.inArray(E&&E.jquery?E[0]:E,this)},attr:function(F,H,G){var E=F;if(typeof F==="string"){if(H===g){return this[0]&&o[G||"attr"](this[0],F)}else{E={};E[F]=H}}return this.each(function(I){for(F in E){o.attr(G?this.style:this,F,o.prop(this,E[F],G,I,F))}})},css:function(E,F){if((E=="width"||E=="height")&&parseFloat(F)<0){F=g}return this.attr(E,F,"curCSS")},text:function(F){if(typeof F!=="object"&&F!=null){return this.empty().append((this[0]&&this[0].ownerDocument||document).createTextNode(F))}var E="";o.each(F||this,function(){o.each(this.childNodes,function(){if(this.nodeType!=8){E+=this.nodeType!=1?this.nodeValue:o.fn.text([this])}})});return E},wrapAll:function(E){if(this[0]){var F=o(E,this[0].ownerDocument).clone();if(this[0].parentNode){F.insertBefore(this[0])}F.map(function(){var G=this;while(G.firstChild){G=G.firstChild}return G}).append(this)}return this},wrapInner:function(E){return this.each(function(){o(this).contents().wrapAll(E)})},wrap:function(E){return this.each(function(){o(this).wrapAll(E)})},append:function(){return this.domManip(arguments,true,function(E){if(this.nodeType==1){this.appendChild(E)}})},prepend:function(){return this.domManip(arguments,true,function(E){if(this.nodeType==1){this.insertBefore(E,this.firstChild)}})},before:function(){return this.domManip(arguments,false,function(E){this.parentNode.insertBefore(E,this)})},after:function(){return this.domManip(arguments,false,function(E){this.parentNode.insertBefore(E,this.nextSibling)})},end:function(){return this.prevObject||o([])},push:[].push,find:function(E){if(this.length===1&&!/,/.test(E)){var G=this.pushStack([],"find",E);G.length=0;o.find(E,this[0],G);return G}else{var F=o.map(this,function(H){return o.find(E,H)});return this.pushStack(/[^+>] [^+>]/.test(E)?o.unique(F):F,"find",E)}},clone:function(F){var E=this.map(function(){if(!o.support.noCloneEvent&&!o.isXMLDoc(this)){var I=this.cloneNode(true),H=document.createElement("div");H.appendChild(I);return o.clean([H.innerHTML])[0]}else{return this.cloneNode(true)}});var G=E.find("*").andSelf().each(function(){if(this[h]!==g){this[h]=null}});if(F===true){this.find("*").andSelf().each(function(I){if(this.nodeType==3){return}var H=o.data(this,"events");for(var K in H){for(var J in H[K]){o.event.add(G[I],K,H[K][J],H[K][J].data)}}})}return E},filter:function(E){return this.pushStack(o.isFunction(E)&&o.grep(this,function(G,F){return E.call(G,F)})||o.multiFilter(E,o.grep(this,function(F){return F.nodeType===1})),"filter",E)},closest:function(E){var F=o.expr.match.POS.test(E)?o(E):null;return this.map(function(){var G=this;while(G&&G.ownerDocument){if(F?F.index(G)>-1:o(G).is(E)){return G}G=G.parentNode}})},not:function(E){if(typeof E==="string"){if(f.test(E)){return this.pushStack(o.multiFilter(E,this,true),"not",E)}else{E=o.multiFilter(E,this)}}var F=E.length&&E[E.length-1]!==g&&!E.nodeType;return this.filter(function(){return F?o.inArray(this,E)<0:this!=E})},add:function(E){return this.pushStack(o.unique(o.merge(this.get(),typeof E==="string"?o(E):o.makeArray(E))))},is:function(E){return !!E&&o.multiFilter(E,this).length>0},hasClass:function(E){return !!E&&this.is("."+E)},val:function(K){if(K===g){var E=this[0];if(E){if(o.nodeName(E,"option")){return(E.attributes.value||{}).specified?E.value:E.text}if(o.nodeName(E,"select")){var I=E.selectedIndex,L=[],M=E.options,H=E.type=="select-one";if(I<0){return null}for(var F=H?I:0,J=H?I+1:M.length;F=0||o.inArray(this.name,K)>=0)}else{if(o.nodeName(this,"select")){var N=o.makeArray(K);o("option",this).each(function(){this.selected=(o.inArray(this.value,N)>=0||o.inArray(this.text,N)>=0)});if(!N.length){this.selectedIndex=-1}}else{this.value=K}}})},html:function(E){return E===g?(this[0]?this[0].innerHTML:null):this.empty().append(E)},replaceWith:function(E){return this.after(E).remove()},eq:function(E){return this.slice(E,+E+1)},slice:function(){return this.pushStack(Array.prototype.slice.apply(this,arguments),"slice",Array.prototype.slice.call(arguments).join(","))},map:function(E){return this.pushStack(o.map(this,function(G,F){return E.call(G,F,G)}))},andSelf:function(){return this.add(this.prevObject)},domManip:function(K,N,M){if(this[0]){var J=(this[0].ownerDocument||this[0]).createDocumentFragment(),G=o.clean(K,(this[0].ownerDocument||this[0]),J),I=J.firstChild,E=this.length>1?J.cloneNode(true):J;if(I){for(var H=0,F=this.length;H0?E.cloneNode(true):J)}}if(G){o.each(G,z)}}return this;function L(O,P){return N&&o.nodeName(O,"table")&&o.nodeName(P,"tr")?(O.getElementsByTagName("tbody")[0]||O.appendChild(O.ownerDocument.createElement("tbody"))):O}}};o.fn.init.prototype=o.fn;function z(E,F){if(F.src){o.ajax({url:F.src,async:false,dataType:"script"})}else{o.globalEval(F.text||F.textContent||F.innerHTML||"")}if(F.parentNode){F.parentNode.removeChild(F)}}function e(){return +new Date}o.extend=o.fn.extend=function(){var J=arguments[0]||{},H=1,I=arguments.length,E=false,G;if(typeof J==="boolean"){E=J;J=arguments[1]||{};H=2}if(typeof J!=="object"&&!o.isFunction(J)){J={}}if(I==H){J=this;--H}for(;H-1}},swap:function(H,G,I){var E={};for(var F in G){E[F]=H.style[F];H.style[F]=G[F]}I.call(H);for(var F in G){H.style[F]=E[F]}},css:function(G,E,I){if(E=="width"||E=="height"){var K,F={position:"absolute",visibility:"hidden",display:"block"},J=E=="width"?["Left","Right"]:["Top","Bottom"];function H(){K=E=="width"?G.offsetWidth:G.offsetHeight;var M=0,L=0;o.each(J,function(){M+=parseFloat(o.curCSS(G,"padding"+this,true))||0;L+=parseFloat(o.curCSS(G,"border"+this+"Width",true))||0});K-=Math.round(M+L)}if(o(G).is(":visible")){H()}else{o.swap(G,F,H)}return Math.max(0,K)}return o.curCSS(G,E,I)},curCSS:function(I,F,G){var L,E=I.style;if(F=="opacity"&&!o.support.opacity){L=o.attr(E,"opacity");return L==""?"1":L}if(F.match(/float/i)){F=w}if(!G&&E&&E[F]){L=E[F]}else{if(q.getComputedStyle){if(F.match(/float/i)){F="float"}F=F.replace(/([A-Z])/g,"-$1").toLowerCase();var M=q.getComputedStyle(I,null);if(M){L=M.getPropertyValue(F)}if(F=="opacity"&&L==""){L="1"}}else{if(I.currentStyle){var J=F.replace(/\-(\w)/g,function(N,O){return O.toUpperCase()});L=I.currentStyle[F]||I.currentStyle[J];if(!/^\d+(px)?$/i.test(L)&&/^\d/.test(L)){var H=E.left,K=I.runtimeStyle.left;I.runtimeStyle.left=I.currentStyle.left;E.left=L||0;L=E.pixelLeft+"px";E.left=H;I.runtimeStyle.left=K}}}}return L},clean:function(F,K,I){K=K||document;if(typeof K.createElement==="undefined"){K=K.ownerDocument||K[0]&&K[0].ownerDocument||document}if(!I&&F.length===1&&typeof F[0]==="string"){var H=/^<(\w+)\s*\/?>$/.exec(F[0]);if(H){return[K.createElement(H[1])]}}var G=[],E=[],L=K.createElement("div");o.each(F,function(P,R){if(typeof R==="number"){R+=""}if(!R){return}if(typeof R==="string"){R=R.replace(/(<(\w+)[^>]*?)\/>/g,function(T,U,S){return S.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i)?T:U+">"});var O=o.trim(R).toLowerCase();var Q=!O.indexOf("",""]||!O.indexOf("",""]||O.match(/^<(thead|tbody|tfoot|colg|cap)/)&&[1,"","
"]||!O.indexOf("",""]||(!O.indexOf("",""]||!O.indexOf("",""]||!o.support.htmlSerialize&&[1,"div
","
"]||[0,"",""];L.innerHTML=Q[1]+R+Q[2];while(Q[0]--){L=L.lastChild}if(!o.support.tbody){var N=!O.indexOf(""&&O.indexOf("=0;--M){if(o.nodeName(N[M],"tbody")&&!N[M].childNodes.length){N[M].parentNode.removeChild(N[M])}}}if(!o.support.leadingWhitespace&&/^\s/.test(R)){L.insertBefore(K.createTextNode(R.match(/^\s*/)[0]),L.firstChild)}R=o.makeArray(L.childNodes)}if(R.nodeType){G.push(R)}else{G=o.merge(G,R)}});if(I){for(var J=0;G[J];J++){if(o.nodeName(G[J],"script")&&(!G[J].type||G[J].type.toLowerCase()==="text/javascript")){E.push(G[J].parentNode?G[J].parentNode.removeChild(G[J]):G[J])}else{if(G[J].nodeType===1){G.splice.apply(G,[J+1,0].concat(o.makeArray(G[J].getElementsByTagName("script"))))}I.appendChild(G[J])}}return E}return G},attr:function(J,G,K){if(!J||J.nodeType==3||J.nodeType==8){return g}var H=!o.isXMLDoc(J),L=K!==g;G=H&&o.props[G]||G;if(J.tagName){var F=/href|src|style/.test(G);if(G=="selected"&&J.parentNode){J.parentNode.selectedIndex}if(G in J&&H&&!F){if(L){if(G=="type"&&o.nodeName(J,"input")&&J.parentNode){throw"type property can't be changed"}J[G]=K}if(o.nodeName(J,"form")&&J.getAttributeNode(G)){return J.getAttributeNode(G).nodeValue}if(G=="tabIndex"){var I=J.getAttributeNode("tabIndex");return I&&I.specified?I.value:J.nodeName.match(/(button|input|object|select|textarea)/i)?0:J.nodeName.match(/^(a|area)$/i)&&J.href?0:g}return J[G]}if(!o.support.style&&H&&G=="style"){return o.attr(J.style,"cssText",K)}if(L){J.setAttribute(G,""+K)}var E=!o.support.hrefNormalized&&H&&F?J.getAttribute(G,2):J.getAttribute(G);return E===null?g:E}if(!o.support.opacity&&G=="opacity"){if(L){J.zoom=1;J.filter=(J.filter||"").replace(/alpha\([^)]*\)/,"")+(parseInt(K)+""=="NaN"?"":"alpha(opacity="+K*100+")")}return J.filter&&J.filter.indexOf("opacity=")>=0?(parseFloat(J.filter.match(/opacity=([^)]*)/)[1])/100)+"":""}G=G.replace(/-([a-z])/ig,function(M,N){return N.toUpperCase()});if(L){J[G]=K}return J[G]},trim:function(E){return(E||"").replace(/^\s+|\s+$/g,"")},makeArray:function(G){var E=[];if(G!=null){var F=G.length;if(F==null||typeof G==="string"||o.isFunction(G)||G.setInterval){E[0]=G}else{while(F){E[--F]=G[F]}}}return E},inArray:function(G,H){for(var E=0,F=H.length;E*",this).remove();while(this.firstChild){this.removeChild(this.firstChild)}}},function(E,F){o.fn[E]=function(){return this.each(F,arguments)}});function j(E,F){return E[0]&&parseInt(o.curCSS(E[0],F,true),10)||0}var h="jQuery"+e(),v=0,A={};o.extend({cache:{},data:function(F,E,G){F=F==l?A:F;var H=F[h];if(!H){H=F[h]=++v}if(E&&!o.cache[H]){o.cache[H]={}}if(G!==g){o.cache[H][E]=G}return E?o.cache[H][E]:H},removeData:function(F,E){F=F==l?A:F;var H=F[h];if(E){if(o.cache[H]){delete o.cache[H][E];E="";for(E in o.cache[H]){break}if(!E){o.removeData(F)}}}else{try{delete F[h]}catch(G){if(F.removeAttribute){F.removeAttribute(h)}}delete o.cache[H]}},queue:function(F,E,H){if(F){E=(E||"fx")+"queue";var G=o.data(F,E);if(!G||o.isArray(H)){G=o.data(F,E,o.makeArray(H))}else{if(H){G.push(H)}}}return G},dequeue:function(H,G){var E=o.queue(H,G),F=E.shift();if(!G||G==="fx"){F=E[0]}if(F!==g){F.call(H)}}});o.fn.extend({data:function(E,G){var H=E.split(".");H[1]=H[1]?"."+H[1]:"";if(G===g){var F=this.triggerHandler("getData"+H[1]+"!",[H[0]]);if(F===g&&this.length){F=o.data(this[0],E)}return F===g&&H[1]?this.data(H[0]):F}else{return this.trigger("setData"+H[1]+"!",[H[0],G]).each(function(){o.data(this,E,G)})}},removeData:function(E){return this.each(function(){o.removeData(this,E)})},queue:function(E,F){if(typeof E!=="string"){F=E;E="fx"}if(F===g){return o.queue(this[0],E)}return this.each(function(){var G=o.queue(this,E,F);if(E=="fx"&&G.length==1){G[0].call(this)}})},dequeue:function(E){return this.each(function(){o.dequeue(this,E)})}}); 13 | /* 14 | * Sizzle CSS Selector Engine - v0.9.3 15 | * Copyright 2009, The Dojo Foundation 16 | * Released under the MIT, BSD, and GPL Licenses. 17 | * More information: http://sizzlejs.com/ 18 | */ 19 | (function(){var Q=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]+['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[]+)+|[>+~])(\s*,\s*)?/g,K=0,G=Object.prototype.toString;var F=function(X,T,aa,ab){aa=aa||[];T=T||document;if(T.nodeType!==1&&T.nodeType!==9){return[]}if(!X||typeof X!=="string"){return aa}var Y=[],V,ae,ah,S,ac,U,W=true;Q.lastIndex=0;while((V=Q.exec(X))!==null){Y.push(V[1]);if(V[2]){U=RegExp.rightContext;break}}if(Y.length>1&&L.exec(X)){if(Y.length===2&&H.relative[Y[0]]){ae=I(Y[0]+Y[1],T)}else{ae=H.relative[Y[0]]?[T]:F(Y.shift(),T);while(Y.length){X=Y.shift();if(H.relative[X]){X+=Y.shift()}ae=I(X,ae)}}}else{var ad=ab?{expr:Y.pop(),set:E(ab)}:F.find(Y.pop(),Y.length===1&&T.parentNode?T.parentNode:T,P(T));ae=F.filter(ad.expr,ad.set);if(Y.length>0){ah=E(ae)}else{W=false}while(Y.length){var ag=Y.pop(),af=ag;if(!H.relative[ag]){ag=""}else{af=Y.pop()}if(af==null){af=T}H.relative[ag](ah,af,P(T))}}if(!ah){ah=ae}if(!ah){throw"Syntax error, unrecognized expression: "+(ag||X)}if(G.call(ah)==="[object Array]"){if(!W){aa.push.apply(aa,ah)}else{if(T.nodeType===1){for(var Z=0;ah[Z]!=null;Z++){if(ah[Z]&&(ah[Z]===true||ah[Z].nodeType===1&&J(T,ah[Z]))){aa.push(ae[Z])}}}else{for(var Z=0;ah[Z]!=null;Z++){if(ah[Z]&&ah[Z].nodeType===1){aa.push(ae[Z])}}}}}else{E(ah,aa)}if(U){F(U,T,aa,ab)}return aa};F.matches=function(S,T){return F(S,null,null,T)};F.find=function(Z,S,aa){var Y,W;if(!Z){return[]}for(var V=0,U=H.order.length;V":function(X,T,Y){if(typeof T==="string"&&!/\W/.test(T)){T=Y?T:T.toUpperCase();for(var U=0,S=X.length;U=0){if(!U){S.push(X)}}else{if(U){T[W]=false}}}}return false},ID:function(S){return S[1].replace(/\\/g,"")},TAG:function(T,S){for(var U=0;S[U]===false;U++){}return S[U]&&P(S[U])?T[1]:T[1].toUpperCase()},CHILD:function(S){if(S[1]=="nth"){var T=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(S[2]=="even"&&"2n"||S[2]=="odd"&&"2n+1"||!/\D/.test(S[2])&&"0n+"+S[2]||S[2]);S[2]=(T[1]+(T[2]||1))-0;S[3]=T[3]-0}S[0]="done"+(K++);return S},ATTR:function(T){var S=T[1].replace(/\\/g,"");if(H.attrMap[S]){T[1]=H.attrMap[S]}if(T[2]==="~="){T[4]=" "+T[4]+" "}return T},PSEUDO:function(W,T,U,S,X){if(W[1]==="not"){if(W[3].match(Q).length>1){W[3]=F(W[3],null,null,T)}else{var V=F.filter(W[3],T,U,true^X);if(!U){S.push.apply(S,V)}return false}}else{if(H.match.POS.test(W[0])){return true}}return W},POS:function(S){S.unshift(true);return S}},filters:{enabled:function(S){return S.disabled===false&&S.type!=="hidden"},disabled:function(S){return S.disabled===true},checked:function(S){return S.checked===true},selected:function(S){S.parentNode.selectedIndex;return S.selected===true},parent:function(S){return !!S.firstChild},empty:function(S){return !S.firstChild},has:function(U,T,S){return !!F(S[3],U).length},header:function(S){return/h\d/i.test(S.nodeName)},text:function(S){return"text"===S.type},radio:function(S){return"radio"===S.type},checkbox:function(S){return"checkbox"===S.type},file:function(S){return"file"===S.type},password:function(S){return"password"===S.type},submit:function(S){return"submit"===S.type},image:function(S){return"image"===S.type},reset:function(S){return"reset"===S.type},button:function(S){return"button"===S.type||S.nodeName.toUpperCase()==="BUTTON"},input:function(S){return/input|select|textarea|button/i.test(S.nodeName)}},setFilters:{first:function(T,S){return S===0},last:function(U,T,S,V){return T===V.length-1},even:function(T,S){return S%2===0},odd:function(T,S){return S%2===1},lt:function(U,T,S){return TS[3]-0},nth:function(U,T,S){return S[3]-0==T},eq:function(U,T,S){return S[3]-0==T}},filter:{CHILD:function(S,V){var Y=V[1],Z=S.parentNode;var X=V[0];if(Z&&(!Z[X]||!S.nodeIndex)){var W=1;for(var T=Z.firstChild;T;T=T.nextSibling){if(T.nodeType==1){T.nodeIndex=W++}}Z[X]=W-1}if(Y=="first"){return S.nodeIndex==1}else{if(Y=="last"){return S.nodeIndex==Z[X]}else{if(Y=="only"){return Z[X]==1}else{if(Y=="nth"){var ab=false,U=V[2],aa=V[3];if(U==1&&aa==0){return true}if(U==0){if(S.nodeIndex==aa){ab=true}}else{if((S.nodeIndex-aa)%U==0&&(S.nodeIndex-aa)/U>=0){ab=true}}return ab}}}}},PSEUDO:function(Y,U,V,Z){var T=U[1],W=H.filters[T];if(W){return W(Y,V,U,Z)}else{if(T==="contains"){return(Y.textContent||Y.innerText||"").indexOf(U[3])>=0}else{if(T==="not"){var X=U[3];for(var V=0,S=X.length;V=0:V==="~="?(" "+X+" ").indexOf(T)>=0:!U[4]?S:V==="!="?X!=T:V==="^="?X.indexOf(T)===0:V==="$="?X.substr(X.length-T.length)===T:V==="|="?X===T||X.substr(0,T.length+1)===T+"-":false},POS:function(W,T,U,X){var S=T[2],V=H.setFilters[S];if(V){return V(W,U,T,X)}}}};var L=H.match.POS;for(var N in H.match){H.match[N]=RegExp(H.match[N].source+/(?![^\[]*\])(?![^\(]*\))/.source)}var E=function(T,S){T=Array.prototype.slice.call(T);if(S){S.push.apply(S,T);return S}return T};try{Array.prototype.slice.call(document.documentElement.childNodes)}catch(M){E=function(W,V){var T=V||[];if(G.call(W)==="[object Array]"){Array.prototype.push.apply(T,W)}else{if(typeof W.length==="number"){for(var U=0,S=W.length;U";var S=document.documentElement;S.insertBefore(T,S.firstChild);if(!!document.getElementById(U)){H.find.ID=function(W,X,Y){if(typeof X.getElementById!=="undefined"&&!Y){var V=X.getElementById(W[1]);return V?V.id===W[1]||typeof V.getAttributeNode!=="undefined"&&V.getAttributeNode("id").nodeValue===W[1]?[V]:g:[]}};H.filter.ID=function(X,V){var W=typeof X.getAttributeNode!=="undefined"&&X.getAttributeNode("id");return X.nodeType===1&&W&&W.nodeValue===V}}S.removeChild(T)})();(function(){var S=document.createElement("div");S.appendChild(document.createComment(""));if(S.getElementsByTagName("*").length>0){H.find.TAG=function(T,X){var W=X.getElementsByTagName(T[1]);if(T[1]==="*"){var V=[];for(var U=0;W[U];U++){if(W[U].nodeType===1){V.push(W[U])}}W=V}return W}}S.innerHTML="";if(S.firstChild&&S.firstChild.getAttribute("href")!=="#"){H.attrHandle.href=function(T){return T.getAttribute("href",2)}}})();if(document.querySelectorAll){(function(){var S=F,T=document.createElement("div");T.innerHTML="

";if(T.querySelectorAll&&T.querySelectorAll(".TEST").length===0){return}F=function(X,W,U,V){W=W||document;if(!V&&W.nodeType===9&&!P(W)){try{return E(W.querySelectorAll(X),U)}catch(Y){}}return S(X,W,U,V)};F.find=S.find;F.filter=S.filter;F.selectors=S.selectors;F.matches=S.matches})()}if(document.getElementsByClassName&&document.documentElement.getElementsByClassName){H.order.splice(1,0,"CLASS");H.find.CLASS=function(S,T){return T.getElementsByClassName(S[1])}}function O(T,Z,Y,ac,aa,ab){for(var W=0,U=ac.length;W0){W=S;break}}}S=S[T]}ab[V]=W}}}var J=document.compareDocumentPosition?function(T,S){return T.compareDocumentPosition(S)&16}:function(T,S){return T!==S&&(T.contains?T.contains(S):true)};var P=function(S){return S.nodeType===9&&S.documentElement.nodeName!=="HTML"||!!S.ownerDocument&&P(S.ownerDocument)};var I=function(S,Z){var V=[],W="",X,U=Z.nodeType?[Z]:Z;while((X=H.match.PSEUDO.exec(S))){W+=X[0];S=S.replace(H.match.PSEUDO,"")}S=H.relative[S]?S+"*":S;for(var Y=0,T=U.length;Y=0){I.type=G=G.slice(0,-1);I.exclusive=true}if(!H){I.stopPropagation();if(this.global[G]){o.each(o.cache,function(){if(this.events&&this.events[G]){o.event.trigger(I,K,this.handle.elem)}})}}if(!H||H.nodeType==3||H.nodeType==8){return g}I.result=g;I.target=H;K=o.makeArray(K);K.unshift(I)}I.currentTarget=H;var J=o.data(H,"handle");if(J){J.apply(H,K)}if((!H[G]||(o.nodeName(H,"a")&&G=="click"))&&H["on"+G]&&H["on"+G].apply(H,K)===false){I.result=false}if(!E&&H[G]&&!I.isDefaultPrevented()&&!(o.nodeName(H,"a")&&G=="click")){this.triggered=true;try{H[G]()}catch(L){}}this.triggered=false;if(!I.isPropagationStopped()){var F=H.parentNode||H.ownerDocument;if(F){o.event.trigger(I,K,F,true)}}},handle:function(K){var J,E;K=arguments[0]=o.event.fix(K||l.event);var L=K.type.split(".");K.type=L.shift();J=!L.length&&!K.exclusive;var I=RegExp("(^|\\.)"+L.slice().sort().join(".*\\.")+"(\\.|$)");E=(o.data(this,"events")||{})[K.type];for(var G in E){var H=E[G];if(J||I.test(H.type)){K.handler=H;K.data=H.data;var F=H.apply(this,arguments);if(F!==g){K.result=F;if(F===false){K.preventDefault();K.stopPropagation()}}if(K.isImmediatePropagationStopped()){break}}}},props:"altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode metaKey newValue originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),fix:function(H){if(H[h]){return H}var F=H;H=o.Event(F);for(var G=this.props.length,J;G;){J=this.props[--G];H[J]=F[J]}if(!H.target){H.target=H.srcElement||document}if(H.target.nodeType==3){H.target=H.target.parentNode}if(!H.relatedTarget&&H.fromElement){H.relatedTarget=H.fromElement==H.target?H.toElement:H.fromElement}if(H.pageX==null&&H.clientX!=null){var I=document.documentElement,E=document.body;H.pageX=H.clientX+(I&&I.scrollLeft||E&&E.scrollLeft||0)-(I.clientLeft||0);H.pageY=H.clientY+(I&&I.scrollTop||E&&E.scrollTop||0)-(I.clientTop||0)}if(!H.which&&((H.charCode||H.charCode===0)?H.charCode:H.keyCode)){H.which=H.charCode||H.keyCode}if(!H.metaKey&&H.ctrlKey){H.metaKey=H.ctrlKey}if(!H.which&&H.button){H.which=(H.button&1?1:(H.button&2?3:(H.button&4?2:0)))}return H},proxy:function(F,E){E=E||function(){return F.apply(this,arguments)};E.guid=F.guid=F.guid||E.guid||this.guid++;return E},special:{ready:{setup:B,teardown:function(){}}},specialAll:{live:{setup:function(E,F){o.event.add(this,F[0],c)},teardown:function(G){if(G.length){var E=0,F=RegExp("(^|\\.)"+G[0]+"(\\.|$)");o.each((o.data(this,"events").live||{}),function(){if(F.test(this.type)){E++}});if(E<1){o.event.remove(this,G[0],c)}}}}}};o.Event=function(E){if(!this.preventDefault){return new o.Event(E)}if(E&&E.type){this.originalEvent=E;this.type=E.type}else{this.type=E}this.timeStamp=e();this[h]=true};function k(){return false}function u(){return true}o.Event.prototype={preventDefault:function(){this.isDefaultPrevented=u;var E=this.originalEvent;if(!E){return}if(E.preventDefault){E.preventDefault()}E.returnValue=false},stopPropagation:function(){this.isPropagationStopped=u;var E=this.originalEvent;if(!E){return}if(E.stopPropagation){E.stopPropagation()}E.cancelBubble=true},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=u;this.stopPropagation()},isDefaultPrevented:k,isPropagationStopped:k,isImmediatePropagationStopped:k};var a=function(F){var E=F.relatedTarget;while(E&&E!=this){try{E=E.parentNode}catch(G){E=this}}if(E!=this){F.type=F.data;o.event.handle.apply(this,arguments)}};o.each({mouseover:"mouseenter",mouseout:"mouseleave"},function(F,E){o.event.special[E]={setup:function(){o.event.add(this,F,a,E)},teardown:function(){o.event.remove(this,F,a)}}});o.fn.extend({bind:function(F,G,E){return F=="unload"?this.one(F,G,E):this.each(function(){o.event.add(this,F,E||G,E&&G)})},one:function(G,H,F){var E=o.event.proxy(F||H,function(I){o(this).unbind(I,E);return(F||H).apply(this,arguments)});return this.each(function(){o.event.add(this,G,E,F&&H)})},unbind:function(F,E){return this.each(function(){o.event.remove(this,F,E)})},trigger:function(E,F){return this.each(function(){o.event.trigger(E,F,this)})},triggerHandler:function(E,G){if(this[0]){var F=o.Event(E);F.preventDefault();F.stopPropagation();o.event.trigger(F,G,this[0]);return F.result}},toggle:function(G){var E=arguments,F=1;while(F=0){var E=G.slice(I,G.length);G=G.slice(0,I)}var H="GET";if(J){if(o.isFunction(J)){K=J;J=null}else{if(typeof J==="object"){J=o.param(J);H="POST"}}}var F=this;o.ajax({url:G,type:H,dataType:"html",data:J,complete:function(M,L){if(L=="success"||L=="notmodified"){F.html(E?o("
").append(M.responseText.replace(//g,"")).find(E):M.responseText)}if(K){F.each(K,[M.responseText,L,M])}}});return this},serialize:function(){return o.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?o.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||/select|textarea/i.test(this.nodeName)||/text|hidden|password/i.test(this.type))}).map(function(E,F){var G=o(this).val();return G==null?null:o.isArray(G)?o.map(G,function(I,H){return{name:F.name,value:I}}):{name:F.name,value:G}}).get()}});o.each("ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend".split(","),function(E,F){o.fn[F]=function(G){return this.bind(F,G)}});var r=e();o.extend({get:function(E,G,H,F){if(o.isFunction(G)){H=G;G=null}return o.ajax({type:"GET",url:E,data:G,success:H,dataType:F})},getScript:function(E,F){return o.get(E,null,F,"script")},getJSON:function(E,F,G){return o.get(E,F,G,"json")},post:function(E,G,H,F){if(o.isFunction(G)){H=G;G={}}return o.ajax({type:"POST",url:E,data:G,success:H,dataType:F})},ajaxSetup:function(E){o.extend(o.ajaxSettings,E)},ajaxSettings:{url:location.href,global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:function(){return l.ActiveXObject?new ActiveXObject("Microsoft.XMLHTTP"):new XMLHttpRequest()},accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},ajax:function(M){M=o.extend(true,M,o.extend(true,{},o.ajaxSettings,M));var W,F=/=\?(&|$)/g,R,V,G=M.type.toUpperCase();if(M.data&&M.processData&&typeof M.data!=="string"){M.data=o.param(M.data)}if(M.dataType=="jsonp"){if(G=="GET"){if(!M.url.match(F)){M.url+=(M.url.match(/\?/)?"&":"?")+(M.jsonp||"callback")+"=?"}}else{if(!M.data||!M.data.match(F)){M.data=(M.data?M.data+"&":"")+(M.jsonp||"callback")+"=?"}}M.dataType="json"}if(M.dataType=="json"&&(M.data&&M.data.match(F)||M.url.match(F))){W="jsonp"+r++;if(M.data){M.data=(M.data+"").replace(F,"="+W+"$1")}M.url=M.url.replace(F,"="+W+"$1");M.dataType="script";l[W]=function(X){V=X;I();L();l[W]=g;try{delete l[W]}catch(Y){}if(H){H.removeChild(T)}}}if(M.dataType=="script"&&M.cache==null){M.cache=false}if(M.cache===false&&G=="GET"){var E=e();var U=M.url.replace(/(\?|&)_=.*?(&|$)/,"$1_="+E+"$2");M.url=U+((U==M.url)?(M.url.match(/\?/)?"&":"?")+"_="+E:"")}if(M.data&&G=="GET"){M.url+=(M.url.match(/\?/)?"&":"?")+M.data;M.data=null}if(M.global&&!o.active++){o.event.trigger("ajaxStart")}var Q=/^(\w+:)?\/\/([^\/?#]+)/.exec(M.url);if(M.dataType=="script"&&G=="GET"&&Q&&(Q[1]&&Q[1]!=location.protocol||Q[2]!=location.host)){var H=document.getElementsByTagName("head")[0];var T=document.createElement("script");T.src=M.url;if(M.scriptCharset){T.charset=M.scriptCharset}if(!W){var O=false;T.onload=T.onreadystatechange=function(){if(!O&&(!this.readyState||this.readyState=="loaded"||this.readyState=="complete")){O=true;I();L();H.removeChild(T)}}}H.appendChild(T);return g}var K=false;var J=M.xhr();if(M.username){J.open(G,M.url,M.async,M.username,M.password)}else{J.open(G,M.url,M.async)}try{if(M.data){J.setRequestHeader("Content-Type",M.contentType)}if(M.ifModified){J.setRequestHeader("If-Modified-Since",o.lastModified[M.url]||"Thu, 01 Jan 1970 00:00:00 GMT")}J.setRequestHeader("X-Requested-With","XMLHttpRequest");J.setRequestHeader("Accept",M.dataType&&M.accepts[M.dataType]?M.accepts[M.dataType]+", */*":M.accepts._default)}catch(S){}if(M.beforeSend&&M.beforeSend(J,M)===false){if(M.global&&!--o.active){o.event.trigger("ajaxStop")}J.abort();return false}if(M.global){o.event.trigger("ajaxSend",[J,M])}var N=function(X){if(J.readyState==0){if(P){clearInterval(P);P=null;if(M.global&&!--o.active){o.event.trigger("ajaxStop")}}}else{if(!K&&J&&(J.readyState==4||X=="timeout")){K=true;if(P){clearInterval(P);P=null}R=X=="timeout"?"timeout":!o.httpSuccess(J)?"error":M.ifModified&&o.httpNotModified(J,M.url)?"notmodified":"success";if(R=="success"){try{V=o.httpData(J,M.dataType,M)}catch(Z){R="parsererror"}}if(R=="success"){var Y;try{Y=J.getResponseHeader("Last-Modified")}catch(Z){}if(M.ifModified&&Y){o.lastModified[M.url]=Y}if(!W){I()}}else{o.handleError(M,J,R)}L();if(X){J.abort()}if(M.async){J=null}}}};if(M.async){var P=setInterval(N,13);if(M.timeout>0){setTimeout(function(){if(J&&!K){N("timeout")}},M.timeout)}}try{J.send(M.data)}catch(S){o.handleError(M,J,null,S)}if(!M.async){N()}function I(){if(M.success){M.success(V,R)}if(M.global){o.event.trigger("ajaxSuccess",[J,M])}}function L(){if(M.complete){M.complete(J,R)}if(M.global){o.event.trigger("ajaxComplete",[J,M])}if(M.global&&!--o.active){o.event.trigger("ajaxStop")}}return J},handleError:function(F,H,E,G){if(F.error){F.error(H,E,G)}if(F.global){o.event.trigger("ajaxError",[H,F,G])}},active:0,httpSuccess:function(F){try{return !F.status&&location.protocol=="file:"||(F.status>=200&&F.status<300)||F.status==304||F.status==1223}catch(E){}return false},httpNotModified:function(G,E){try{var H=G.getResponseHeader("Last-Modified");return G.status==304||H==o.lastModified[E]}catch(F){}return false},httpData:function(J,H,G){var F=J.getResponseHeader("content-type"),E=H=="xml"||!H&&F&&F.indexOf("xml")>=0,I=E?J.responseXML:J.responseText;if(E&&I.documentElement.tagName=="parsererror"){throw"parsererror"}if(G&&G.dataFilter){I=G.dataFilter(I,H)}if(typeof I==="string"){if(H=="script"){o.globalEval(I)}if(H=="json"){I=l["eval"]("("+I+")")}}return I},param:function(E){var G=[];function H(I,J){G[G.length]=encodeURIComponent(I)+"="+encodeURIComponent(J)}if(o.isArray(E)||E.jquery){o.each(E,function(){H(this.name,this.value)})}else{for(var F in E){if(o.isArray(E[F])){o.each(E[F],function(){H(F,this)})}else{H(F,o.isFunction(E[F])?E[F]():E[F])}}}return G.join("&").replace(/%20/g,"+")}});var m={},n,d=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];function t(F,E){var G={};o.each(d.concat.apply([],d.slice(0,E)),function(){G[this]=F});return G}o.fn.extend({show:function(J,L){if(J){return this.animate(t("show",3),J,L)}else{for(var H=0,F=this.length;H").appendTo("body");K=I.css("display");if(K==="none"){K="block"}I.remove();m[G]=K}this[H].style.display=o.data(this[H],"olddisplay",K)}}return this}},hide:function(H,I){if(H){return this.animate(t("hide",3),H,I)}else{for(var G=0,F=this.length;G=0;H--){if(G[H].elem==this){if(E){G[H](true)}G.splice(H,1)}}});if(!E){this.dequeue()}return this}});o.each({slideDown:t("show",1),slideUp:t("hide",1),slideToggle:t("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(E,F){o.fn[E]=function(G,H){return this.animate(F,G,H)}});o.extend({speed:function(G,H,F){var E=typeof G==="object"?G:{complete:F||!F&&H||o.isFunction(G)&&G,duration:G,easing:F&&H||H&&!o.isFunction(H)&&H};E.duration=o.fx.off?0:typeof E.duration==="number"?E.duration:o.fx.speeds[E.duration]||o.fx.speeds._default;E.old=E.complete;E.complete=function(){if(E.queue!==false){o(this).dequeue()}if(o.isFunction(E.old)){E.old.call(this)}};return E},easing:{linear:function(G,H,E,F){return E+F*G},swing:function(G,H,E,F){return((-Math.cos(G*Math.PI)/2)+0.5)*F+E}},timers:[],fx:function(F,E,G){this.options=E;this.elem=F;this.prop=G;if(!E.orig){E.orig={}}}});o.fx.prototype={update:function(){if(this.options.step){this.options.step.call(this.elem,this.now,this)}(o.fx.step[this.prop]||o.fx.step._default)(this);if((this.prop=="height"||this.prop=="width")&&this.elem.style){this.elem.style.display="block"}},cur:function(F){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null)){return this.elem[this.prop]}var E=parseFloat(o.css(this.elem,this.prop,F));return E&&E>-10000?E:parseFloat(o.curCSS(this.elem,this.prop))||0},custom:function(I,H,G){this.startTime=e();this.start=I;this.end=H;this.unit=G||this.unit||"px";this.now=this.start;this.pos=this.state=0;var E=this;function F(J){return E.step(J)}F.elem=this.elem;if(F()&&o.timers.push(F)==1){n=setInterval(function(){var K=o.timers;for(var J=0;J=this.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;var E=true;for(var F in this.options.curAnim){if(this.options.curAnim[F]!==true){E=false}}if(E){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;this.elem.style.display=this.options.display;if(o.css(this.elem,"display")=="none"){this.elem.style.display="block"}}if(this.options.hide){o(this.elem).hide()}if(this.options.hide||this.options.show){for(var I in this.options.curAnim){o.attr(this.elem.style,I,this.options.orig[I])}}this.options.complete.call(this.elem)}return false}else{var J=G-this.startTime;this.state=J/this.options.duration;this.pos=o.easing[this.options.easing||(o.easing.swing?"swing":"linear")](this.state,J,0,1,this.options.duration);this.now=this.start+((this.end-this.start)*this.pos);this.update()}return true}};o.extend(o.fx,{speeds:{slow:600,fast:200,_default:400},step:{opacity:function(E){o.attr(E.elem.style,"opacity",E.now)},_default:function(E){if(E.elem.style&&E.elem.style[E.prop]!=null){E.elem.style[E.prop]=E.now+E.unit}else{E.elem[E.prop]=E.now}}}});if(document.documentElement.getBoundingClientRect){o.fn.offset=function(){if(!this[0]){return{top:0,left:0}}if(this[0]===this[0].ownerDocument.body){return o.offset.bodyOffset(this[0])}var G=this[0].getBoundingClientRect(),J=this[0].ownerDocument,F=J.body,E=J.documentElement,L=E.clientTop||F.clientTop||0,K=E.clientLeft||F.clientLeft||0,I=G.top+(self.pageYOffset||o.boxModel&&E.scrollTop||F.scrollTop)-L,H=G.left+(self.pageXOffset||o.boxModel&&E.scrollLeft||F.scrollLeft)-K;return{top:I,left:H}}}else{o.fn.offset=function(){if(!this[0]){return{top:0,left:0}}if(this[0]===this[0].ownerDocument.body){return o.offset.bodyOffset(this[0])}o.offset.initialized||o.offset.initialize();var J=this[0],G=J.offsetParent,F=J,O=J.ownerDocument,M,H=O.documentElement,K=O.body,L=O.defaultView,E=L.getComputedStyle(J,null),N=J.offsetTop,I=J.offsetLeft;while((J=J.parentNode)&&J!==K&&J!==H){M=L.getComputedStyle(J,null);N-=J.scrollTop,I-=J.scrollLeft;if(J===G){N+=J.offsetTop,I+=J.offsetLeft;if(o.offset.doesNotAddBorder&&!(o.offset.doesAddBorderForTableAndCells&&/^t(able|d|h)$/i.test(J.tagName))){N+=parseInt(M.borderTopWidth,10)||0,I+=parseInt(M.borderLeftWidth,10)||0}F=G,G=J.offsetParent}if(o.offset.subtractsBorderForOverflowNotVisible&&M.overflow!=="visible"){N+=parseInt(M.borderTopWidth,10)||0,I+=parseInt(M.borderLeftWidth,10)||0}E=M}if(E.position==="relative"||E.position==="static"){N+=K.offsetTop,I+=K.offsetLeft}if(E.position==="fixed"){N+=Math.max(H.scrollTop,K.scrollTop),I+=Math.max(H.scrollLeft,K.scrollLeft)}return{top:N,left:I}}}o.offset={initialize:function(){if(this.initialized){return}var L=document.body,F=document.createElement("div"),H,G,N,I,M,E,J=L.style.marginTop,K='
';M={position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"};for(E in M){F.style[E]=M[E]}F.innerHTML=K;L.insertBefore(F,L.firstChild);H=F.firstChild,G=H.firstChild,I=H.nextSibling.firstChild.firstChild;this.doesNotAddBorder=(G.offsetTop!==5);this.doesAddBorderForTableAndCells=(I.offsetTop===5);H.style.overflow="hidden",H.style.position="relative";this.subtractsBorderForOverflowNotVisible=(G.offsetTop===-5);L.style.marginTop="1px";this.doesNotIncludeMarginInBodyOffset=(L.offsetTop===0);L.style.marginTop=J;L.removeChild(F);this.initialized=true},bodyOffset:function(E){o.offset.initialized||o.offset.initialize();var G=E.offsetTop,F=E.offsetLeft;if(o.offset.doesNotIncludeMarginInBodyOffset){G+=parseInt(o.curCSS(E,"marginTop",true),10)||0,F+=parseInt(o.curCSS(E,"marginLeft",true),10)||0}return{top:G,left:F}}};o.fn.extend({position:function(){var I=0,H=0,F;if(this[0]){var G=this.offsetParent(),J=this.offset(),E=/^body|html$/i.test(G[0].tagName)?{top:0,left:0}:G.offset();J.top-=j(this,"marginTop");J.left-=j(this,"marginLeft");E.top+=j(G,"borderTopWidth");E.left+=j(G,"borderLeftWidth");F={top:J.top-E.top,left:J.left-E.left}}return F},offsetParent:function(){var E=this[0].offsetParent||document.body;while(E&&(!/^body|html$/i.test(E.tagName)&&o.css(E,"position")=="static")){E=E.offsetParent}return o(E)}});o.each(["Left","Top"],function(F,E){var G="scroll"+E;o.fn[G]=function(H){if(!this[0]){return null}return H!==g?this.each(function(){this==l||this==document?l.scrollTo(!F?H:o(l).scrollLeft(),F?H:o(l).scrollTop()):this[G]=H}):this[0]==l||this[0]==document?self[F?"pageYOffset":"pageXOffset"]||o.boxModel&&document.documentElement[G]||document.body[G]:this[0][G]}});o.each(["Height","Width"],function(H,F){var E=H?"Left":"Top",G=H?"Right":"Bottom";o.fn["inner"+F]=function(){return this[F.toLowerCase()]()+j(this,"padding"+E)+j(this,"padding"+G)};o.fn["outer"+F]=function(J){return this["inner"+F]()+j(this,"border"+E+"Width")+j(this,"border"+G+"Width")+(J?j(this,"margin"+E)+j(this,"margin"+G):0)};var I=F.toLowerCase();o.fn[I]=function(J){return this[0]==l?document.compatMode=="CSS1Compat"&&document.documentElement["client"+F]||document.body["client"+F]:this[0]==document?Math.max(document.documentElement["client"+F],document.body["scroll"+F],document.documentElement["scroll"+F],document.body["offset"+F],document.documentElement["offset"+F]):J===g?(this.length?o.css(this[0],I):null):this.css(I,typeof J==="string"?J:J+"px")}})})(); -------------------------------------------------------------------------------- /examples/public/javascripts/jquery.template.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jQuery Templates 3 | * 4 | * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) 5 | * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. 6 | * 7 | * Written by: Stan Lemon 8 | * 9 | * Based off of the Ext.Template library, available at: 10 | * http://www.extjs.com 11 | * 12 | * This library provides basic templating functionality, allowing for macro-based 13 | * templates within jQuery. 14 | * 15 | * Basic Usage: 16 | * 17 | * var t = $.template('
Hello ${name}, how are you ${question}? I am ${me:substr(0,10)}
'); 18 | * 19 | * $(selector).append( t , { 20 | * name: 'Stan', 21 | * question: 'feeling', 22 | * me: 'doing quite well myself, thank you very much!' 23 | * }); 24 | * 25 | * Requires: jQuery 1.2+ 26 | * 27 | * 28 | * @todo Add callbacks to the DOM manipulation methods, so that events can be bound 29 | * to template nodes after creation. 30 | */ 31 | (function($){ 32 | 33 | /** 34 | * Create a New Template 35 | */ 36 | $.template = function(html, options) { 37 | return new $.template.instance(html, options); 38 | }; 39 | 40 | /** 41 | * Template constructor - Creates a new template instance. 42 | * 43 | * @param html The string of HTML to be used for the template. 44 | * @param options An object of configurable options. Currently 45 | * you can toggle compile as a boolean value and set a custom 46 | * template regular expression on the property regx by 47 | * specifying the key of the regx to use from the regx object. 48 | */ 49 | $.template.instance = function(html, options) { 50 | // If a custom regular expression has been set, grab it from the regx object 51 | if ( options && options['regx'] ) options.regx = this.regx[ options.regx ]; 52 | 53 | this.options = $.extend({ 54 | compile: false, 55 | regx: this.regx.standard 56 | }, options || {}); 57 | 58 | this.html = html; 59 | 60 | if (this.options.compile) { 61 | this.compile(); 62 | } 63 | this.isTemplate = true; 64 | }; 65 | 66 | /** 67 | * Regular Expression for Finding Variables 68 | * 69 | * The default pattern looks for variables in JSP style, the form of: ${variable} 70 | * There are also regular expressions available for ext-style variables and 71 | * jTemplate style variables. 72 | * 73 | * You can add your own regular expressions for variable ussage by doing. 74 | * $.extend({ $.template.re , { 75 | * myvartype: /...../g 76 | * } 77 | * 78 | * Then when creating a template do: 79 | * var t = $.template("
...
", { regx: 'myvartype' }); 80 | */ 81 | $.template.regx = $.template.instance.prototype.regx = { 82 | jsp: /\$\{([\w-]+)(?:\:([\w\.]*)(?:\((.*?)?\))?)?\}/g, 83 | ext: /\{([\w-]+)(?:\:([\w\.]*)(?:\((.*?)?\))?)?\}/g, 84 | jtemplates: /\{\{([\w-]+)(?:\:([\w\.]*)(?:\((.*?)?\))?)?\}\}/g 85 | }; 86 | 87 | /** 88 | * Set the standard regular expression to be used. 89 | */ 90 | $.template.regx.standard = $.template.regx.jsp; 91 | 92 | /** 93 | * Variable Helper Methods 94 | * 95 | * This is a collection of methods which can be used within the variable syntax, ie: 96 | * ${variable:substr(0,30)} Which would only print a substring, 30 characters in length 97 | * begining at the first character for the variable named "variable". 98 | * 99 | * A basic substring helper is provided as an example of how you can define helpers. 100 | * To add more helpers simply do: 101 | * $.extend( $.template.helpers , { 102 | * sampleHelper: function() { ... } 103 | * }); 104 | */ 105 | $.template.helpers = $.template.instance.prototype.helpers = { 106 | substr : function(value, start, length){ 107 | return String(value).substr(start, length); 108 | } 109 | }; 110 | 111 | 112 | /** 113 | * Template Instance Methods 114 | */ 115 | $.extend( $.template.instance.prototype, { 116 | 117 | /** 118 | * Apply Values to a Template 119 | * 120 | * This is the macro-work horse of the library, it receives an object 121 | * and the properties of that objects are assigned to the template, where 122 | * the variables in the template represent keys within the object itself. 123 | * 124 | * @param values An object of properties mapped to template variables 125 | */ 126 | apply: function(values) { 127 | if (this.options.compile) { 128 | return this.compiled(values); 129 | } else { 130 | var tpl = this; 131 | var fm = this.helpers; 132 | 133 | var fn = function(m, name, format, args) { 134 | if (format) { 135 | if (format.substr(0, 5) == "this."){ 136 | return tpl.call(format.substr(5), values[name], values); 137 | } else { 138 | if (args) { 139 | // quoted values are required for strings in compiled templates, 140 | // but for non compiled we need to strip them 141 | // quoted reversed for jsmin 142 | var re = /^\s*['"](.*)["']\s*$/; 143 | args = args.split(','); 144 | 145 | for(var i = 0, len = args.length; i < len; i++) { 146 | args[i] = args[i].replace(re, "$1"); 147 | } 148 | args = [values[name]].concat(args); 149 | } else { 150 | args = [values[name]]; 151 | } 152 | 153 | return fm[format].apply(fm, args); 154 | } 155 | } else { 156 | return values[name] !== undefined ? values[name] : ""; 157 | } 158 | }; 159 | 160 | return this.html.replace(this.options.regx, fn); 161 | } 162 | }, 163 | 164 | /** 165 | * Compile a template for speedier usage 166 | */ 167 | compile: function() { 168 | var sep = $.browser.mozilla ? "+" : ","; 169 | var fm = this.helpers; 170 | 171 | var fn = function(m, name, format, args){ 172 | if (format) { 173 | args = args ? ',' + args : ""; 174 | 175 | if (format.substr(0, 5) != "this.") { 176 | format = "fm." + format + '('; 177 | } else { 178 | format = 'this.call("'+ format.substr(5) + '", '; 179 | args = ", values"; 180 | } 181 | } else { 182 | args= ''; format = "(values['" + name + "'] == undefined ? '' : "; 183 | } 184 | return "'"+ sep + format + "values['" + name + "']" + args + ")"+sep+"'"; 185 | }; 186 | 187 | var body; 188 | 189 | if ($.browser.mozilla) { 190 | body = "this.compiled = function(values){ return '" + 191 | this.html.replace(/\\/g, '\\\\').replace(/(\r\n|\n)/g, '\\n').replace(/'/g, "\\'").replace(this.options.regx, fn) + 192 | "';};"; 193 | } else { 194 | body = ["this.compiled = function(values){ return ['"]; 195 | body.push(this.html.replace(/\\/g, '\\\\').replace(/(\r\n|\n)/g, '\\n').replace(/'/g, "\\'").replace(this.options.regx, fn)); 196 | body.push("'].join('');};"); 197 | body = body.join(''); 198 | } 199 | eval(body); 200 | return this; 201 | } 202 | }); 203 | 204 | 205 | /** 206 | * Save a reference in this local scope to the original methods which we're 207 | * going to overload. 208 | **/ 209 | var $_old = { 210 | domManip: $.fn.domManip, 211 | text: $.fn.text, 212 | html: $.fn.html 213 | }; 214 | 215 | /** 216 | * Overwrite the domManip method so that we can use things like append() by passing a 217 | * template object and macro parameters. 218 | */ 219 | $.fn.domManip = function( args, table, reverse, callback ) { 220 | if (args[0].isTemplate) { 221 | // Apply the template and it's arguments... 222 | args[0] = args[0].apply( args[1] ); 223 | // Get rid of the arguements, we don't want to pass them on 224 | delete args[1]; 225 | } 226 | 227 | // Call the original method 228 | var r = $_old.domManip.apply(this, arguments); 229 | 230 | return r; 231 | }; 232 | 233 | /** 234 | * Overwrite the html() method 235 | */ 236 | $.fn.html = function( value , o ) { 237 | if (value && value.isTemplate) var value = value.apply( o ); 238 | 239 | var r = $_old.html.apply(this, [value]); 240 | 241 | return r; 242 | }; 243 | 244 | /** 245 | * Overwrite the text() method 246 | */ 247 | $.fn.text = function( value , o ) { 248 | if (value && value.isTemplate) var value = value.apply( o ); 249 | 250 | var r = $_old.text.apply(this, [value]); 251 | 252 | return r; 253 | }; 254 | 255 | })(jQuery); 256 | -------------------------------------------------------------------------------- /examples/views/article.haml: -------------------------------------------------------------------------------- 1 | - article = @article if @article 2 | .article 3 | %h2 4 | %a{:href => article.permalink} 5 | =article.title 6 | %span 7 | =article.comments.size 8 | %h3 9 | ="Posted by #{article.author} on #{humanized_date article.publish_date}" 10 | %p.entry 11 | =article.body_html 12 | .meta 13 | %span 14 | Tags: 15 | =link_to_tags article.tags 16 | %br/ 17 | %span 18 | Meta: 19 | ="#{article.comments.size} comments, permalink" 20 | -------------------------------------------------------------------------------- /examples/views/articles.haml: -------------------------------------------------------------------------------- 1 | #articles 2 | =partial :article, :collection => @articles 3 | -------------------------------------------------------------------------------- /examples/views/comment.haml: -------------------------------------------------------------------------------- 1 | .comment 2 | %h2 3 | %a{:href => comment.homepage} 4 | =comment.author 5 | %span 6 | =comment.publish_date 7 | %p 8 | =comment.body 9 | -------------------------------------------------------------------------------- /examples/views/home.haml: -------------------------------------------------------------------------------- 1 | #articles 2 | =partial :article, :collection => @articles 3 | -------------------------------------------------------------------------------- /examples/views/layout.haml: -------------------------------------------------------------------------------- 1 | !!! Strict 2 | %html 3 | %head 4 | %title= page_title 5 | %script{ 'type' => "text/javascript", :src => "/javascripts/jquery-1.3.1.min.js" } 6 | %script{ 'type' => "text/javascript", :src => "/javascripts/jquery.template.js" } 7 | %script{ 'type' => "text/javascript", :src => "/javascripts/application.js" } 8 | %link{:href => '/style.css', :rel => 'stylesheet', :type => 'text/css'} 9 | %link{:href => "#{base_url}/feed", :rel => 'alternate', :type => 'application/atom+xml', :title => "Feed for #{}" } 10 | %body 11 | #container 12 | #header 13 | #logo 14 | %h1 15 | %a{:href => '/'}= Aerial.config.title 16 | %span 17 | =Aerial.config.subtitle 18 | #content= yield 19 | #sidebar= partial :sidebar 20 | #footer 21 | %p#legal= "© #{Time.now.strftime('%Y')} #{Aerial.config.author}" 22 | %p#powered= "powered by Aerial" 23 | -------------------------------------------------------------------------------- /examples/views/not_found.haml: -------------------------------------------------------------------------------- 1 | The page you requested could not be found. 2 | -------------------------------------------------------------------------------- /examples/views/post.haml: -------------------------------------------------------------------------------- 1 | =partial :article 2 | %h5 3 | Comments 4 | #comments 5 | =partial :comment, :collection => @article.comments 6 | 7 | %div{:id => "new_comment"} 8 | %form{:action => "/article/#{@article.id}/comments"} 9 | %p 10 | %input{:type => "text", :id => "comment_author"} 11 | %label{:id => "author_label"} 12 | Your name (required) 13 | %p 14 | %input{:type => "text", :id => "comment_email"} 15 | %label{:id => "email_label"} 16 | Your email address (required) 17 | %p 18 | %input{:type => "text", :id => "comment_homepage"} 19 | %label{:id => "homepage_label"} 20 | Website 21 | %p 22 | %label{:id => "comment_label"} 23 | Please enter your comment 24 | %textarea{:id => "comment_body"} 25 | %p 26 | %button{:type => "submit", :id => "comment_submit", :onclick => "Comment.submit('#{@article.id}'); return false;"} 27 | Submit Comment (Thanks) 28 | -------------------------------------------------------------------------------- /examples/views/rss.haml: -------------------------------------------------------------------------------- 1 | !!! XML 2 | %rss{"version" => "2.0"} 3 | %channel 4 | %title= "#{Aerial.config.title}" 5 | %link= "#{base_url}" 6 | %language= "en-us" 7 | %ttl= "40" 8 | %description= "#{Aerial.config.subtitle}" 9 | - @articles.each do |article| 10 | %item 11 | %title= article.title 12 | %link= full_hostname(article.permalink) 13 | %description= article.body_html 14 | %pubDate= article.publish_date 15 | %guid{"isPermaLink" => "false"}= article.id 16 | -------------------------------------------------------------------------------- /examples/views/sidebar.haml: -------------------------------------------------------------------------------- 1 | #about 2 | %h2 About 3 | %p 4 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut 5 | labore et dolore magna aliqua. Ut enimad minim veniam, quis nostrud exercitation ullamco 6 | laboris nisi ut aliquip ex ea commodo consequat. 7 | 8 | #menu 9 | %h2 Menu 10 | %ul 11 | %li 12 | %a{:href => "/"}Home 13 | %li 14 | %a{:href => "/about"}About 15 | %h2 Recent Posts 16 | %ul 17 | -Aerial::Article.recent.each do |article| 18 | %li 19 | %a{:href => "#{article.permalink}"}=article.title 20 | %h2 Categories 21 | %ul 22 | -Aerial::Article.tags.each do |tag| 23 | %li 24 | %a{:href => "/tags/#{tag}"}=tag 25 | %h2 Archives 26 | %ul 27 | -Aerial::Article.archives.each do |archive| 28 | %li 29 | %a{:href => "/archives/#{archive[0][0]}"}="#{archive[0][1]} (#{archive[1]})" 30 | -------------------------------------------------------------------------------- /examples/views/style.sass: -------------------------------------------------------------------------------- 1 | !red = #f00 2 | !black = #000 3 | !darkgrey = #555 4 | !lightgrey = #eeeeee 5 | !blue = #2168a6 6 | !red = #ed1e24 7 | 8 | =image-replacement 9 | :text-indent -9999px 10 | :margin-bottom 0.3em 11 | 12 | body 13 | :color = !black 14 | :font normal 12px Verdana, Arial, sans-serif 15 | :font-size 88% 16 | :line-height 1.5 17 | a 18 | :text-decoration none 19 | :color = !darkgrey 20 | &:visited 21 | :color = !darkgrey 22 | &:hover, &:visited 23 | :text-decoration underline 24 | h2 25 | :font-size 100% 26 | 27 | ul 28 | :padding 0 29 | li 30 | :list-style none 31 | :line-height 1.2 32 | :margin 0 0 5px 0 33 | a 34 | :text-decoration underline 35 | &:hover 36 | :text-decoration none 37 | form 38 | :background = !lightgrey 39 | :padding 10px 40 | :border-top 1px solid #ddd 41 | :font-size 100% 42 | p 43 | :margin 0 0 5px 0 44 | label 45 | :font-size 88% 46 | label.error 47 | :background-color = !red 48 | :padding 2px 49 | :color #fff 50 | input 51 | :border 1px solid 52 | :padding 2px 53 | :width 300px 54 | :border-color = #ddd 55 | :font-size 100% 56 | textarea 57 | :border 1px solid 58 | :border-color = #ddd 59 | :width 500px 60 | :padding 3px 61 | :height 75px 62 | :font normal 14px Verdana, Arial, sans-serif 63 | 64 | #header 65 | :height 60px 66 | :width 100% 67 | :border-bottom 1px dashed 68 | :border-color = !lightgrey 69 | #logo 70 | :float left 71 | :height 50px 72 | h1 73 | :font 300% arial, sans-serif 74 | :padding 5px 0 75 | :margin 0 76 | a 77 | :color = !blue 78 | :text-decoration none 79 | span 80 | :font-size 16pt 81 | :color = !darkgrey 82 | 83 | #container 84 | :width 800px 85 | :margin 0 auto 86 | 87 | #content 88 | :width 575px 89 | :float left 90 | :border-right 1px dashed 91 | :border-color = !lightgrey 92 | :padding 10px 10px 0 0 93 | h5 94 | :font-size 110% 95 | :background-color #ffd 96 | :margin 1.2em 0 0.3em 97 | :padding 3px 98 | :border-bottom 1px dotted #aaa 99 | .article, .page 100 | h2 101 | :color = !darkgrey 102 | :font-family arial, sans-serif 103 | :font-weight normal 104 | :letter-spacing -1px 105 | :font-size 28px 106 | :margin 0 0 -9px 0 107 | a 108 | :text-decoration none 109 | span 110 | :color = !lightgrey 111 | h3 112 | :color #777 113 | :font-weight normal 114 | :margin 0 0 0 2px 115 | :padding 0 116 | :font-size 110% 117 | :letter-spacing -0.5px 118 | .meta 119 | :font-size 8pt 120 | :background = !lightgrey 121 | :padding 5px 122 | :border 1px solid #ddd 123 | :margin 15px 0 124 | span 125 | :color = !darkgrey !important 126 | :font-weight bold 127 | 128 | .comment 129 | :margin 15px 0 130 | :padding 10px 131 | :border 3px solid 132 | :border-color = !lightgrey 133 | h2 134 | span 135 | :font-size 88% 136 | :margin-left 5px 137 | :color #777 138 | 139 | #sidebar 140 | :float right 141 | :width 200px 142 | :font-size 88% 143 | p 144 | :margin-top -7px 145 | a 146 | :color = !blue 147 | 148 | #footer 149 | :width 100% 150 | :height 100px 151 | :float left 152 | :margin-top 20px 153 | :border-top 1px dashed 154 | :border-color = !lightgrey 155 | p 156 | :margin-top 5px 157 | #legal 158 | :width 40% 159 | :float left 160 | #powered 161 | :width 50% 162 | :float right 163 | :text-align right 164 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | aerial 8 | 123 | 124 | 125 |
126 | 127 | Fork me on GitHub 128 | 129 |
130 |
131 | Aerial uses Git to store articles and pages. 132 |
133 |

Aerial

134 |

Designed for Ruby developers, there is no admin interface and no SQL database. Articles are written in your favorite text editor and versioned with Git. Comments are also stored as plain-text files and pushed to the remote repository when created. It uses Grit (http://github.com/mojombo/grit) to interface with local and remote Git repositories.

135 |

Installation

136 | 137 |

via Rubygems:

138 |
$ gem install aerial
139 | $ aerial install /home/user/myblog
140 |

This will create a couple files and directories. Mainly, the views, public, and configuration files. You may edit config.yml to your liking

141 |

The installer provides special configuration files for Thin and Passenger.

142 |

via Clone:

143 |
$ git clone git://github.com/mattsears/aerial
144 | 145 |

Pasenger

146 |
$ aerial install /home/user/myblog --passenger
147 | $ cd /home/user/myblog
148 |

Then, restart Passenger $ touch tmp/restart.txt

149 |

Thin

150 |

Install Thin:

151 |
$ gem install thin
152 |

Run the installer:

153 |
$ aerial install /home/user/myblog --thin
154 | $ cd /home/user/myblog
155 |

Tweak the thin.yml

156 |

Start the thin server

157 |
$ thin -C thin.yml -R config.ru start
158 |

Issues

159 |

Find a bug? Want a feature? Submit an issue here. Patches welcome! 160 |

162 |
163 | 164 | 165 | -------------------------------------------------------------------------------- /lib/aerial.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | libdir = File.dirname(__FILE__) 3 | $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir) 4 | AERIAL_ROOT = File.join(File.dirname(__FILE__), '..') unless defined? AERIAL_ROOT 5 | CONFIG = YAML.load_file( File.join(AERIAL_ROOT, 'config', 'config.yml') ) unless defined?(CONFIG) 6 | 7 | # System requirements 8 | require 'rubygems' 9 | require 'grit' 10 | require 'sinatra' 11 | require 'haml' 12 | require 'sass' 13 | require 'redcarpet' 14 | require 'albino' 15 | require 'html_truncator' 16 | require 'aerial/base' 17 | require 'aerial/content' 18 | require 'aerial/article' 19 | require 'aerial/config' 20 | require 'aerial/migrator' 21 | require 'aerial/site' 22 | 23 | module Aerial 24 | 25 | # Make sure git is added to the env path 26 | ENV['PATH'] = "#{ENV['PATH']}:/usr/local/bin" 27 | 28 | class << self 29 | attr_accessor :debug, :logger, :repo, :config, :root, :env 30 | end 31 | 32 | def self.new(overrides, env = :development) 33 | @root ||= Dir.pwd 34 | @logger ||= ::Logger.new(STDOUT) 35 | @debug ||= false 36 | @repo ||= Grit::Repo.new(@root) rescue nil 37 | @env = env 38 | @config = Aerial::Config.new(overrides) 39 | require 'aerial/app' 40 | return self 41 | end 42 | 43 | def self.log(str) 44 | logger.debug { str } if debug 45 | end 46 | 47 | end 48 | -------------------------------------------------------------------------------- /lib/aerial/app.rb: -------------------------------------------------------------------------------- 1 | module Aerial 2 | class App < Sinatra::Base 3 | include Aerial 4 | 5 | before do 6 | # kill trailing slashes for all requests except '/' 7 | request.env['PATH_INFO'].gsub!(/\/$/, '') if request.env['PATH_INFO'] != '/' 8 | end 9 | 10 | set :environment, Aerial.env 11 | 12 | # Helpers 13 | helpers do 14 | include Rack::Utils 15 | include Aerial::Helper 16 | alias_method :h, :escape_html 17 | end 18 | 19 | Aerial::Site.include_local_app 20 | 21 | # Homepage 22 | get '/' do 23 | @articles = Aerial::Article.recent(:limit => 5) 24 | haml(Aerial.config.views.default.to_sym) 25 | end 26 | 27 | # Articles 28 | get "/#{Aerial.config.articles.dir}" do 29 | @articles = Aerial::Article.recent(:limit => 5) 30 | haml(:"#{Aerial.config.articles.dir}") 31 | end 32 | 33 | get '/feed*' do 34 | content_type 'text/xml', :charset => 'utf-8' 35 | @articles = Aerial::Article.all 36 | haml(:rss, :layout => false) 37 | end 38 | 39 | # Sassy! 40 | get '/style.css' do 41 | content_type 'text/css', :charset => 'utf-8' 42 | sass(:style) 43 | end 44 | 45 | # Single article page 46 | get "/*/:year/:month/:day/:article" do 47 | link = [Aerial.config.articles.dir, params[:year], params[:month], params[:day], params[:article]].join("/") 48 | @article = Aerial::Article.with_permalink("#{link}") 49 | throw :halt, [404, not_found ] unless @article 50 | @page_title = @article.title 51 | # haml(:post) 52 | haml(:"#{Aerial.config.articles.dir}") 53 | end 54 | 55 | # Article tags 56 | get "/#{Aerial.config.articles.dir}/tags/:tag" do 57 | @articles = Aerial::Article.with_tag(params[:tag]) 58 | haml(:"#{Aerial.config.articles.dir}") 59 | end 60 | 61 | # Article archives 62 | get "/#{Aerial.config.articles.dir}/archives/:year/:month" do 63 | @articles = Aerial::Article.with_date(params[:year], params[:month]) 64 | haml(:"#{Aerial.config.articles.dir}") 65 | end 66 | 67 | not_found do 68 | haml(:not_found) 69 | end 70 | 71 | # Try to find some kind of page 72 | get "*" do 73 | parts = params[:splat].map{ |p| p.sub(/\//, "") } 74 | page = File.expand_path(File.join(Aerial.root, 'views', parts)) 75 | raise Sinatra::NotFound unless File.exist?("#{page}.haml") 76 | haml(parts.join('/').to_sym) 77 | end 78 | 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/aerial/article.rb: -------------------------------------------------------------------------------- 1 | module Aerial 2 | 3 | class Article < Content 4 | 5 | attr_reader :id, :tags, :archive_name, :body_html, 6 | :meta, :updated_on, :publish_date, :file_name 7 | 8 | # ============================================================================================= 9 | # PUBLIC CLASS METHODS 10 | # ============================================================================================= 11 | 12 | # Find all articles, including drafts 13 | def self.all(options={}) 14 | self.find_all 15 | end 16 | 17 | # A quick way to load an article by blob id 18 | # +id+ of the blob 19 | def self.open(id, options = {}) 20 | self.find_by_blob_id(id, options) 21 | end 22 | 23 | # Find a single article by id 24 | # +id+ of the blob 25 | def self.find(id, options={}) 26 | self.find_by_id(id, options) 27 | end 28 | 29 | # Find a single article by name 30 | # +name+ of the article file 31 | def self.with_name(name, options={}) 32 | self.find_by_name(name, options) 33 | end 34 | 35 | # Find articles by tag 36 | # +tag+ category 37 | def self.with_tag(tag, options={}) 38 | self.find_by_tag(tag, options) 39 | end 40 | 41 | # Find articles by month and year 42 | # +year+ of when article was published 43 | # + month+ of when the article was published 44 | def self.with_date(year, month, options={}) 45 | self.find_by_date(year, month, options) 46 | end 47 | 48 | # Return an article given its permalink value 49 | # +link+ full path of the link 50 | def self.with_permalink(link, options={}) 51 | self.find_by_permalink(link, options) 52 | end 53 | 54 | # Find the most recent articles 55 | def self.recent(options={}) 56 | limit = options.delete(:limit) || 4 57 | self.find_all(options).first(limit) 58 | end 59 | 60 | # Return true if the article file exists 61 | # +id+ 62 | def self.exists?(id) 63 | self.find_by_name(id) ? true : false 64 | end 65 | 66 | # Return all the tags assigned to the articles 67 | def self.tags 68 | self.find_tags 69 | end 70 | 71 | # Calculate the archives 72 | def self.archives 73 | self.find_archives 74 | end 75 | 76 | # ============================================================================================= 77 | # PUBLIC INSTANCE METHODS 78 | # ============================================================================================= 79 | 80 | # Make a permanent link for the article 81 | def permalink 82 | link = self.file_name.gsub(/\.article$|\.markdown$|\.md$|\.mdown$|\.mkd$|\.mkdn$/, '') 83 | "#{Aerial.config.articles.dir}/#{publish_date.year}/#{publish_date.month}/#{publish_date.day}/#{escape(link)}" 84 | end 85 | 86 | # Returns the absolute path to the article file 87 | def expand_path 88 | return "#{self.archive_expand_path}/#{self.file_name}" 89 | end 90 | 91 | # Returns the full path to the article archive (directory) 92 | def archive_expand_path 93 | return unless archive = self.archive_name 94 | return "#{Aerial.repo.working_dir}/#{Aerial.config.articles.dir}/#{archive}" 95 | end 96 | 97 | private 98 | 99 | # ============================================================================================= 100 | # PRIVATE CLASS METHODS 101 | # ============================================================================================= 102 | 103 | # Find a single article given the article name 104 | # +name+ file name 105 | def self.find_by_name(name, options={}) 106 | if tree = Aerial.repo.tree/"#{Aerial.config.articles.dir}/#{name}" 107 | return self.find_article(tree) 108 | end 109 | end 110 | 111 | # Find a single article by id 112 | # +id+ the blob id 113 | # +options+ 114 | def self.find_by_id(article_id, options = {}) 115 | if blog = Aerial.repo.tree/"#{Aerial.config.articles.dir}" 116 | blog.contents.each do |entry| 117 | article = self.find_article(entry, options) 118 | return article if article.id == article_id 119 | end 120 | end 121 | raise "Article not found" 122 | end 123 | 124 | # Find an article by blob id 125 | # This is a more efficient way of finding an article 126 | # However, we won't know anything else about the article such as the filename, tree, etc 127 | # +id+ of the blob 128 | def self.find_by_blob_id(id, options = {}) 129 | blob = Aerial.repo.blob(id) 130 | if blob.size > 0 131 | attributes = self.extract_article(blob, options) 132 | return Article.new(attributes) if attributes 133 | end 134 | raise "Article doesn't exists" 135 | end 136 | 137 | # Returns all articles by tag 138 | # +tag+ the article category 139 | def self.find_by_tag(tag, options = {}) 140 | articles = [] 141 | self.find_all.each do |article| 142 | if article.tags.include?(tag) 143 | articles << article 144 | end 145 | end 146 | return articles 147 | end 148 | 149 | # Find a single article by permalink 150 | # +link+ 151 | def self.find_by_permalink(link, options={}) 152 | if blog = Aerial.repo.tree/"#{Aerial.config.articles.dir}/" 153 | blog.contents.each do |entry| 154 | article = self.find_article(entry, options) 155 | return article if article.permalink == link 156 | end 157 | end 158 | return false 159 | end 160 | 161 | # Find all the articles by year and month 162 | def self.find_by_date(year, month, options ={}) 163 | articles = [] 164 | self.find_all.each do |article| 165 | if article.publish_date.year == year.to_i && 166 | article.publish_date.month == month.to_i 167 | articles << article 168 | end 169 | end 170 | return articles 171 | end 172 | 173 | # Find all the articles in the repository 174 | def self.find_all(options={}) 175 | articles = [] 176 | if blog = Aerial.repo.tree/"#{Aerial.config.articles.dir}/" 177 | blog.contents.first( options[:limit] || 100 ).each do |entry| 178 | article = self.find_article(entry, options) 179 | if article && article.publish_date < DateTime.now 180 | articles << self.find_article(entry, options) 181 | end 182 | end 183 | end 184 | return articles.sort_by { |article| article.publish_date}.reverse 185 | end 186 | 187 | # Look in the given tree, find the article 188 | # +tree+ repository tree 189 | # +options+ :blob_id 190 | def self.find_article(tree, options = {}) 191 | attributes = nil 192 | tree.contents.each do |archive| 193 | if archive.name =~ /article/ 194 | attributes = self.extract_article(archive, options) 195 | attributes[:archive_name] = tree.name 196 | attributes[:file_name] = archive.name 197 | end 198 | end 199 | return Article.new(attributes) if attributes 200 | end 201 | 202 | # Find all the tags assign to the articles 203 | def self.find_tags 204 | tags = [] 205 | self.all.each do |article| 206 | tags.concat(article.tags) 207 | end 208 | return tags.uniq 209 | end 210 | 211 | # Create a histogram of article archives 212 | def self.find_archives 213 | dates = [] 214 | self.all.each do |article| 215 | date = article.publish_date 216 | dates << [date.strftime("%Y/%m"), date.strftime("%B %Y")] 217 | end 218 | return dates.inject(Hash.new(0)) { |h,x| h[x] += 1; h } 219 | end 220 | 221 | # Extract the Article attributes from the file 222 | # +blob+ 223 | def self.extract_article(blob, options={}) 224 | article = self.extract_attributes(blob.data) 225 | article[:id] = blob.id 226 | article[:tags] = article[:tags].split(/, /) 227 | options = [:autolink, :no_intraemphasis, :fenced_code, :gh_blockcode, :tables] 228 | article[:body_html] = self.colorize(Redcarpet.new(article[:body], *options).to_html) 229 | return article 230 | end 231 | 232 | end 233 | end 234 | -------------------------------------------------------------------------------- /lib/aerial/base.rb: -------------------------------------------------------------------------------- 1 | require "html_truncator" 2 | 3 | module Aerial 4 | 5 | module Helper 6 | 7 | # Returns the current url 8 | def url() request.url end 9 | 10 | # Returns the request host 11 | # TODO: just use request.host (http://rack.lighthouseapp.com/projects/22435/tickets/77-requesthost-should-answer-the-forwarded-host) 12 | def host 13 | if request.env['HTTP_X_FORWARDED_SERVER'] =~ /[a-z]*/ 14 | request.env['HTTP_X_FORWARDED_SERVER'] 15 | else 16 | request.host 17 | end 18 | end 19 | 20 | # Returns the path 21 | def path 22 | base = "#{request.env['REQUEST_URI']}".scan(/\w+/).first 23 | return base.blank? ? "index" : base 24 | end 25 | 26 | # Returns the absolute base url 27 | def base_url 28 | scheme = request.scheme 29 | port = request.port 30 | url = "#{scheme}://#{host}" 31 | if scheme == "http" && port != 80 || scheme == "https" && port != 443 32 | url << ":#{port}" 33 | end 34 | url << request.script_name 35 | end 36 | 37 | # Creates an absolute link 38 | # +link+ link to append to the baseurl 39 | # TODO: should we add more value to this? it seems like we might as well 40 | # just take care of this by appending the link to base_url in the app 41 | def full_hostname(link = "") 42 | "#{base_url}#{link}" 43 | end 44 | 45 | # Display the page titles in proper format 46 | def page_title 47 | title = @page_title ? "| #{@page_title}" : "" 48 | return "#{Aerial.config.title} #{title}" 49 | end 50 | 51 | # Format just the DATE in a nice easy to read format 52 | def humanized_date(date) 53 | if date && date.respond_to?(:strftime) 54 | date.strftime('%A %B, %d %Y').strip 55 | else 56 | 'Never' 57 | end 58 | end 59 | 60 | # Format just the DATE in a short way 61 | def short_date(date) 62 | if date && date.respond_to?(:strftime) 63 | date.strftime('%b %d').strip 64 | else 65 | 'Never' 66 | end 67 | end 68 | 69 | # Format for the rss 2.0 feed 70 | def rss_date(date) 71 | date.strftime("%a, %d %b %Y %H:%M:%S %z") #Tue, 03 Jun 2003 09:39:21 GMT 72 | end 73 | 74 | # Truncate a string 75 | def blurb(text, options ={}) 76 | return if text.nil? 77 | options.merge!(:length => 22, :omission => "...") 78 | HTML_Truncator.truncate("

#{Redcarpet.new(text).to_html}

", 79 | options[:length], :ellipsis => options[:omission]) 80 | end 81 | 82 | # Handy method to render partials including collections 83 | def partial(template, *args) 84 | template_array = template.to_s.split('/') 85 | template = template_array[0..-2].join('/') + "/_#{template_array[-1]}" 86 | options = args.last.is_a?(Hash) ? args.pop : {} 87 | options.merge!(:layout => false) 88 | if collection = options.delete(:collection) then 89 | collection.inject([]) do |buffer, member| 90 | buffer << haml(:"#{template}", options.merge(:layout => 91 | false, :locals => {template_array[-1].to_sym => member})) 92 | end.join("\n") 93 | else 94 | haml(:"#{template}", options) 95 | end 96 | end 97 | 98 | # Author link 99 | def link_to_author(comment) 100 | unless comment.homepage.blank? 101 | return "#{comment.author}" 102 | end 103 | comment.author 104 | end 105 | 106 | # Create a list of hyperlinks with a set of tags 107 | def link_to_tags(tags) 108 | return unless tags 109 | links = [] 110 | tags.each do |tag| 111 | links << "#{tag}" 112 | end 113 | links.join(", ") 114 | end 115 | 116 | end 117 | 118 | # Provides a few methods for interacting with that Aerial repository 119 | class Git 120 | 121 | # Commit the new file and push it to the remote repository 122 | def self.commit_and_push(path, message) 123 | self.commit(path, message) 124 | self.push 125 | end 126 | 127 | # Added the file in the path and commit the changs to the repo 128 | # +path+ to the new file to commit 129 | # +message+ description of the commit 130 | def self.commit(path, message) 131 | Dir.chdir(File.expand_path(Aerial.repo.working_dir)) do 132 | Aerial.repo.add(path) 133 | end 134 | Aerial.repo.commit_index(message) 135 | end 136 | 137 | # Adds all untracked files and commits them to the repo 138 | def self.commit_all(path = ".", message = "Commited all changes at: #{DateTime.now}") 139 | unless Aerial.repo.status.untracked.empty? 140 | self.commit(path, message) 141 | end 142 | true 143 | end 144 | 145 | # Upload all new commits to the remote repo (if exists) 146 | def self.push 147 | return unless Aerial.config.git.name && Aerial.config.git.branch 148 | 149 | begin 150 | cmd = "push #{Aerial.config.git.name} #{Aerial.config.git.branch} " 151 | Aerial.repo.git.run('', cmd, '', {}, "") 152 | rescue Exception => e 153 | Aerial.log(e.message) 154 | end 155 | end 156 | 157 | end 158 | 159 | end 160 | 161 | class Object 162 | def blank? 163 | respond_to?(:empty?) ? empty? : !self 164 | end 165 | end 166 | 167 | class Hash 168 | # Merges self with another hash, recursively. 169 | # 170 | # This code was lovingly stolen from some random gem: 171 | # http://gemjack.com/gems/tartan-0.1.1/classes/Hash.html 172 | # 173 | # Thanks to whoever made it. 174 | def deep_merge(hash) 175 | target = dup 176 | 177 | hash.keys.each do |key| 178 | if hash[key].is_a? Hash and self[key].is_a? Hash 179 | target[key] = target[key].deep_merge(hash[key]) 180 | next 181 | end 182 | 183 | target[key] = hash[key] 184 | end 185 | 186 | target 187 | end 188 | end 189 | -------------------------------------------------------------------------------- /lib/aerial/build.rb: -------------------------------------------------------------------------------- 1 | require "thor" 2 | require File.dirname(__FILE__) + "/../aerial" 3 | 4 | module Aerial 5 | class Build < Thor 6 | attr_reader :root 7 | include Thor::Actions 8 | 9 | desc "build", "Build alls static files." 10 | method_options :config => 'config.yml' 11 | def build 12 | 13 | Aerial.new(options, :production) 14 | Aerial::App.set :root, Aerial.root 15 | Aerial::App.set :environment, :production 16 | @app = Aerial::App 17 | @app.set :root, @root 18 | @app.set :environment, :production 19 | # ENV['RACK_ENV'] = 'production' 20 | # Aerial.env = :production 21 | @request = Rack::MockRequest.new(@app) 22 | Aerial::Build.source_root(Aerial.root) 23 | @site_path = Aerial.config.static.dir 24 | @blog_path = File.join(@site_path, Aerial.config.articles.dir) 25 | @site = Aerial::Site.new 26 | 27 | build_pages_html 28 | build_style_css 29 | build_articles_html 30 | build_tags_html 31 | build_archives_html 32 | build_feed_xml 33 | say "Static site generated" 34 | end 35 | 36 | private 37 | 38 | def build_style_css 39 | create_file "#{@site_path}/style.css" do 40 | @request.request('get', '/style.css').body 41 | end 42 | end 43 | 44 | def build_pages_html 45 | create_file "#{@site_path}/index.html", :force => true do 46 | @request.request('get', '/').body 47 | end 48 | create_file "#{@site_path}/#{Aerial.config.articles.dir}.html", :force => true do 49 | @request.request('get', Aerial.config.articles.dir).body 50 | end 51 | @site.read_pages.each do |f| 52 | path = f.chomp(File.extname(f)) 53 | create_file "#{@site_path}/#{f.gsub(File.extname(f), '.html')}", :force => true do 54 | @request.request('get', "/#{path}").body 55 | end 56 | end 57 | end 58 | 59 | def build_articles_html 60 | @articles = Aerial::Article.all 61 | @articles.each do |article| 62 | create_file "#{@site_path}/#{article.permalink}.html", :force => true do 63 | @request.request('get', "#{article.permalink}").body 64 | end 65 | end 66 | end 67 | 68 | def build_tags_html 69 | Aerial::Article.tags.each do |tag| 70 | create_file "#{@blog_path}/tags/#{tag}.html", :force => true do 71 | @request.request('get', "#{Aerial.config.articles.dir}/tags/#{tag}").body 72 | end 73 | end 74 | end 75 | 76 | def build_archives_html 77 | Aerial::Article.archives.each do |archive| 78 | create_file "#{@blog_path}/archives/#{archive.first.first}.html", :force => true do 79 | @request.request('get', "#{Aerial.config.articles.dir}/archives/#{archive.first.first}").body 80 | end 81 | end 82 | end 83 | 84 | def build_feed_xml 85 | create_file "#{@site_path}/feed.xml", :force => true do 86 | @request.request('get', "/feed.xml").body 87 | end 88 | end 89 | 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/aerial/config.rb: -------------------------------------------------------------------------------- 1 | module Aerial 2 | class Config 3 | 4 | class << self 5 | attr_accessor :config 6 | end 7 | 8 | # Default options. Overriden by values in config.yml or command-line opts. 9 | # (Strings rather symbols used for compatability with YAML). 10 | DEFAULTS = { 11 | 'title' => 'Aerial Title', 12 | 'subtitle' => 'Aerial Subtitle', 13 | 'name' => 'Aerial User Name', 14 | 'email' => 'Aerial User Email', 15 | 'server' => false, 16 | 'server_port' => 3000, 17 | 18 | 'source' => Dir.pwd, 19 | 'config' => 'config.yml', 20 | 'destination' => File.join(Dir.pwd, 'public', '_site'), 21 | 22 | 'markdown' => 'maruku', 23 | 'permalink' => 'date', 24 | 25 | 'articles' => { 26 | 'dir' => 'articles' 27 | }, 28 | 29 | 'views' => { 30 | 'dir' => 'views', 31 | 'default' => 'home' 32 | } 33 | } 34 | 35 | # Generate a Aerial configuration Hash by merging the default options 36 | # with anything in config.yml, and adding the given options on top. 37 | # 38 | # override - A Hash of config directives that override any options in both 39 | # the defaults and the config file. See Jekyll::DEFAULTS for a 40 | # list of option names and their defaults. 41 | # 42 | # Returns the final configuration Hash. 43 | def self.new(override) 44 | # _config.yml may override default source location, but until 45 | # then, we need to know where to look for _config.yml 46 | config_name = override['config'] || Aerial::Config::DEFAULTS['config'] 47 | source = override['source'] || Aerial::Config::DEFAULTS['source'] 48 | 49 | # Get configuration from /_config.yml 50 | config_file = File.join(source, config_name) 51 | 52 | begin 53 | config = YAML.load_file(config_file) 54 | raise "Invalid configuration - #{config_file}" if !config.is_a?(Hash) 55 | $stdout.puts "Configuration from #{config_file}" 56 | rescue => err 57 | $stderr.puts "WARNING: Could not read configuration. " + 58 | "Using defaults (and options)." 59 | $stderr.puts "\t" + err.to_s 60 | config = {} 61 | end 62 | 63 | # Merge DEFAULTS < _config.yml < override 64 | configs = Aerial::Config::DEFAULTS.deep_merge(config).deep_merge(override) 65 | self.nested_hash_to_openstruct(configs) 66 | end 67 | 68 | def method_missing(method_name, *attributes) 69 | if @config.respond_to?(method_name.to_sym) 70 | return @config.send(method_name.to_sym) 71 | else 72 | false 73 | end 74 | end 75 | 76 | private 77 | 78 | # Recursively convert nested Hashes into Openstructs 79 | def self.nested_hash_to_openstruct(obj) 80 | if obj.is_a? Hash 81 | obj.each { |key, value| obj[key] = nested_hash_to_openstruct(value) } 82 | OpenStruct.new(obj) 83 | else 84 | return obj 85 | end 86 | end 87 | 88 | end 89 | 90 | end 91 | -------------------------------------------------------------------------------- /lib/aerial/content.rb: -------------------------------------------------------------------------------- 1 | #require 'coderay' 2 | require 'date' 3 | 4 | module Aerial 5 | 6 | # Base class for all the site's content 7 | class Content 8 | 9 | attr_reader :id, :author, :title, :body, :publish_date, :archive_name, :file_name 10 | 11 | def initialize(atts = {}) 12 | atts.each_pair { |key, value| instance_variable_set("@#{key}", value) if self.respond_to? key} 13 | end 14 | 15 | protected 16 | 17 | # ============================================================================================= 18 | # PROTECTED CLASS METHODS 19 | # ============================================================================================= 20 | 21 | def self.extract_attributes(content, options={}) 22 | attributes = Hash.new 23 | header, body = content.split(/\n\n/, 2) 24 | attributes[:body] = body.strip if body 25 | 26 | header.each_line do |line| 27 | field, data = line.split(/:/, 2) 28 | field = field.downcase.strip.gsub(' ', '_').gsub('-', '_') 29 | attributes[field.to_sym] = data.to_s.strip 30 | begin 31 | attributes[:publish_date] = DateTime.parse(attributes[:publish_date]) 32 | rescue 33 | end 34 | end 35 | return attributes 36 | end 37 | 38 | # With help from Albino and Nokogiri, look for the pre tags and colorize 39 | # any code blocks we find. 40 | def self.colorize(html) 41 | doc = Nokogiri::HTML(html) 42 | doc.search("//pre[@lang]").each do |pre| 43 | pre.replace Albino.colorize(pre.text.rstrip, pre[:lang]) 44 | end 45 | doc.css('body/*').to_s 46 | end 47 | 48 | # ============================================================================================= 49 | # PROTECTED INSTANCE METHODS 50 | # ============================================================================================= 51 | 52 | # Ensure string contains valid ASCII characters 53 | def escape(string) 54 | return unless string 55 | result = String.new(string) 56 | result.gsub!(/[^\x00-\x7F]+/, '') # Remove anything non-ASCII entirely (e.g. diacritics). 57 | result.gsub!(/[^\w_ \-]+/i, '') # Remove unwanted chars. 58 | result.gsub!(/[ \-]+/i, '-') # No more than one of the separator in a row. 59 | result.gsub!(/^\-|\-$/i, '') # Remove leading/trailing separator. 60 | result.downcase! 61 | return result 62 | end 63 | 64 | end 65 | 66 | end 67 | -------------------------------------------------------------------------------- /lib/aerial/installer.rb: -------------------------------------------------------------------------------- 1 | require "thor" 2 | require File.dirname(__FILE__) + "/../aerial" 3 | 4 | module Aerial 5 | 6 | class Installer < Thor 7 | include FileUtils 8 | map "-T" => :list 9 | 10 | desc "install [PATH]", 11 | "Copy template files to PATH for desired deployement strategy 12 | (either Thin or Passenger). Next, go there and edit them." 13 | method_options :passenger => :boolean, :thin => :boolean 14 | def install(path = '.') 15 | @root = Pathname(path).expand_path 16 | create_dir_structure 17 | copy_config_files 18 | edit_config_files 19 | bootstrap 20 | puts post_install_message 21 | end 22 | 23 | desc "launch [CONFIG]", "Launch Aerial cartwheel." 24 | method_options :config => 'config.yml', :env => :development 25 | def server 26 | require 'thin' 27 | Aerial.new(options, options[:env] || :development) 28 | Aerial::App.set :root, Aerial.root 29 | Thin::Server.start("0.0.0.0", Aerial.config.server_port, Aerial::App) 30 | rescue LoadError => boom 31 | missing_dependency = boom.message.split("--").last.lstrip 32 | puts "Please install #{missing_dependency} to launch Aerial" 33 | end 34 | 35 | desc "overrides", "Allow aerial to be customized" 36 | def overrides 37 | @root = Pathname(".").expand_path 38 | create_app_file 39 | end 40 | 41 | desc "import", "Import articles from another blog." 42 | method_option :articles, :type => :hash, :default => {}, :required => true 43 | def import 44 | @root = Pathname(".").expand_path 45 | Aerial.new(@root, "/config.yml") 46 | Aerial::Migrator.new(options).process! 47 | rescue LoadError => boom 48 | missing_dependency = boom.message.split("--").last.lstrip 49 | puts "Please install #{missing_dependency} to import article into Aerial" 50 | end 51 | 52 | private 53 | 54 | # ========================================================================== 55 | # PRIVATE INSTANCE METHODS 56 | # ========================================================================== 57 | 58 | attr_reader :root 59 | 60 | def create_dir_structure 61 | mkdir_p root 62 | mkdir_p "#{root}/log" 63 | 64 | if options[:passenger] 65 | mkdir_p "#{root}/public" 66 | mkdir_p "#{root}/tmp" 67 | end 68 | end 69 | 70 | # Copy over all files need to run the app 71 | def bootstrap 72 | copy "views", "../../examples" 73 | create_initial_article 74 | end 75 | 76 | def create_initial_article 77 | copy "articles", "../../examples" 78 | end 79 | 80 | # Create a new repo if on none exists 81 | def initialize_repo 82 | unless File.exist?(File.join(root, '.git')) 83 | system "cd #{root}; git init" 84 | end 85 | end 86 | 87 | def initial_commit 88 | Aerial.new(root, "/config.yml") 89 | system "cd #{root}; git add ." 90 | Aerial::Git.commit("#{root}/articles", "Initial installation of Aerial") 91 | end 92 | 93 | # Rename and the sample config files 94 | def copy_config_files 95 | copy "config.sample.ru" 96 | copy "config.sample.yml" 97 | end 98 | 99 | # Customize the settings for the current location 100 | def edit_config_files 101 | edit_aerial_configuration 102 | end 103 | 104 | def edit_aerial_configuration 105 | config = File.read("#{root}/config.yml") 106 | config.gsub! %r(/var/log), "#{root}/log" 107 | File.open("#{root}/config.yml", "w") { |f| f.puts config } 108 | end 109 | 110 | def copy(source, path = "../../config") 111 | cp_r(Pathname(__FILE__).dirname.join(path, source), 112 | root.join(File.basename(source).gsub(/\.sample/, ""))) 113 | end 114 | 115 | def create_app_file 116 | unless File.exist?(File.join(root, 'app.rb')) 117 | File.open('app.rb', 'w') do |file| 118 | file.puts "module Aerial" 119 | file.puts " class App < Sinatra::Base" 120 | file.puts " helpers do" 121 | file.puts "" 122 | file.puts " end" 123 | file.puts " end" 124 | file.puts "end" 125 | end 126 | end 127 | end 128 | 129 | def post_install_message 130 | < 'mephisto'}) 16 | # 17 | # Returns a newly initialized Grit::Migrator. 18 | # Raises NameError if the migrator does not exist. 19 | # 20 | def initialize(options = {}) 21 | @provider = options['articles']['provider'] 22 | @dbname = options['articles']['dbname'] 23 | @user = options['articles']['user'] 24 | @pass = options['articles']['pass'] 25 | @host = options['articles']['host'] 26 | end 27 | 28 | def process! 29 | migrator = eval("Aerial::#{@provider.classify}") 30 | require 'aerial/migrators/#{migrator}' 31 | migrator.import(@dbname, @user, @pass, @host) 32 | rescue NameError 33 | puts "Oh sorry, #{@provider} isn't supported by Aerial" 34 | end 35 | 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/aerial/migrators/mephisto.rb: -------------------------------------------------------------------------------- 1 | require 'sequel' 2 | require 'sequel/extensions/inflector' 3 | require 'sequel/extensions/string_date_time' 4 | require 'fileutils' 5 | require 'yaml' 6 | require 'aerial/migrators/mephisto' 7 | require 'mcbean' 8 | 9 | module Aerial 10 | module Mephisto 11 | ARTICLES = "SELECT id, user_id, title, permalink, body, published_at FROM contents 12 | WHERE type = 'Article' ORDER BY published_at DESC" 13 | TAGS = "SELECT tags.name FROM tags 14 | INNER join taggings on tags.id = taggings.tag_id where taggable_id = " 15 | USERS = "SELECT login from users where users.id = " 16 | 17 | def self.import(dbname, user, pass, host = 'localhost') 18 | db = Sequel.mysql(dbname, :user => user, :password => pass, :host => host, :encoding => 'utf8') 19 | FileUtils.mkdir_p File.join(Aerial.root, Aerial.config.articles.dir) 20 | db[ARTICLES].each do |post| 21 | date = post[:published_at].to_time 22 | slug = post[:permalink] 23 | path = File.join(Aerial.root, Aerial.config.articles.dir, "#{date.strftime("%Y-%m-%d")}-#{slug}") 24 | tags = db[TAGS + post[:id].to_s].map(:name).join(',') 25 | user = db[USERS + post[:user_id].to_s].map(:login).join(',') 26 | body = McBean.fragment(post[:body].gsub("\r\n", "\n").gsub(/<[^>]*$/, "")).to_markdown.strip 27 | FileUtils.mkdir_p(path) 28 | File.open("#{path}/#{slug}.article", "w") do |f| 29 | f.puts "Title : #{post[:title]}" 30 | f.puts "Tags : #{tags}" 31 | f.puts "Publish Date : #{date.strftime("%Y-%m-%d %H:%M:%S")}" 32 | f.puts "Author : #{user}" 33 | f.puts "" 34 | f.puts body 35 | end 36 | end 37 | end 38 | 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/aerial/site.rb: -------------------------------------------------------------------------------- 1 | module Aerial 2 | class Site 3 | 4 | attr_accessor :config, :exclude 5 | 6 | def initialize 7 | self.config = Aerial.config 8 | self.exclude = ['blog', 'post', 'layout', 'not_found', 'rss', 'style'] 9 | end 10 | 11 | # Public: Loads a local Sinatra app if exists. 12 | # 13 | # name - The String file name of the app. 14 | # Traditionally it's called "app.rb". 15 | # 16 | # Returns Boolean if app exists and loads. 17 | # 18 | def self.include_local_app(name = 'app') 19 | require File.expand_path(File.join(Aerial.root, name)) 20 | rescue LoadError 21 | end 22 | 23 | def process! 24 | self.read_pages 25 | end 26 | 27 | def read_pages 28 | base = File.join(Aerial.root, 'views') 29 | return unless File.exists?(base) 30 | request = Rack::MockRequest.new(Aerial::App) 31 | pages = Dir.chdir(base) { filter_entries(Dir['**/*']) } 32 | end 33 | 34 | def filter_entries(entries) 35 | entries = entries.reject do |e| 36 | unless ['.htaccess'].include?(e) 37 | file_name = File.basename(e, File.extname(e)) 38 | ['.', '_', '#'].include?(file_name[0..0]) || file_name[-1..-1] == '~' || self.exclude.include?(file_name) || File.directory?(e) 39 | end 40 | end 41 | end 42 | 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/aerial/version.rb: -------------------------------------------------------------------------------- 1 | module Aerial 2 | VERSION = "0.2.0" 3 | end 4 | -------------------------------------------------------------------------------- /public/javascripts/application.js: -------------------------------------------------------------------------------- 1 | /*------------------------------------ 2 | Global Javascripts 3 | Aerial 4 | Version / 1.0 5 | Author / Matt Sears 6 | email / matt@mattsears.com 7 | website / www.mattsears.com 8 | -------------------------------------*/ 9 | 10 | /* When page is loaded 11 | ----------------------------*/ 12 | $(document).ready(function() { 13 | externalLinks(); 14 | }); 15 | -------------------------------------------------------------------------------- /spec/aerial_spec.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/spec_helper" 2 | 3 | describe 'main aerial application' do 4 | 5 | before do 6 | setup_repo 7 | end 8 | 9 | it "should load the configuration from the file" do 10 | Aerial.config.should_not be_nil 11 | Aerial.config.name.should == "Aerial" 12 | Aerial.config.public.dir.should == "public" 13 | Aerial.config.author.should == "Awesome Ruby Developor" 14 | Aerial.config.subtitle.should == "Article, Pages, and such" 15 | Aerial.config.email.should == "aerial@example.com" 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /spec/app_spec.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/spec_helper" 2 | 3 | describe 'main app application' do 4 | 5 | before do 6 | setup_repo 7 | @articles = Article.all 8 | end 9 | 10 | it 'should show the default index page' do 11 | get '/' 12 | last_response.status.should == 200 13 | end 14 | 15 | it 'should send the not_found action if page does not exists' do 16 | get '/does_not_exist.html' 17 | last_response.status.should == 200 18 | end 19 | 20 | it "should render a single article page" do 21 | get '/2009/1/31/test-article' 22 | last_response.status.should == 200 23 | end 24 | 25 | it "should render the home page" do 26 | get '/home' 27 | last_response.status.should == 200 28 | end 29 | 30 | describe "calling /style.css" do 31 | 32 | before do 33 | get '/style.css' 34 | end 35 | 36 | it "should return an okay status code" do 37 | last_response.status.should == 200 38 | end 39 | 40 | it "should return a css stylesheet" do 41 | last_response.headers["Content-Type"].should == "text/css;charset=utf-8" 42 | end 43 | 44 | end 45 | 46 | describe "calling /tags" do 47 | 48 | before do 49 | @article = Article.new(:title => "Test Article", 50 | :tags => "git, sinatra", 51 | :publish_date => DateTime.new, 52 | :file_name => "test-article") 53 | Aerial::Article.stub!(:with_tag).and_return([@article]) 54 | get '/tags/git' 55 | end 56 | 57 | it "should return a valid response" do 58 | last_response.status.should == 200 59 | end 60 | 61 | it "should return a response body" do 62 | last_response.body.should_not be_empty 63 | end 64 | 65 | end 66 | 67 | describe "calling /feed" do 68 | 69 | before do 70 | @articles = Article.find_all 71 | Aerial::Article.stub!(:all).and_return(@articles) 72 | get '/feed' 73 | end 74 | 75 | it "should produce an rss tag" do 76 | last_response.body.should have_tag('//rss') 77 | end 78 | 79 | it "should contain a title tag" do 80 | last_response.body.should have_tag('//title').with_text(Aerial.config.title) 81 | end 82 | 83 | it "should contain a language tag" do 84 | last_response.body.should have_tag('//language').with_text("en-us") 85 | end 86 | 87 | it "should contain a description tag that containts the subtitle" do 88 | last_response.body.should have_tag('//description').with_text(Aerial.config.subtitle) 89 | end 90 | 91 | it "should contain an item tag" do 92 | last_response.body.should have_tag('//item') 93 | end 94 | 95 | it "should have the title tags for the articles" do 96 | last_response.body.should have_tag('//item[1]/title').with_text(@articles[0].title) 97 | last_response.body.should have_tag('//item[2]/title').with_text(@articles[1].title) 98 | end 99 | 100 | it "should have the link tag with the articles permalink" do 101 | #last_response.body.should have_tag('//item[1]/link').with_text("http://#{@articles[0].permalink}") 102 | end 103 | 104 | it "should have a pubDate tag with the article's publication date" do 105 | last_response.body.should have_tag('//item[1]/pubDate').with_text(@articles[0].publish_date.to_s) 106 | last_response.body.should have_tag('//item[2]/pubDate').with_text(@articles[1].publish_date.to_s) 107 | end 108 | 109 | it "should have a guid date that matches the articles id" do 110 | last_response.body.should have_tag('//item[1]/guid').with_text(@articles[0].id) 111 | last_response.body.should have_tag('//item[2]/guid').with_text(@articles[1].id) 112 | end 113 | 114 | after do 115 | @articles = nil 116 | end 117 | 118 | end 119 | 120 | describe "calling /feed" do 121 | 122 | before do 123 | @articles = Article.find_all 124 | Aerial::Article.stub!(:all).and_return(@articles) 125 | get '/articles' 126 | end 127 | 128 | it "should return a valid response" do 129 | last_response.status.should == 200 130 | end 131 | 132 | it "should return a response body" do 133 | last_response.body.should_not be_empty 134 | end 135 | 136 | end 137 | 138 | 139 | describe "calling /archives" do 140 | before do 141 | @article = Article.new(:title => "Test Article", 142 | :body => "Test Content", 143 | :id => 333, 144 | :publish_date => DateTime.new, 145 | :file_name => "test-article.article") 146 | Aerial::Article.stub!(:with_date).and_return([@article]) 147 | get '/archives/year/month' 148 | end 149 | 150 | it "should return a valid response" do 151 | last_response.status.should == 200 152 | end 153 | 154 | end 155 | 156 | describe "calling Git operations" do 157 | 158 | before do 159 | @dir = "#{Aerial.repo.working_dir}/articles/new_dir" 160 | FileUtils.mkdir(@dir) 161 | FileUtils.cd(@dir) do 162 | FileUtils.touch 'new.file' 163 | end 164 | end 165 | 166 | it "should commit all new untracked and tracked content" do 167 | Aerial.repo.status.untracked.should_not be_empty 168 | get '/' 169 | # Aerial.repo.status.untracked.should be_empty 170 | end 171 | 172 | end 173 | 174 | end 175 | -------------------------------------------------------------------------------- /spec/article_spec.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/spec_helper" 2 | 3 | describe 'article' do 4 | 5 | before do 6 | setup_repo 7 | end 8 | 9 | describe "when finding an article" do 10 | 11 | before(:each) do 12 | @article = Article.with_name("test-article-one") 13 | end 14 | 15 | it "should find an article with .article extension " do 16 | @article.should_not be_nil 17 | end 18 | 19 | it "should assign the article's author" do 20 | @article.author.should == "Matt Sears" 21 | end 22 | 23 | it "should assign the article title" do 24 | @article.title.should == "This is the first article" 25 | end 26 | 27 | it "should assign the file name of the article" do 28 | @article.file_name.should == "test-article.article" 29 | end 30 | 31 | it "should assing a list of tags" do 32 | @article.tags.should == ["ruby", "sinatra", "git"] 33 | end 34 | 35 | it "should assign the article date" do 36 | @article.id.should_not be_empty 37 | end 38 | 39 | it "should assign the article a publication date" do 40 | @article.publish_date.should == DateTime.new(y=2009,m=1,d=31) 41 | end 42 | 43 | it "should assign the article a body attribute" do 44 | @article.body.should == "Lorem ipsum dolor sit amet, adipisicing **elit**, sed do eiusmod 45 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam" 46 | end 47 | 48 | it "should convert body text to body html" do 49 | @article.body_html.should have_tag('//strong').with_text('elit') 50 | end 51 | 52 | it "should calculate a permalink based on the directory the article is saved in" do 53 | @article.permalink.should == "/2009/1/31/test-article" 54 | end 55 | 56 | end 57 | 58 | describe "when opening the article with the blob id" do 59 | 60 | before(:each) do 61 | article_id = Article.with_name("test-article-one").id 62 | @article = Article.open(article_id, :fast => true) 63 | end 64 | 65 | it "should return a valid article object" do 66 | @article.should_not be_nil 67 | end 68 | 69 | it "should be an instance of an Article object" do 70 | @article.should be_instance_of(Article) 71 | end 72 | 73 | end 74 | 75 | describe "when finding the first article by the id" do 76 | 77 | before(:each) do 78 | article_id = Article.with_name("test-article-one").id 79 | @article = Article.find(article_id) 80 | end 81 | 82 | it "should return a valid article object with the id" do 83 | @article.should_not be_nil 84 | end 85 | 86 | it "should assign a file name to the article " do 87 | @article.file_name.should == "test-article.article" 88 | end 89 | 90 | it "should assign a tree id of the article" do 91 | @article.archive_name.should == "test-article-one" 92 | end 93 | 94 | it "should find the file path of where the article is stored" do 95 | @article.expand_path.should == "#{@repo_path}/articles/test-article-one/test-article.article" 96 | end 97 | 98 | it "should be an instance of an Article object" do 99 | @article.should be_instance_of(Article) 100 | end 101 | 102 | end 103 | 104 | describe "when articles don't exist" do 105 | 106 | it "should raise error when article could not be found" do 107 | lambda { 108 | @article = Article.find("doesn't exist") 109 | }.should raise_error(RuntimeError) 110 | end 111 | 112 | 113 | it "should raise error when article blob doesn't exist" do 114 | lambda { 115 | @article = Article.open("doesn't exists") 116 | }.should raise_error(RuntimeError) 117 | end 118 | 119 | end 120 | 121 | describe "when finding the second article by the id" do 122 | 123 | before(:each) do 124 | article_id = Article.with_name("test-article-two").id 125 | @article = Article.find(article_id) 126 | end 127 | 128 | it "should find the second article and not the first" do 129 | @article.should_not be_nil 130 | end 131 | 132 | it "should assign a tree id of the article" do 133 | @article.archive_name.should == "test-article-two" 134 | end 135 | 136 | end 137 | 138 | describe "when finding an article by permalink" do 139 | 140 | before(:each) do 141 | @article = Article.find_by_permalink("/2009/1/31/test-article") 142 | end 143 | 144 | it "should return an article with a valid permalink" do 145 | @article.should be_instance_of(Article) 146 | end 147 | 148 | it "should return nil if article can't be found" do 149 | Article.find_by_permalink("does-not-exist").should == false 150 | end 151 | 152 | after(:each) do 153 | @article = nil 154 | end 155 | end 156 | 157 | describe "finding all articles" do 158 | 159 | before do 160 | @articles = Article.find_all 161 | end 162 | 163 | it "should return an array of article objects" do 164 | @articles.should be_instance_of(Array) 165 | end 166 | 167 | it "should contain 5 articles" do 168 | @articles.size.should == 5 169 | end 170 | 171 | after do 172 | @articles = nil 173 | end 174 | 175 | end 176 | 177 | describe "finding all articles with a specific tag" do 178 | 179 | before do 180 | @tag = "sinatra" 181 | @articles = Article.with_tag(@tag) 182 | end 183 | 184 | it "should return an array of articles" do 185 | @articles.should be_instance_of(Array) 186 | end 187 | 188 | it "should contain 4 articles" do 189 | @articles.size.should == 4 190 | end 191 | 192 | it "should include articles with a specific task" do 193 | @articles.each { |article| article.tags.should include(@tag)} 194 | end 195 | 196 | end 197 | 198 | describe "finding all articles by publication date" do 199 | 200 | before do 201 | @articles = Article.with_date(2009, 12) 202 | end 203 | 204 | it "should return an array of articles" do 205 | @articles.should be_instance_of(Array) 206 | end 207 | 208 | it "should return 2 articles" do 209 | @articles.size.should == 2 210 | end 211 | 212 | it "should return 2 articles published in the 12th month" do 213 | @articles.each { |a| a.publish_date.month.should == 12} 214 | end 215 | 216 | it "should return 3 articles published in the year 2009" do 217 | @articles.each { |a| a.publish_date.year.should == 2009} 218 | end 219 | 220 | it "should find articles with dates in string format" do 221 | articles = Article.with_date("2009", "01") 222 | articles.should_not be_empty 223 | end 224 | 225 | end 226 | 227 | 228 | describe "calling Article.archives" do 229 | 230 | before do 231 | @archives = Article.archives 232 | end 233 | 234 | it "should return an array" do 235 | @archives.should be_instance_of(Hash) 236 | end 237 | 238 | it "should return a list of publication dates" do 239 | @archives.should == {["2009/01", "January 2009"]=>2, ["2009/03", "March 2009"]=>1, ["2009/12", "December 2009"]=>2} 240 | end 241 | 242 | end 243 | 244 | describe "calling Article.exists?" do 245 | 246 | it "should determine if an article exists" do 247 | Article.exists?("test-article-two").should == true 248 | end 249 | 250 | it "should return false when article doesn't exist" do 251 | Article.exists?("ghost-article").should == false 252 | end 253 | 254 | end 255 | 256 | describe "calling Article.recent" do 257 | 258 | before(:each) do 259 | @articles = Article.recent(:limit => 2) 260 | end 261 | 262 | it "should return an array of arricles" do 263 | @articles.should_not be_nil 264 | end 265 | 266 | it "should limit the number of articles" do 267 | @articles.size.should == 2 268 | end 269 | 270 | end 271 | 272 | describe "calling Article.tags" do 273 | 274 | before(:each) do 275 | @tags = Article.tags 276 | end 277 | 278 | it "should return an array even if empty" do 279 | @tags.should be_instance_of(Array) 280 | end 281 | 282 | it "should return a list of 4 tag strings" do 283 | @tags.size.should == 4 284 | end 285 | 286 | end 287 | 288 | end 289 | -------------------------------------------------------------------------------- /spec/base_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | require "#{File.dirname(__FILE__)}/spec_helper" 3 | 4 | describe 'article' do 5 | 6 | before do 7 | setup_repo 8 | end 9 | 10 | it "should provide an interface to the logger when debug mode is on" do 11 | Aerial.debug = true 12 | Aerial.log("testing the logger!").should == true 13 | end 14 | 15 | it "should not log messages when debug mode is off" do 16 | Aerial.debug = false 17 | Aerial.log("this should not log").should be_nil 18 | end 19 | 20 | describe Aerial::Helper do 21 | 22 | it "should return 'Never' for invalid dates" do 23 | humanized_date("Invalid Date").should == "Never" 24 | end 25 | 26 | it "should properly format a valid date" do 27 | humanized_date(DateTime.now).should_not == "Never" 28 | end 29 | 30 | it "should create a list of hyperlinks for each tag" do 31 | tags = ["ruby", "sinatra"] 32 | link_to_tags(tags).should == "ruby, sinatra" 33 | end 34 | 35 | end 36 | 37 | describe Aerial::Git do 38 | 39 | before do 40 | Aerial.repo.stub!(:add).and_return(true) 41 | Aerial.repo.stub!(:commit_index).and_return(true) 42 | Aerial.repo.status.untracked.stub!(:empty?).and_return(false) 43 | end 44 | 45 | it "should commit changes" do 46 | Aerial::Git.commit("/path/to/change", "message").should == true 47 | end 48 | 49 | it "should commit all changes" do 50 | Aerial::Git.commit_all.should == true 51 | end 52 | 53 | it "should add the remote repository " do 54 | Aerial::Git.push 55 | end 56 | 57 | end 58 | 59 | 60 | 61 | end 62 | -------------------------------------------------------------------------------- /spec/config_spec.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/spec_helper" 2 | 3 | describe 'system configuration settings' do 4 | 5 | it "should set theme directory" do 6 | Aerial.config.views.dir.should == "views" 7 | end 8 | 9 | it "should set a value for blog directory" do 10 | Aerial.config.articles.dir.should == "articles" 11 | end 12 | 13 | it "should define the directory for the theme directory" do 14 | Aerial.config.theme_directory.should == "#{AERIAL_ROOT}/views" 15 | end 16 | 17 | it "should define the directory for the public directory" do 18 | Aerial.config.public.dir.should == "public" 19 | end 20 | 21 | it "should return false if config does not contain a variable" do 22 | Aerial.config.undefined == false 23 | end 24 | 25 | end 26 | -------------------------------------------------------------------------------- /spec/fixtures/articles/congratulations-aerial-is-configured-correctly/congratulations-aerial-is-configured-correctly.article: -------------------------------------------------------------------------------- 1 | Title : Congratulations! Aerial is configured correctly 2 | Tags : ruby, sinatra, git, aerial 3 | Publish Date : 03/31/2009 4 | Author : Aerial 5 | 6 | Congratulations! Aerial appears to be up and running. This is a sample article created during the bootstrap process. You may overwrite this article or create a new one. 7 | -------------------------------------------------------------------------------- /spec/fixtures/articles/sample-article/sample-article.article: -------------------------------------------------------------------------------- 1 | Title : Sample article 2 | Tags : ruby, sinatra, git 3 | Publish Date : 01/31/2009 4 | Author : Aerial 5 | 6 | This is a sample article created during the bootstrap process. You may overwrite this article or create a new one. 7 | -------------------------------------------------------------------------------- /spec/fixtures/articles/test-article-one/test-article.article: -------------------------------------------------------------------------------- 1 | Title : This is the first article 2 | Tags : ruby, sinatra, git 3 | Publish Date : 01/31/2009 4 | Author : Matt Sears 5 | 6 | Lorem ipsum dolor sit amet, adipisicing **elit**, sed do eiusmod 7 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam -------------------------------------------------------------------------------- /spec/fixtures/articles/test-article-three/test-article.article: -------------------------------------------------------------------------------- 1 | Title : This is the third test article 2 | Tags : ruby, sinatra, git 3 | Publish Date : 12/25/2009 4 | Author : Matt Sears 5 | 6 | Lorem ipsum dolor sit amet, adipisicing **elit**, sed do eiusmod 7 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam -------------------------------------------------------------------------------- /spec/fixtures/articles/test-article-two/comment-missing-fields.comment: -------------------------------------------------------------------------------- 1 | Author : Anonymous Coward 2 | Email : anonymous@coward.com 3 | Homepage : 4 | Referrer : http://mattsears.com 5 | Publish Date : 10/26/2009 6 | IP : 127.0.0.1 7 | 8 | This is a comment with a few fields missing above 9 | -------------------------------------------------------------------------------- /spec/fixtures/articles/test-article-two/test-article.article: -------------------------------------------------------------------------------- 1 | Title : This is the second article 2 | Tags : ruby, git 3 | Publish Date : 12/15/2009 4 | Author : Matt Sears 5 | 6 | Lorem ipsum dolor sit amet, adipisicing **elit**, sed do eiusmod 7 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim venia -------------------------------------------------------------------------------- /spec/fixtures/articles/test-article-two/test-comment.comment: -------------------------------------------------------------------------------- 1 | Author : Anonymous Coward 2 | Email : anonymous@coward.com 3 | Homepage : http://littlelines.com 4 | Referrer : http://mattsears.com 5 | User-Agent : CERN-LineMode/2.15 libwww/2.17b3 6 | Publish Date : 10/26/2009 7 | IP : 127.0.0.1 8 | 9 | Lorem ipsum dolor sit amet, adipisicing **comment**, sed do eiusmod 10 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim venia 11 | -------------------------------------------------------------------------------- /spec/fixtures/config.yml: -------------------------------------------------------------------------------- 1 | title: "Aerial" 2 | subtitle: "Article, Pages, and such" 3 | name: "Aerial" 4 | author: "Awesome Ruby Developor" 5 | email: "aerial@example.com" 6 | 7 | articles: 8 | dir: "articles" 9 | 10 | public: 11 | dir: "public" 12 | 13 | views: 14 | default: "home" 15 | dir: "views" 16 | 17 | git: 18 | url: "git@github.com:mattsears/aerial.git" 19 | name: "origin" 20 | branch: "master" 21 | 22 | # If you want to add synchronizing via Github post-receive hooks, 23 | # insert some secure password here. Then set a "Post-Receive URL" 24 | # in Github administration to http://{YOUR SERVER}/sync?token={WHAT YOU SET BELOW} 25 | github_token: ~ 26 | 27 | # See http://soakedandsoaped.com/articles/2006/10/01/how-to-protect-a-rails-application-against-spam-with-akismet 28 | akismet: 29 | key: "{REPLACE WITH YOUR AKISMET KEY}" 30 | url: "{REPLACE WITH YOUR AKISMET URL}" 31 | 32 | # http://antispam.typepad.com/info/developers.html 33 | typekey_antispam: 34 | key: "{REPLACE WITH YOUR TYPEKEY ANTISPAM URL}" 35 | url: "{REPLACE WITH YOUR BLOG URL}" 36 | -------------------------------------------------------------------------------- /spec/fixtures/public/javascripts/application.js: -------------------------------------------------------------------------------- 1 | /*------------------------------------ 2 | Global Javascripts 3 | Aerial 4 | Version / 1.0 5 | Author / att Sears 6 | email / matt@mattsears.com 7 | website / www.mattsears.com 8 | -------------------------------------*/ 9 | 10 | /* When page is loaded 11 | ----------------------------*/ 12 | $(document).ready(function() { 13 | externalLinks(); 14 | }); 15 | 16 | // 17 | var Comment = { 18 | 19 | author: '', 20 | homepage: '', 21 | email: '', 22 | body: '', 23 | article: '', 24 | 25 | // Submit a new comment to the server via ajax 26 | submit: function(article_id) { 27 | 28 | this.author = $("input#comment_author").val(); 29 | this.homepage = $("input#comment_website").val(); 30 | this.email = $("input#comment_email").val(); 31 | this.body = $("textarea#comment_body").val(); 32 | this.article = article_id; 33 | 34 | // Make sure we have the required fields 35 | if (!this.valid()){ 36 | return false; 37 | } 38 | 39 | // Append a new comment if post is successful 40 | if (this.post()){ 41 | this.appendNew(); 42 | } 43 | }, 44 | 45 | // Post the comment back to the server 46 | post: function() { 47 | 48 | // Data posted to server 49 | var data = 'author='+ this.author + '&email=' + this.email + '&homepage=' + this.phone + '&body=' + this.body; 50 | var url = "/article/" + this.article + "/comments"; 51 | 52 | $.ajax({ 53 | type: "POST", 54 | url: url, 55 | data: data 56 | }); 57 | 58 | return true; 59 | }, 60 | 61 | // Add a div for the new comment 62 | appendNew: function() { 63 | 64 | // Template for the new comment div 65 | var t = $.template( 66 | "

${author}${date}

${message}

" 67 | ); 68 | 69 | // Append 70 | $("#comments").append( t , { 71 | author: this.author, 72 | homepage: this.homepage, 73 | message: this.body 74 | }); 75 | }, 76 | 77 | // Ensure all required fields are filled-in 78 | valid: function() { 79 | 80 | if (this.author == "") { 81 | $("#author_label").addClass("error"); 82 | return false; 83 | } 84 | 85 | if (this.email == "") { 86 | $("#email_label").addClass("error"); 87 | return false; 88 | } 89 | 90 | if (this.comment == "") { 91 | $("#comment_label").addClass("error"); 92 | return false; 93 | } 94 | return true; 95 | }, 96 | 97 | } 98 | 99 | // Make all 'external' links in a new window 100 | function externalLinks() { 101 | if (!document.getElementsByTagName) return; 102 | var anchors = document.getElementsByTagName("a"); 103 | for (var i = 0; i < anchors.length; i++) { 104 | var anchor = anchors[i]; 105 | if (anchor.getAttribute("href") && 106 | anchor.getAttribute("rel") == "external") 107 | anchor.target = "_blank"; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /spec/fixtures/public/javascripts/jquery.template.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jQuery Templates 3 | * 4 | * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) 5 | * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. 6 | * 7 | * Written by: Stan Lemon 8 | * 9 | * Based off of the Ext.Template library, available at: 10 | * http://www.extjs.com 11 | * 12 | * This library provides basic templating functionality, allowing for macro-based 13 | * templates within jQuery. 14 | * 15 | * Basic Usage: 16 | * 17 | * var t = $.template('
Hello ${name}, how are you ${question}? I am ${me:substr(0,10)}
'); 18 | * 19 | * $(selector).append( t , { 20 | * name: 'Stan', 21 | * question: 'feeling', 22 | * me: 'doing quite well myself, thank you very much!' 23 | * }); 24 | * 25 | * Requires: jQuery 1.2+ 26 | * 27 | * 28 | * @todo Add callbacks to the DOM manipulation methods, so that events can be bound 29 | * to template nodes after creation. 30 | */ 31 | (function($){ 32 | 33 | /** 34 | * Create a New Template 35 | */ 36 | $.template = function(html, options) { 37 | return new $.template.instance(html, options); 38 | }; 39 | 40 | /** 41 | * Template constructor - Creates a new template instance. 42 | * 43 | * @param html The string of HTML to be used for the template. 44 | * @param options An object of configurable options. Currently 45 | * you can toggle compile as a boolean value and set a custom 46 | * template regular expression on the property regx by 47 | * specifying the key of the regx to use from the regx object. 48 | */ 49 | $.template.instance = function(html, options) { 50 | // If a custom regular expression has been set, grab it from the regx object 51 | if ( options && options['regx'] ) options.regx = this.regx[ options.regx ]; 52 | 53 | this.options = $.extend({ 54 | compile: false, 55 | regx: this.regx.standard 56 | }, options || {}); 57 | 58 | this.html = html; 59 | 60 | if (this.options.compile) { 61 | this.compile(); 62 | } 63 | this.isTemplate = true; 64 | }; 65 | 66 | /** 67 | * Regular Expression for Finding Variables 68 | * 69 | * The default pattern looks for variables in JSP style, the form of: ${variable} 70 | * There are also regular expressions available for ext-style variables and 71 | * jTemplate style variables. 72 | * 73 | * You can add your own regular expressions for variable ussage by doing. 74 | * $.extend({ $.template.re , { 75 | * myvartype: /...../g 76 | * } 77 | * 78 | * Then when creating a template do: 79 | * var t = $.template("
...
", { regx: 'myvartype' }); 80 | */ 81 | $.template.regx = $.template.instance.prototype.regx = { 82 | jsp: /\$\{([\w-]+)(?:\:([\w\.]*)(?:\((.*?)?\))?)?\}/g, 83 | ext: /\{([\w-]+)(?:\:([\w\.]*)(?:\((.*?)?\))?)?\}/g, 84 | jtemplates: /\{\{([\w-]+)(?:\:([\w\.]*)(?:\((.*?)?\))?)?\}\}/g 85 | }; 86 | 87 | /** 88 | * Set the standard regular expression to be used. 89 | */ 90 | $.template.regx.standard = $.template.regx.jsp; 91 | 92 | /** 93 | * Variable Helper Methods 94 | * 95 | * This is a collection of methods which can be used within the variable syntax, ie: 96 | * ${variable:substr(0,30)} Which would only print a substring, 30 characters in length 97 | * begining at the first character for the variable named "variable". 98 | * 99 | * A basic substring helper is provided as an example of how you can define helpers. 100 | * To add more helpers simply do: 101 | * $.extend( $.template.helpers , { 102 | * sampleHelper: function() { ... } 103 | * }); 104 | */ 105 | $.template.helpers = $.template.instance.prototype.helpers = { 106 | substr : function(value, start, length){ 107 | return String(value).substr(start, length); 108 | } 109 | }; 110 | 111 | 112 | /** 113 | * Template Instance Methods 114 | */ 115 | $.extend( $.template.instance.prototype, { 116 | 117 | /** 118 | * Apply Values to a Template 119 | * 120 | * This is the macro-work horse of the library, it receives an object 121 | * and the properties of that objects are assigned to the template, where 122 | * the variables in the template represent keys within the object itself. 123 | * 124 | * @param values An object of properties mapped to template variables 125 | */ 126 | apply: function(values) { 127 | if (this.options.compile) { 128 | return this.compiled(values); 129 | } else { 130 | var tpl = this; 131 | var fm = this.helpers; 132 | 133 | var fn = function(m, name, format, args) { 134 | if (format) { 135 | if (format.substr(0, 5) == "this."){ 136 | return tpl.call(format.substr(5), values[name], values); 137 | } else { 138 | if (args) { 139 | // quoted values are required for strings in compiled templates, 140 | // but for non compiled we need to strip them 141 | // quoted reversed for jsmin 142 | var re = /^\s*['"](.*)["']\s*$/; 143 | args = args.split(','); 144 | 145 | for(var i = 0, len = args.length; i < len; i++) { 146 | args[i] = args[i].replace(re, "$1"); 147 | } 148 | args = [values[name]].concat(args); 149 | } else { 150 | args = [values[name]]; 151 | } 152 | 153 | return fm[format].apply(fm, args); 154 | } 155 | } else { 156 | return values[name] !== undefined ? values[name] : ""; 157 | } 158 | }; 159 | 160 | return this.html.replace(this.options.regx, fn); 161 | } 162 | }, 163 | 164 | /** 165 | * Compile a template for speedier usage 166 | */ 167 | compile: function() { 168 | var sep = $.browser.mozilla ? "+" : ","; 169 | var fm = this.helpers; 170 | 171 | var fn = function(m, name, format, args){ 172 | if (format) { 173 | args = args ? ',' + args : ""; 174 | 175 | if (format.substr(0, 5) != "this.") { 176 | format = "fm." + format + '('; 177 | } else { 178 | format = 'this.call("'+ format.substr(5) + '", '; 179 | args = ", values"; 180 | } 181 | } else { 182 | args= ''; format = "(values['" + name + "'] == undefined ? '' : "; 183 | } 184 | return "'"+ sep + format + "values['" + name + "']" + args + ")"+sep+"'"; 185 | }; 186 | 187 | var body; 188 | 189 | if ($.browser.mozilla) { 190 | body = "this.compiled = function(values){ return '" + 191 | this.html.replace(/\\/g, '\\\\').replace(/(\r\n|\n)/g, '\\n').replace(/'/g, "\\'").replace(this.options.regx, fn) + 192 | "';};"; 193 | } else { 194 | body = ["this.compiled = function(values){ return ['"]; 195 | body.push(this.html.replace(/\\/g, '\\\\').replace(/(\r\n|\n)/g, '\\n').replace(/'/g, "\\'").replace(this.options.regx, fn)); 196 | body.push("'].join('');};"); 197 | body = body.join(''); 198 | } 199 | eval(body); 200 | return this; 201 | } 202 | }); 203 | 204 | 205 | /** 206 | * Save a reference in this local scope to the original methods which we're 207 | * going to overload. 208 | **/ 209 | var $_old = { 210 | domManip: $.fn.domManip, 211 | text: $.fn.text, 212 | html: $.fn.html 213 | }; 214 | 215 | /** 216 | * Overwrite the domManip method so that we can use things like append() by passing a 217 | * template object and macro parameters. 218 | */ 219 | $.fn.domManip = function( args, table, reverse, callback ) { 220 | if (args[0].isTemplate) { 221 | // Apply the template and it's arguments... 222 | args[0] = args[0].apply( args[1] ); 223 | // Get rid of the arguements, we don't want to pass them on 224 | delete args[1]; 225 | } 226 | 227 | // Call the original method 228 | var r = $_old.domManip.apply(this, arguments); 229 | 230 | return r; 231 | }; 232 | 233 | /** 234 | * Overwrite the html() method 235 | */ 236 | $.fn.html = function( value , o ) { 237 | if (value && value.isTemplate) var value = value.apply( o ); 238 | 239 | var r = $_old.html.apply(this, [value]); 240 | 241 | return r; 242 | }; 243 | 244 | /** 245 | * Overwrite the text() method 246 | */ 247 | $.fn.text = function( value , o ) { 248 | if (value && value.isTemplate) var value = value.apply( o ); 249 | 250 | var r = $_old.text.apply(this, [value]); 251 | 252 | return r; 253 | }; 254 | 255 | })(jQuery); 256 | -------------------------------------------------------------------------------- /spec/fixtures/views/article.haml: -------------------------------------------------------------------------------- 1 | - article = @article if @article 2 | .article 3 | %h2 4 | %a{:href => article.permalink} 5 | =article.title 6 | %h3 7 | ="Posted by #{article.author} on #{humanized_date article.publish_date}" 8 | %p.entry 9 | =article.body_html 10 | .meta 11 | %span 12 | Tags: 13 | =link_to_tags article.tags 14 | %br/ 15 | %span 16 | Meta: 17 | %a{:href => "#{article.permalink}", :rel => 'permalink'} 18 | permalink 19 | -------------------------------------------------------------------------------- /spec/fixtures/views/articles.haml: -------------------------------------------------------------------------------- 1 | #articles 2 | =partial :article, :collection => @articles 3 | -------------------------------------------------------------------------------- /spec/fixtures/views/home.haml: -------------------------------------------------------------------------------- 1 | #articles 2 | =partial :article, :collection => @articles 3 | -------------------------------------------------------------------------------- /spec/fixtures/views/layout.haml: -------------------------------------------------------------------------------- 1 | !!! Strict 2 | %html 3 | %head 4 | %title= page_title 5 | %script{ 'type' => "text/javascript", :src => "/javascripts/jquery-1.3.1.min.js" } 6 | %script{ 'type' => "text/javascript", :src => "/javascripts/jquery.template.js" } 7 | %script{ 'type' => "text/javascript", :src => "/javascripts/application.js" } 8 | %link{:href => '/style.css', :rel => 'stylesheet', :type => 'text/css'} 9 | %link{:href => "#{base_url}/feed", :rel => 'alternate', :type => 'application/atom+xml', :title => "Feed for #{}" } 10 | %body 11 | #container 12 | #header 13 | #logo 14 | %h1 15 | %a{:href => '/'}= Aerial.config.title 16 | %span 17 | =Aerial.config.subtitle 18 | #content= yield 19 | #sidebar= partial :sidebar 20 | #footer 21 | %p#legal= "© #{Time.now.strftime('%Y')} #{Aerial.config.author}" 22 | %p#powered= "powered by Aerial" 23 | -------------------------------------------------------------------------------- /spec/fixtures/views/not_found.haml: -------------------------------------------------------------------------------- 1 | The page you requested could not be found. 2 | -------------------------------------------------------------------------------- /spec/fixtures/views/post.haml: -------------------------------------------------------------------------------- 1 | =partial :article 2 | -------------------------------------------------------------------------------- /spec/fixtures/views/rss.haml: -------------------------------------------------------------------------------- 1 | !!! XML 2 | %rss{"version" => "2.0"} 3 | %channel 4 | %title= "#{Aerial.config.title}" 5 | %link= "#{base_url}" 6 | %language= "en-us" 7 | %ttl= "40" 8 | %description= "#{Aerial.config.subtitle}" 9 | - @articles.each do |article| 10 | %item 11 | %title= article.title 12 | %link= full_hostname(article.permalink) 13 | %description= article.body_html 14 | %pubDate= article.publish_date 15 | %guid{"isPermaLink" => "false"}= article.id 16 | -------------------------------------------------------------------------------- /spec/fixtures/views/sidebar.haml: -------------------------------------------------------------------------------- 1 | #about 2 | %h2 About 3 | %p 4 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut 5 | labore et dolore magna aliqua. Ut enimad minim veniam, quis nostrud exercitation ullamco 6 | laboris nisi ut aliquip ex ea commodo consequat. 7 | 8 | #menu 9 | %h2 Menu 10 | %ul 11 | %li 12 | %a{:href => "/"}Home 13 | %li 14 | %a{:href => "/about"}About 15 | %h2 Recent Posts 16 | %ul 17 | -Aerial::Article.recent.each do |article| 18 | %li 19 | %a{:href => "#{article.permalink}"}=article.title 20 | %h2 Categories 21 | %ul 22 | -Aerial::Article.tags.each do |tag| 23 | %li 24 | %a{:href => "/tags/#{tag}"}=tag 25 | %h2 Archives 26 | %ul 27 | -Aerial::Article.archives.each do |archive| 28 | %li 29 | %a{:href => "/archives/#{archive[0][0]}"}="#{archive[0][1]} (#{archive[1]})" 30 | -------------------------------------------------------------------------------- /spec/fixtures/views/style.sass: -------------------------------------------------------------------------------- 1 | !red = #f00 2 | !black = #000 3 | !darkgrey = #555 4 | !lightgrey = #eeeeee 5 | !blue = #2168a6 6 | !red = #ed1e24 7 | 8 | =image-replacement 9 | :text-indent -9999px 10 | :margin-bottom 0.3em 11 | 12 | body 13 | :color = !black 14 | :font normal 12px Verdana, Arial, sans-serif 15 | :font-size 88% 16 | :line-height 1.5 17 | a 18 | :text-decoration none 19 | :color = !darkgrey 20 | &:visited 21 | :color = !darkgrey 22 | &:hover, &:visited 23 | :text-decoration underline 24 | h2 25 | :font-size 100% 26 | 27 | ul 28 | :padding 0 29 | li 30 | :list-style none 31 | :line-height 1.2 32 | :margin 0 0 5px 0 33 | a 34 | :text-decoration underline 35 | &:hover 36 | :text-decoration none 37 | form 38 | :background = !lightgrey 39 | :padding 10px 40 | :border-top 1px solid #ddd 41 | :font-size 100% 42 | p 43 | :margin 0 0 5px 0 44 | label 45 | :font-size 88% 46 | label.error 47 | :background-color = !red 48 | :padding 2px 49 | :color #fff 50 | input 51 | :border 1px solid 52 | :padding 2px 53 | :width 300px 54 | :border-color = #ddd 55 | :font-size 100% 56 | textarea 57 | :border 1px solid 58 | :border-color = #ddd 59 | :width 500px 60 | :padding 3px 61 | :height 75px 62 | :font normal 14px Verdana, Arial, sans-serif 63 | 64 | #header 65 | :height 60px 66 | :width 100% 67 | :border-bottom 1px dashed 68 | :border-color = !lightgrey 69 | #logo 70 | :float left 71 | :height 50px 72 | h1 73 | :font 300% arial, sans-serif 74 | :padding 5px 0 75 | :margin 0 76 | a 77 | :color = !blue 78 | :text-decoration none 79 | span 80 | :font-size 16pt 81 | :color = !darkgrey 82 | 83 | #container 84 | :width 800px 85 | :margin 0 auto 86 | 87 | #content 88 | :width 575px 89 | :float left 90 | :border-right 1px dashed 91 | :border-color = !lightgrey 92 | :padding 10px 10px 0 0 93 | h5 94 | :font-size 110% 95 | :background-color #ffd 96 | :margin 1.2em 0 0.3em 97 | :padding 3px 98 | :border-bottom 1px dotted #aaa 99 | .article, .page 100 | h2 101 | :color = !darkgrey 102 | :font-family arial, sans-serif 103 | :font-weight normal 104 | :letter-spacing -1px 105 | :font-size 28px 106 | :margin 0 0 -9px 0 107 | a 108 | :text-decoration none 109 | span 110 | :color = !lightgrey 111 | h3 112 | :color #777 113 | :font-weight normal 114 | :margin 0 0 0 2px 115 | :padding 0 116 | :font-size 110% 117 | :letter-spacing -0.5px 118 | .meta 119 | :font-size 8pt 120 | :background = !lightgrey 121 | :padding 5px 122 | :border 1px solid #ddd 123 | :margin 15px 0 124 | span 125 | :color = !darkgrey !important 126 | :font-weight bold 127 | 128 | .comment 129 | :margin 15px 0 130 | :padding 10px 131 | :border 3px solid 132 | :border-color = !lightgrey 133 | h2 134 | span 135 | :font-size 88% 136 | :margin-left 5px 137 | :color #777 138 | 139 | #sidebar 140 | :float right 141 | :width 200px 142 | :font-size 88% 143 | p 144 | :margin-top -7px 145 | a 146 | :color = !blue 147 | 148 | #footer 149 | :width 100% 150 | :height 100px 151 | :float left 152 | :margin-top 20px 153 | :border-top 1px dashed 154 | :border-color = !lightgrey 155 | p 156 | :margin-top 5px 157 | #legal 158 | :width 40% 159 | :float left 160 | #powered 161 | :width 50% 162 | :float right 163 | :text-align right 164 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.dirname(__FILE__) 2 | 3 | require 'rubygems' 4 | require 'hpricot' 5 | require 'sinatra' 6 | require 'git' 7 | require 'fileutils' 8 | require 'grit' 9 | require 'rack' 10 | require 'spec' 11 | require 'rack/test' 12 | 13 | def app 14 | Aerial::App.set :root, File.expand_path( File.join(File.dirname(__FILE__), 'repo') ) 15 | Aerial::App 16 | end 17 | 18 | # Helper for matching html tags 19 | module TagMatchers 20 | 21 | class TagMatcher 22 | 23 | def initialize (expected) 24 | @expected = expected 25 | @text = nil 26 | end 27 | 28 | def with_text (text) 29 | @text = text 30 | self 31 | end 32 | 33 | def matches? (target) 34 | @target = target 35 | doc = Hpricot(target) 36 | @elem = doc.at(@expected) 37 | @elem && (@text.nil? || @elem.inner_html == @text) 38 | end 39 | 40 | def failure_message 41 | "Expected #{match_message}" 42 | end 43 | 44 | def negative_failure_message 45 | "Did not expect #{match_message}" 46 | end 47 | 48 | protected 49 | 50 | def match_message 51 | if @elem 52 | "#{@elem} to have text #{@text} but got #{@elem.inner_html}" 53 | else 54 | "#{@target.inspect} to contain element #{@expected.inspect}" 55 | end 56 | end 57 | end 58 | 59 | def have_tag (expression) 60 | TagMatcher.new(expression) 61 | end 62 | 63 | end 64 | 65 | # Helpers for creating a test Git repo 66 | module GitHelper 67 | 68 | def new_git_repo 69 | delete_git_repo # delete the old repo first 70 | path = File.expand_path( File.join(File.dirname(__FILE__), 'repo') ) 71 | data = File.expand_path( File.join(File.dirname(__FILE__), 'fixtures') ) 72 | Dir.mkdir(path) 73 | Dir.chdir(path) do 74 | git = Git.init 75 | FileUtils.cp_r "#{data}/.", "#{path}/" 76 | git.add 77 | git.commit('Copied test articles from Fixtures directory so we can test against them') 78 | end 79 | return path 80 | end 81 | 82 | def delete_git_repo 83 | repo = File.join(File.dirname(__FILE__), 'repo') 84 | if File.directory? repo 85 | FileUtils.rm_rf repo 86 | end 87 | end 88 | 89 | def new_file(name, contents) 90 | File.open(name, 'w') do |f| 91 | f.puts contents 92 | end 93 | end 94 | 95 | end 96 | 97 | include GitHelper 98 | 99 | Spec::Runner.configure do |config| 100 | require 'yaml' 101 | repo_path = new_git_repo 102 | CONFIG = YAML.load_file( File.join(File.dirname(__FILE__), 'fixtures', 'config.yml') ) unless defined?(CONFIG) 103 | AERIAL_ROOT = File.join(File.dirname(__FILE__), 'repo') unless defined?(AERIAL_ROOT) 104 | 105 | require File.expand_path(File.dirname(__FILE__) + "/../lib/aerial") 106 | config.include TagMatchers 107 | config.include GitHelper 108 | config.include Rack::Test::Methods 109 | config.include Aerial 110 | config.include Aerial::Helper 111 | end 112 | 113 | # set test environment 114 | set :environment, :test 115 | set :run, false 116 | set :raise_errors, true 117 | set :logging, false 118 | set :views => File.join(File.dirname(__FILE__), "/..", "repo", "views"), 119 | :public => File.join(File.dirname(__FILE__), "/..", "repo", "public") 120 | 121 | include Aerial 122 | 123 | def setup_repo 124 | @repo_path = new_git_repo 125 | @config_path = File.join(@repo_path, "config") 126 | Aerial.stub!(:repo).and_return(Grit::Repo.new(@repo_path)) 127 | Aerial.new(@repo_path, "config.yml") 128 | end 129 | -------------------------------------------------------------------------------- /views/article.haml: -------------------------------------------------------------------------------- 1 | - article = @article if @article 2 | .article 3 | %h2 4 | %a{:href => article.permalink} 5 | =article.title 6 | %h3 7 | ="Posted by #{article.author} on #{humanized_date article.publish_date}" 8 | %p.entry 9 | =article.body_html 10 | .meta 11 | %span 12 | Tags: 13 | =link_to_tags article.tags 14 | %br/ 15 | %span 16 | Meta: 17 | %a{:href => "#{article.permalink}", :rel => 'permalink'} 18 | permalink 19 | 20 | -------------------------------------------------------------------------------- /views/articles.haml: -------------------------------------------------------------------------------- 1 | #articles 2 | =partial :article, :collection => @articles 3 | -------------------------------------------------------------------------------- /views/home.haml: -------------------------------------------------------------------------------- 1 | #articles 2 | =partial :article, :collection => @articles 3 | -------------------------------------------------------------------------------- /views/layout.haml: -------------------------------------------------------------------------------- 1 | !!! Strict 2 | %html 3 | %head 4 | %title= page_title 5 | %script{ 'type' => "text/javascript", :src => "/javascripts/jquery-1.3.1.min.js" } 6 | %script{ 'type' => "text/javascript", :src => "/javascripts/jquery.template.js" } 7 | %script{ 'type' => "text/javascript", :src => "/javascripts/application.js" } 8 | %link{:href => '/style.css', :rel => 'stylesheet', :type => 'text/css'} 9 | %link{:href => "#{base_url}/feed", :rel => 'alternate', :type => 'application/atom+xml', :title => "Feed for #{}" } 10 | %body 11 | #container 12 | #header 13 | #logo 14 | %h1 15 | %a{:href => '/'}= Aerial.config.title 16 | %span 17 | =Aerial.config.subtitle 18 | #content= yield 19 | #sidebar= partial :sidebar 20 | #footer 21 | %p#legal= "© #{Time.now.strftime('%Y')} #{Aerial.config.author}" 22 | %p#powered= "powered by Aerial" 23 | -------------------------------------------------------------------------------- /views/not_found.haml: -------------------------------------------------------------------------------- 1 | The page you requested could not be found. 2 | -------------------------------------------------------------------------------- /views/post.haml: -------------------------------------------------------------------------------- 1 | =partial :article 2 | -------------------------------------------------------------------------------- /views/rss.haml: -------------------------------------------------------------------------------- 1 | !!! XML 2 | %rss{"version" => "2.0"} 3 | %channel 4 | %title= "#{Aerial.config.title}" 5 | %link= "#{base_url}" 6 | %language= "en-us" 7 | %ttl= "40" 8 | %description= "#{Aerial.config.subtitle}" 9 | - @articles.each do |article| 10 | %item 11 | %title= article.title 12 | %link= full_hostname(article.permalink) 13 | %description= article.body_html 14 | %pubDate= article.publish_date 15 | %guid{"isPermaLink" => "false"}= article.id 16 | -------------------------------------------------------------------------------- /views/sidebar.haml: -------------------------------------------------------------------------------- 1 | #about 2 | %h2 About 3 | %p 4 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut 5 | labore et dolore magna aliqua. Ut enimad minim veniam, quis nostrud exercitation ullamco 6 | laboris nisi ut aliquip ex ea commodo consequat. 7 | 8 | #menu 9 | %h2 Menu 10 | %ul 11 | %li 12 | %a{:href => "/"}Home 13 | %li 14 | %a{:href => "/about"}About 15 | %h2 Recent Posts 16 | %ul 17 | -Aerial::Article.recent.each do |article| 18 | %li 19 | %a{:href => "#{article.permalink}"}=article.title 20 | %h2 Categories 21 | %ul 22 | -Aerial::Article.tags.each do |tag| 23 | %li 24 | %a{:href => "/tags/#{tag}"}=tag 25 | %h2 Archives 26 | %ul 27 | -Aerial::Article.archives.each do |archive| 28 | %li 29 | %a{:href => "/archives/#{archive[0][0]}"}="#{archive[0][1]} (#{archive[1]})" 30 | -------------------------------------------------------------------------------- /views/style.sass: -------------------------------------------------------------------------------- 1 | !red = #f00 2 | !black = #000 3 | !darkgrey = #555 4 | !lightgrey = #eeeeee 5 | !blue = #2168a6 6 | !red = #ed1e24 7 | 8 | =image-replacement 9 | :text-indent -9999px 10 | :margin-bottom 0.3em 11 | 12 | body 13 | :color = !black 14 | :font normal 12px Verdana, Arial, sans-serif 15 | :font-size 88% 16 | :line-height 1.5 17 | a 18 | :text-decoration none 19 | :color = !darkgrey 20 | &:visited 21 | :color = !darkgrey 22 | &:hover, &:visited 23 | :text-decoration underline 24 | h2 25 | :font-size 100% 26 | 27 | ul 28 | :padding 0 29 | li 30 | :list-style none 31 | :line-height 1.2 32 | :margin 0 0 5px 0 33 | a 34 | :text-decoration underline 35 | &:hover 36 | :text-decoration none 37 | form 38 | :background = !lightgrey 39 | :padding 10px 40 | :border-top 1px solid #ddd 41 | :font-size 100% 42 | p 43 | :margin 0 0 5px 0 44 | label 45 | :font-size 88% 46 | label.error 47 | :background-color = !red 48 | :padding 2px 49 | :color #fff 50 | input 51 | :border 1px solid 52 | :padding 2px 53 | :width 300px 54 | :border-color = #ddd 55 | :font-size 100% 56 | textarea 57 | :border 1px solid 58 | :border-color = #ddd 59 | :width 500px 60 | :padding 3px 61 | :height 75px 62 | :font normal 14px Verdana, Arial, sans-serif 63 | 64 | #header 65 | :height 60px 66 | :width 100% 67 | :border-bottom 1px dashed 68 | :border-color = !lightgrey 69 | #logo 70 | :float left 71 | :height 50px 72 | h1 73 | :font 300% arial, sans-serif 74 | :padding 5px 0 75 | :margin 0 76 | a 77 | :color = !blue 78 | :text-decoration none 79 | span 80 | :font-size 16pt 81 | :color = !darkgrey 82 | 83 | #container 84 | :width 800px 85 | :margin 0 auto 86 | 87 | #content 88 | :width 575px 89 | :float left 90 | :border-right 1px dashed 91 | :border-color = !lightgrey 92 | :padding 10px 10px 0 0 93 | h5 94 | :font-size 110% 95 | :background-color #ffd 96 | :margin 1.2em 0 0.3em 97 | :padding 3px 98 | :border-bottom 1px dotted #aaa 99 | .article, .page 100 | h2 101 | :color = !darkgrey 102 | :font-family arial, sans-serif 103 | :font-weight normal 104 | :letter-spacing -1px 105 | :font-size 28px 106 | :margin 0 0 -9px 0 107 | a 108 | :text-decoration none 109 | span 110 | :color = !lightgrey 111 | h3 112 | :color #777 113 | :font-weight normal 114 | :margin 0 0 0 2px 115 | :padding 0 116 | :font-size 110% 117 | :letter-spacing -0.5px 118 | .meta 119 | :font-size 8pt 120 | :background = !lightgrey 121 | :padding 5px 122 | :border 1px solid #ddd 123 | :margin 15px 0 124 | span 125 | :color = !darkgrey !important 126 | :font-weight bold 127 | 128 | .comment 129 | :margin 15px 0 130 | :padding 10px 131 | :border 3px solid 132 | :border-color = !lightgrey 133 | h2 134 | span 135 | :font-size 88% 136 | :margin-left 5px 137 | :color #777 138 | 139 | #sidebar 140 | :float right 141 | :width 200px 142 | :font-size 88% 143 | p 144 | :margin-top -7px 145 | a 146 | :color = !blue 147 | 148 | #footer 149 | :width 100% 150 | :height 100px 151 | :float left 152 | :margin-top 20px 153 | :border-top 1px dashed 154 | :border-color = !lightgrey 155 | p 156 | :margin-top 5px 157 | #legal 158 | :width 40% 159 | :float left 160 | #powered 161 | :width 50% 162 | :float right 163 | :text-align right 164 | --------------------------------------------------------------------------------