├── .gitignore ├── .rspec ├── .travis.yml ├── .watchr ├── CHANGES.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── bin └── schnitzelpress ├── lib ├── assets │ └── js │ │ ├── jquery-1.7.1.js │ │ ├── jquery-ujs.js │ │ ├── jquery.cookie.js │ │ └── schnitzelpress.js ├── public │ ├── .gitkeep │ ├── favicon.ico │ ├── font │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.svgz │ │ ├── fontawesome-webfont.ttf │ │ └── fontawesome-webfont.woff │ ├── img │ │ └── background.png │ └── moo.txt ├── schnitzelpress.rb ├── schnitzelpress │ ├── actions │ │ ├── admin.rb │ │ ├── assets.rb │ │ ├── auth.rb │ │ └── blog.rb │ ├── app.rb │ ├── cache_control.rb │ ├── cli.rb │ ├── config.rb │ ├── env.rb │ ├── helpers.rb │ ├── markdown_renderer.rb │ ├── post.rb │ ├── static.rb │ └── version.rb ├── templates │ └── new_blog │ │ ├── .gitignore │ │ ├── Gemfile │ │ ├── Gemfile.lock.tt │ │ ├── Procfile │ │ └── config.ru.tt └── views │ ├── 404.haml │ ├── admin │ ├── admin.haml │ ├── config.haml │ ├── edit.haml │ └── new.haml │ ├── atom.haml │ ├── blog.scss │ ├── index.haml │ ├── layout.haml │ ├── login.haml │ ├── partials │ ├── _admin_post_list.haml │ ├── _disqus.haml │ ├── _form_field.haml │ ├── _gauges.haml │ ├── _google_analytics.haml │ ├── _gosquared.haml │ ├── _post.haml │ └── _post_form.haml │ ├── post.haml │ └── schnitzelpress.scss ├── schnitzelpress.gemspec └── spec ├── app_spec.rb ├── assets_spec.rb ├── factories.rb ├── post_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | .rbfu-version 7 | .powenv 8 | .sass-cache 9 | InstalledFiles 10 | _yardoc 11 | coverage 12 | doc/ 13 | lib/bundler/man 14 | pkg 15 | rdoc 16 | spec/reports 17 | test/tmp 18 | test/version_tmp 19 | tmp 20 | *.sublime* 21 | Gemfile.lock 22 | .DS_Store 23 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.8.7 4 | - 1.9.2 5 | - 1.9.3 6 | #- jruby-19mode # JRuby in 1.9 mode 7 | # - rbx-18mode 8 | # - rbx-19mode # currently in active development, may or may not work for your project 9 | # uncomment this line if your project needs to run something other than `rake`: 10 | # script: bundle exec rspec spec 11 | -------------------------------------------------------------------------------- /.watchr: -------------------------------------------------------------------------------- 1 | def run(cmd, msg = nil) 2 | puts "=== %s" % msg if msg 3 | puts "=== %s" % cmd 4 | system cmd 5 | puts "\n" 6 | end 7 | 8 | watch("spec/.*_spec\.rb") { |m| run("bundle exec rspec %s" % m[0]) } 9 | watch("lib/schnitzelpress/(.*)\.rb") { |m| run("bundle exec rspec spec/%s_spec.rb" % m[1]) } 10 | watch('^spec/(spec_helper|factories)\.rb') { |f| run "bundle exec rake spec", "%s.rb has been modified" % f } 11 | 12 | # Ctrl-\ 13 | Signal.trap('QUIT') { run("bundle exec rake spec") } 14 | # Ctrl-C 15 | Signal.trap('INT') { abort("\nQuitting.") } 16 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## 0.2.0 2 | 3 | Upgrade Notes: 4 | 5 | * SchnitzelPress has been rebranded to Schnitzelpress (lower-case p; better now than later). If you've made any modifications to the host app's code, you'll probably need to make changes to reflect this. 6 | * The code you need to push to Heroku (or your own server) has been significantly thinned down. We're now providing a skeleton app that you can download to set up or update your blog. Find details on www.schnitzelpress.org. 7 | * Configuration is now stored in the database and can be edited from the new "Configuration" page in your Admin Panel; this obviously means that some of the stuff happening within your Schnitzelpress 0.1.x host application needs to be removed and re-entered in the web configuration. 8 | * Schnitzelpress now expects an environment variable to be present named SCHNITZELPRESS_OWNER, containing the email address of the admin user. On Heroku, you can add this through the `heroku config:add` command. 9 | 10 | Changes: 11 | 12 | * By popular request (haha), you can now delete posts. 13 | * The various available rake tasks have been moved to the `schnitzelpress` command line tool. 14 | * Most of your blog's configuration is now stored in MongoDB and can be modified from the new "Configuration" page in your the admin panel. 15 | * Post with dates now use double-digit days and months in their canonical URLs. (Your existing posts will forward to the new canonical URLs automatically.) 16 | * When logged in as an admin, you will be shown a small admin actions panel in the upper right corner of your browser, allowing you to quickly edit posts, jump to the admin section, or log out. 17 | * Schnitzelpress now has a light-weight, custom-built asset pipeline that serves all Javascripts and Stylesheets as one single file each, compressed and ready for hardcore caching. 18 | * When running Schnitzelpress locally (aka: development mode), you can use a simple developer-only login provider to log into your blog for testing purposes. 19 | * Various performance improvements. 20 | 21 | ## 0.1.1 (2012-02-26) 22 | 23 | * Add improved caching of post views and post indices. 24 | * Minor bugfixes. 25 | 26 | ## 0.1.0 (2012-02-25) 27 | 28 | * Initial Release 29 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in schnitzelpress.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Hendrik Mans 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Schnitzelpress [](http://unmaintained.tech/) 2 | 3 | ## A lean, mean blogging machine for hackers and fools. 4 | 5 | ### http://schnitzelpress.org 6 | 7 | [](http://travis-ci.org/teamschnitzel/schnitzelpress) 8 | 9 | ### [Sites powered by Schnitzelpress](https://github.com/hmans/schnitzelpress/wiki/Sites-powered-by-Schnitzelpress) 10 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require "bundler/gem_tasks" 3 | 4 | # integrate rspec 5 | require 'rspec/core/rake_task' 6 | RSpec::Core::RakeTask.new('spec') 7 | task :default => :spec 8 | -------------------------------------------------------------------------------- /bin/schnitzelpress: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "schnitzelpress/cli" 4 | 5 | Schnitzelpress::Cli.start 6 | -------------------------------------------------------------------------------- /lib/assets/js/jquery-ujs.js: -------------------------------------------------------------------------------- 1 | (function($, undefined) { 2 | 3 | /** 4 | * Unobtrusive scripting adapter for jQuery 5 | * 6 | * Requires jQuery 1.6.0 or later. 7 | * https://github.com/rails/jquery-ujs 8 | 9 | * Uploading file using rails.js 10 | * ============================= 11 | * 12 | * By default, browsers do not allow files to be uploaded via AJAX. As a result, if there are any non-blank file fields 13 | * in the remote form, this adapter aborts the AJAX submission and allows the form to submit through standard means. 14 | * 15 | * The `ajax:aborted:file` event allows you to bind your own handler to process the form submission however you wish. 16 | * 17 | * Ex: 18 | * $('form').live('ajax:aborted:file', function(event, elements){ 19 | * // Implement own remote file-transfer handler here for non-blank file inputs passed in `elements`. 20 | * // Returning false in this handler tells rails.js to disallow standard form submission 21 | * return false; 22 | * }); 23 | * 24 | * The `ajax:aborted:file` event is fired when a file-type input is detected with a non-blank value. 25 | * 26 | * Third-party tools can use this hook to detect when an AJAX file upload is attempted, and then use 27 | * techniques like the iframe method to upload the file instead. 28 | * 29 | * Required fields in rails.js 30 | * =========================== 31 | * 32 | * If any blank required inputs (required="required") are detected in the remote form, the whole form submission 33 | * is canceled. Note that this is unlike file inputs, which still allow standard (non-AJAX) form submission. 34 | * 35 | * The `ajax:aborted:required` event allows you to bind your own handler to inform the user of blank required inputs. 36 | * 37 | * !! Note that Opera does not fire the form's submit event if there are blank required inputs, so this event may never 38 | * get fired in Opera. This event is what causes other browsers to exhibit the same submit-aborting behavior. 39 | * 40 | * Ex: 41 | * $('form').live('ajax:aborted:required', function(event, elements){ 42 | * // Returning false in this handler tells rails.js to submit the form anyway. 43 | * // The blank required inputs are passed to this function in `elements`. 44 | * return ! confirm("Would you like to submit the form with missing info?"); 45 | * }); 46 | */ 47 | 48 | // Shorthand to make it a little easier to call public rails functions from within rails.js 49 | var rails; 50 | 51 | $.rails = rails = { 52 | // Link elements bound by jquery-ujs 53 | linkClickSelector: 'a[data-confirm], a[data-method], a[data-remote], a[data-disable-with]', 54 | 55 | // Select elements bound by jquery-ujs 56 | inputChangeSelector: 'select[data-remote], input[data-remote], textarea[data-remote]', 57 | 58 | // Form elements bound by jquery-ujs 59 | formSubmitSelector: 'form', 60 | 61 | // Form input elements bound by jquery-ujs 62 | formInputClickSelector: 'form input[type=submit], form input[type=image], form button[type=submit], form button:not(button[type])', 63 | 64 | // Form input elements disabled during form submission 65 | disableSelector: 'input[data-disable-with], button[data-disable-with], textarea[data-disable-with]', 66 | 67 | // Form input elements re-enabled after form submission 68 | enableSelector: 'input[data-disable-with]:disabled, button[data-disable-with]:disabled, textarea[data-disable-with]:disabled', 69 | 70 | // Form required input elements 71 | requiredInputSelector: 'input[name][required]:not([disabled]),textarea[name][required]:not([disabled])', 72 | 73 | // Form file input elements 74 | fileInputSelector: 'input:file', 75 | 76 | // Link onClick disable selector with possible reenable after remote submission 77 | linkDisableSelector: 'a[data-disable-with]', 78 | 79 | // Make sure that every Ajax request sends the CSRF token 80 | CSRFProtection: function(xhr) { 81 | var token = $('meta[name="csrf-token"]').attr('content'); 82 | if (token) xhr.setRequestHeader('X-CSRF-Token', token); 83 | }, 84 | 85 | // Triggers an event on an element and returns false if the event result is false 86 | fire: function(obj, name, data) { 87 | var event = $.Event(name); 88 | obj.trigger(event, data); 89 | return event.result !== false; 90 | }, 91 | 92 | // Default confirm dialog, may be overridden with custom confirm dialog in $.rails.confirm 93 | confirm: function(message) { 94 | return confirm(message); 95 | }, 96 | 97 | // Default ajax function, may be overridden with custom function in $.rails.ajax 98 | ajax: function(options) { 99 | return $.ajax(options); 100 | }, 101 | 102 | // Submits "remote" forms and links with ajax 103 | handleRemote: function(element) { 104 | var method, url, data, 105 | crossDomain = element.data('cross-domain') || null, 106 | dataType = element.data('type') || ($.ajaxSettings && $.ajaxSettings.dataType), 107 | options; 108 | 109 | if (rails.fire(element, 'ajax:before')) { 110 | 111 | if (element.is('form')) { 112 | method = element.attr('method'); 113 | url = element.attr('action'); 114 | data = element.serializeArray(); 115 | // memoized value from clicked submit button 116 | var button = element.data('ujs:submit-button'); 117 | if (button) { 118 | data.push(button); 119 | element.data('ujs:submit-button', null); 120 | } 121 | } else if (element.is(rails.inputChangeSelector)) { 122 | method = element.data('method'); 123 | url = element.data('url'); 124 | data = element.serialize(); 125 | if (element.data('params')) data = data + "&" + element.data('params'); 126 | } else { 127 | method = element.data('method'); 128 | url = element.attr('href'); 129 | data = element.data('params') || null; 130 | } 131 | 132 | options = { 133 | type: method || 'GET', data: data, dataType: dataType, crossDomain: crossDomain, 134 | // stopping the "ajax:beforeSend" event will cancel the ajax request 135 | beforeSend: function(xhr, settings) { 136 | if (settings.dataType === undefined) { 137 | xhr.setRequestHeader('accept', '*/*;q=0.5, ' + settings.accepts.script); 138 | } 139 | return rails.fire(element, 'ajax:beforeSend', [xhr, settings]); 140 | }, 141 | success: function(data, status, xhr) { 142 | element.trigger('ajax:success', [data, status, xhr]); 143 | }, 144 | complete: function(xhr, status) { 145 | element.trigger('ajax:complete', [xhr, status]); 146 | }, 147 | error: function(xhr, status, error) { 148 | element.trigger('ajax:error', [xhr, status, error]); 149 | } 150 | }; 151 | // Only pass url to `ajax` options if not blank 152 | if (url) { options.url = url; } 153 | 154 | return rails.ajax(options); 155 | } else { 156 | return false; 157 | } 158 | }, 159 | 160 | // Handles "data-method" on links such as: 161 | // Delete 162 | handleMethod: function(link) { 163 | var href = link.attr('href'), 164 | method = link.data('method'), 165 | target = link.attr('target'), 166 | csrf_token = $('meta[name=csrf-token]').attr('content'), 167 | csrf_param = $('meta[name=csrf-param]').attr('content'), 168 | form = $('
'), 169 | metadata_input = ''; 170 | 171 | if (csrf_param !== undefined && csrf_token !== undefined) { 172 | metadata_input += ''; 173 | } 174 | 175 | if (target) { form.attr('target', target); } 176 | 177 | form.hide().append(metadata_input).appendTo('body'); 178 | form.submit(); 179 | }, 180 | 181 | /* Disables form elements: 182 | - Caches element value in 'ujs:enable-with' data store 183 | - Replaces element text with value of 'data-disable-with' attribute 184 | - Sets disabled property to true 185 | */ 186 | disableFormElements: function(form) { 187 | form.find(rails.disableSelector).each(function() { 188 | var element = $(this), method = element.is('button') ? 'html' : 'val'; 189 | element.data('ujs:enable-with', element[method]()); 190 | element[method](element.data('disable-with')); 191 | element.prop('disabled', true); 192 | }); 193 | }, 194 | 195 | /* Re-enables disabled form elements: 196 | - Replaces element text with cached value from 'ujs:enable-with' data store (created in `disableFormElements`) 197 | - Sets disabled property to false 198 | */ 199 | enableFormElements: function(form) { 200 | form.find(rails.enableSelector).each(function() { 201 | var element = $(this), method = element.is('button') ? 'html' : 'val'; 202 | if (element.data('ujs:enable-with')) element[method](element.data('ujs:enable-with')); 203 | element.prop('disabled', false); 204 | }); 205 | }, 206 | 207 | /* For 'data-confirm' attribute: 208 | - Fires `confirm` event 209 | - Shows the confirmation dialog 210 | - Fires the `confirm:complete` event 211 | 212 | Returns `true` if no function stops the chain and user chose yes; `false` otherwise. 213 | Attaching a handler to the element's `confirm` event that returns a `falsy` value cancels the confirmation dialog. 214 | Attaching a handler to the element's `confirm:complete` event that returns a `falsy` value makes this function 215 | return false. The `confirm:complete` event is fired whether or not the user answered true or false to the dialog. 216 | */ 217 | allowAction: function(element) { 218 | var message = element.data('confirm'), 219 | answer = false, callback; 220 | if (!message) { return true; } 221 | 222 | if (rails.fire(element, 'confirm')) { 223 | answer = rails.confirm(message); 224 | callback = rails.fire(element, 'confirm:complete', [answer]); 225 | } 226 | return answer && callback; 227 | }, 228 | 229 | // Helper function which checks for blank inputs in a form that match the specified CSS selector 230 | blankInputs: function(form, specifiedSelector, nonBlank) { 231 | var inputs = $(), input, 232 | selector = specifiedSelector || 'input,textarea'; 233 | form.find(selector).each(function() { 234 | input = $(this); 235 | // Collect non-blank inputs if nonBlank option is true, otherwise, collect blank inputs 236 | if (nonBlank ? input.val() : !input.val()) { 237 | inputs = inputs.add(input); 238 | } 239 | }); 240 | return inputs.length ? inputs : false; 241 | }, 242 | 243 | // Helper function which checks for non-blank inputs in a form that match the specified CSS selector 244 | nonBlankInputs: function(form, specifiedSelector) { 245 | return rails.blankInputs(form, specifiedSelector, true); // true specifies nonBlank 246 | }, 247 | 248 | // Helper function, needed to provide consistent behavior in IE 249 | stopEverything: function(e) { 250 | $(e.target).trigger('ujs:everythingStopped'); 251 | e.stopImmediatePropagation(); 252 | return false; 253 | }, 254 | 255 | // find all the submit events directly bound to the form and 256 | // manually invoke them. If anyone returns false then stop the loop 257 | callFormSubmitBindings: function(form, event) { 258 | var events = form.data('events'), continuePropagation = true; 259 | if (events !== undefined && events['submit'] !== undefined) { 260 | $.each(events['submit'], function(i, obj){ 261 | if (typeof obj.handler === 'function') return continuePropagation = obj.handler(event); 262 | }); 263 | } 264 | return continuePropagation; 265 | }, 266 | 267 | // replace element's html with the 'data-disable-with' after storing original html 268 | // and prevent clicking on it 269 | disableElement: function(element) { 270 | element.data('ujs:enable-with', element.html()); // store enabled state 271 | element.html(element.data('disable-with')); // set to disabled state 272 | element.bind('click.railsDisable', function(e) { // prevent further clicking 273 | return rails.stopEverything(e) 274 | }); 275 | }, 276 | 277 | // restore element to its original state which was disabled by 'disableElement' above 278 | enableElement: function(element) { 279 | if (element.data('ujs:enable-with') !== undefined) { 280 | element.html(element.data('ujs:enable-with')); // set to old enabled state 281 | // this should be element.removeData('ujs:enable-with') 282 | // but, there is currently a bug in jquery which makes hyphenated data attributes not get removed 283 | element.data('ujs:enable-with', false); // clean up cache 284 | } 285 | element.unbind('click.railsDisable'); // enable element 286 | } 287 | 288 | }; 289 | 290 | $.ajaxPrefilter(function(options, originalOptions, xhr){ if ( !options.crossDomain ) { rails.CSRFProtection(xhr); }}); 291 | 292 | $(document).delegate(rails.linkDisableSelector, 'ajax:complete', function() { 293 | rails.enableElement($(this)); 294 | }); 295 | 296 | $(document).delegate(rails.linkClickSelector, 'click.rails', function(e) { 297 | var link = $(this), method = link.data('method'), data = link.data('params'); 298 | if (!rails.allowAction(link)) return rails.stopEverything(e); 299 | 300 | if (link.is(rails.linkDisableSelector)) rails.disableElement(link); 301 | 302 | if (link.data('remote') !== undefined) { 303 | if ( (e.metaKey || e.ctrlKey) && (!method || method === 'GET') && !data ) { return true; } 304 | 305 | if (rails.handleRemote(link) === false) { rails.enableElement(link); } 306 | return false; 307 | 308 | } else if (link.data('method')) { 309 | rails.handleMethod(link); 310 | return false; 311 | } 312 | }); 313 | 314 | $(document).delegate(rails.inputChangeSelector, 'change.rails', function(e) { 315 | var link = $(this); 316 | if (!rails.allowAction(link)) return rails.stopEverything(e); 317 | 318 | rails.handleRemote(link); 319 | return false; 320 | }); 321 | 322 | $(document).delegate(rails.formSubmitSelector, 'submit.rails', function(e) { 323 | var form = $(this), 324 | remote = form.data('remote') !== undefined, 325 | blankRequiredInputs = rails.blankInputs(form, rails.requiredInputSelector), 326 | nonBlankFileInputs = rails.nonBlankInputs(form, rails.fileInputSelector); 327 | 328 | if (!rails.allowAction(form)) return rails.stopEverything(e); 329 | 330 | // skip other logic when required values are missing or file upload is present 331 | if (blankRequiredInputs && form.attr("novalidate") == undefined && rails.fire(form, 'ajax:aborted:required', [blankRequiredInputs])) { 332 | return rails.stopEverything(e); 333 | } 334 | 335 | if (remote) { 336 | if (nonBlankFileInputs) { 337 | return rails.fire(form, 'ajax:aborted:file', [nonBlankFileInputs]); 338 | } 339 | 340 | // If browser does not support submit bubbling, then this live-binding will be called before direct 341 | // bindings. Therefore, we should directly call any direct bindings before remotely submitting form. 342 | if (!$.support.submitBubbles && $().jquery < '1.7' && rails.callFormSubmitBindings(form, e) === false) return rails.stopEverything(e); 343 | 344 | rails.handleRemote(form); 345 | return false; 346 | 347 | } else { 348 | // slight timeout so that the submit button gets properly serialized 349 | setTimeout(function(){ rails.disableFormElements(form); }, 13); 350 | } 351 | }); 352 | 353 | $(document).delegate(rails.formInputClickSelector, 'click.rails', function(event) { 354 | var button = $(this); 355 | 356 | if (!rails.allowAction(button)) return rails.stopEverything(event); 357 | 358 | // register the pressed submit button 359 | var name = button.attr('name'), 360 | data = name ? {name:name, value:button.val()} : null; 361 | 362 | button.closest('form').data('ujs:submit-button', data); 363 | }); 364 | 365 | $(document).delegate(rails.formSubmitSelector, 'ajax:beforeSend.rails', function(event) { 366 | if (this == event.target) rails.disableFormElements($(this)); 367 | }); 368 | 369 | $(document).delegate(rails.formSubmitSelector, 'ajax:complete.rails', function(event) { 370 | if (this == event.target) rails.enableFormElements($(this)); 371 | }); 372 | 373 | })( jQuery ); 374 | -------------------------------------------------------------------------------- /lib/assets/js/jquery.cookie.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Cookie Plugin 3 | * https://github.com/carhartl/jquery-cookie 4 | * 5 | * Copyright 2011, Klaus Hartl 6 | * Dual licensed under the MIT or GPL Version 2 licenses. 7 | * http://www.opensource.org/licenses/mit-license.php 8 | * http://www.opensource.org/licenses/GPL-2.0 9 | */ 10 | (function($) { 11 | $.cookie = function(key, value, options) { 12 | 13 | // key and at least value given, set cookie... 14 | if (arguments.length > 1 && (!/Object/.test(Object.prototype.toString.call(value)) || value === null || value === undefined)) { 15 | options = $.extend({}, options); 16 | 17 | if (value === null || value === undefined) { 18 | options.expires = -1; 19 | } 20 | 21 | if (typeof options.expires === 'number') { 22 | var days = options.expires, t = options.expires = new Date(); 23 | t.setDate(t.getDate() + days); 24 | } 25 | 26 | value = String(value); 27 | 28 | return (document.cookie = [ 29 | encodeURIComponent(key), '=', options.raw ? value : encodeURIComponent(value), 30 | options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE 31 | options.path ? '; path=' + options.path : '', 32 | options.domain ? '; domain=' + options.domain : '', 33 | options.secure ? '; secure' : '' 34 | ].join('')); 35 | } 36 | 37 | // key and possibly options given, get cookie... 38 | options = value || {}; 39 | var decode = options.raw ? function(s) { return s; } : decodeURIComponent; 40 | 41 | var pairs = document.cookie.split('; '); 42 | for (var i = 0, pair; pair = pairs[i] && pairs[i].split('='); i++) { 43 | if (decode(pair[0]) === key) return decode(pair[1] || ''); // IE saves cookies with empty string as "c; ", e.g. without "=" as opposed to EOMB, thus pair[1] may be undefined 44 | } 45 | return null; 46 | }; 47 | })(jQuery); 48 | -------------------------------------------------------------------------------- /lib/assets/js/schnitzelpress.js: -------------------------------------------------------------------------------- 1 | function loginViaEmail() { 2 | navigator.id.getVerifiedEmail(function(assertion) { 3 | if (assertion) { 4 | $('input[name=assertion]').val(assertion); 5 | $('form').submit(); 6 | } else { 7 | window.location = "/auth/failure"; 8 | } 9 | }); 10 | } 11 | 12 | $(document).ready(function() { 13 | $('form').submit(function(evt) { 14 | $('html').addClass('loading'); 15 | }); 16 | 17 | $('a#browser_id').click(function(evt) { 18 | evt.preventDefault(); 19 | loginViaEmail(); 20 | }); 21 | 22 | var showAdmin = $.cookie('show_admin'); 23 | if (showAdmin) { 24 | $('body').addClass('show_admin'); 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /lib/public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hmans/schnitzelpress/23cec2070d4c0784fb30923e9516d75394c420e4/lib/public/.gitkeep -------------------------------------------------------------------------------- /lib/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hmans/schnitzelpress/23cec2070d4c0784fb30923e9516d75394c420e4/lib/public/favicon.ico -------------------------------------------------------------------------------- /lib/public/font/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hmans/schnitzelpress/23cec2070d4c0784fb30923e9516d75394c420e4/lib/public/font/fontawesome-webfont.eot -------------------------------------------------------------------------------- /lib/public/font/fontawesome-webfont.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /lib/public/font/fontawesome-webfont.svgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hmans/schnitzelpress/23cec2070d4c0784fb30923e9516d75394c420e4/lib/public/font/fontawesome-webfont.svgz -------------------------------------------------------------------------------- /lib/public/font/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hmans/schnitzelpress/23cec2070d4c0784fb30923e9516d75394c420e4/lib/public/font/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /lib/public/font/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hmans/schnitzelpress/23cec2070d4c0784fb30923e9516d75394c420e4/lib/public/font/fontawesome-webfont.woff -------------------------------------------------------------------------------- /lib/public/img/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hmans/schnitzelpress/23cec2070d4c0784fb30923e9516d75394c420e4/lib/public/img/background.png -------------------------------------------------------------------------------- /lib/public/moo.txt: -------------------------------------------------------------------------------- 1 | moo from schnitzelpress 2 | -------------------------------------------------------------------------------- /lib/schnitzelpress.rb: -------------------------------------------------------------------------------- 1 | require 'schnitzelpress/version' 2 | 3 | require 'sinatra' 4 | require 'haml' 5 | require 'sass' 6 | require 'redcarpet' 7 | require 'schnitzelstyle' 8 | require 'rack/contrib' 9 | require 'rack/cache' 10 | require 'mongoid' 11 | require 'chronic' 12 | 13 | require 'active_support/inflector' 14 | require 'active_support/core_ext/class' 15 | require 'active_support/concern' 16 | 17 | require 'schnitzelpress/cache_control' 18 | require 'schnitzelpress/env' 19 | require 'schnitzelpress/static' 20 | require 'schnitzelpress/helpers' 21 | require 'schnitzelpress/markdown_renderer' 22 | require 'schnitzelpress/config' 23 | require 'schnitzelpress/post' 24 | require 'schnitzelpress/actions/assets' 25 | require 'schnitzelpress/actions/blog' 26 | require 'schnitzelpress/actions/auth' 27 | require 'schnitzelpress/actions/admin' 28 | require 'schnitzelpress/app' 29 | 30 | Sass::Engine::DEFAULT_OPTIONS[:load_paths].unshift(File.expand_path("../views", __FILE__)) 31 | Sass::Engine::DEFAULT_OPTIONS[:load_paths].unshift(File.expand_path("./views")) 32 | 33 | Mongoid.logger.level = 3 34 | 35 | module Schnitzelpress 36 | mattr_reader :mongo_uri 37 | 38 | class << self 39 | def mongo_uri=(uri) 40 | Mongoid::Config.from_hash("uri" => uri) 41 | Schnitzelpress::Post.create_indexes 42 | @@mongo_uri = uri 43 | end 44 | 45 | def init! 46 | # Mongoid.load!("./config/mongo.yml") 47 | if mongo_uri = ENV['MONGOLAB_URI'] || ENV['MONGOHQ_URL'] || ENV['MONGO_URL'] 48 | self.mongo_uri = mongo_uri 49 | else 50 | raise "Please set MONGO_URL, MONGOHQ_URL or MONGOLAB_URI to your MongoDB connection string." 51 | end 52 | Schnitzelpress::Post.create_indexes 53 | end 54 | 55 | def omnomnom! 56 | init! 57 | App.with_local_files 58 | end 59 | end 60 | end 61 | 62 | # teach HAML to use RedCarpet for markdown 63 | module Haml::Filters::Redcarpet 64 | include Haml::Filters::Base 65 | 66 | def render(text) 67 | Redcarpet::Markdown.new(Schnitzelpress::MarkdownRenderer, 68 | :autolink => true, :space_after_headers => true, :fenced_code_blocks => true). 69 | render(text) 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/schnitzelpress/actions/admin.rb: -------------------------------------------------------------------------------- 1 | module Schnitzelpress 2 | module Actions 3 | module Admin 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | before '/admin/?*' do 8 | admin_only! 9 | end 10 | 11 | get '/admin/?' do 12 | @posts = Post.published.posts.desc(:published_at) 13 | @pages = Post.published.pages 14 | @drafts = Post.drafts 15 | haml :'admin/admin' 16 | end 17 | 18 | get '/admin/config/?' do 19 | haml :'admin/config' 20 | end 21 | 22 | post '/admin/config' do 23 | config.attributes = params[:config] 24 | if config.save 25 | CacheControl.bust! 26 | redirect '/admin' 27 | else 28 | haml :'admin/config' 29 | end 30 | end 31 | 32 | get '/admin/new/?' do 33 | @post = Post.new 34 | haml :'admin/new' 35 | end 36 | 37 | post '/admin/new/?' do 38 | @post = Post.new(params[:post]) 39 | if @post.save 40 | redirect url_for(@post) 41 | else 42 | haml :'admin/new' 43 | end 44 | end 45 | 46 | get '/admin/edit/:id/?' do 47 | @post = Post.find(params[:id]) 48 | haml :'admin/edit' 49 | end 50 | 51 | put '/admin/edit/:id/?' do 52 | @post = Post.find(params[:id]) 53 | @post.attributes = params[:post] 54 | if @post.save 55 | redirect url_for(@post) 56 | else 57 | haml :'admin/edit' 58 | end 59 | end 60 | 61 | delete '/admin/edit/:id/?' do 62 | @post = Post.find(params[:id]) 63 | @post.destroy 64 | redirect '/admin' 65 | end 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/schnitzelpress/actions/assets.rb: -------------------------------------------------------------------------------- 1 | require 'packr' 2 | 3 | 4 | module Schnitzelpress 5 | class JavascriptPacker 6 | def self.pack_javascripts!(files) 7 | plain = files.map do |filename| 8 | File.read(File.expand_path("../lib/assets/js/#{filename}", settings.root)) 9 | end.join("\n") 10 | 11 | Packr.pack(plain) 12 | end 13 | end 14 | 15 | module Actions 16 | module Assets 17 | extend ActiveSupport::Concern 18 | 19 | ASSET_TIMESTAMP = Time.now.to_i 20 | JAVASCRIPT_ASSETS = ['jquery-1.7.1.js', 'jquery.cookie.js', 'schnitzelpress.js', 'jquery-ujs.js'] 21 | 22 | included do 23 | get '/assets/schnitzelpress.:timestamp.css' do 24 | cache_control :public, :max_age => 1.year.to_i 25 | scss :blog 26 | end 27 | 28 | get '/assets/schnitzelpress.:timestamp.js' do 29 | cache_control :public, :max_age => 1.year.to_i 30 | content_type 'text/javascript; charset=utf-8' 31 | JavascriptPacker.pack_javascripts!(JAVASCRIPT_ASSETS) 32 | end 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/schnitzelpress/actions/auth.rb: -------------------------------------------------------------------------------- 1 | require 'omniauth' 2 | require 'omniauth-browserid' 3 | 4 | module Schnitzelpress 5 | module Actions 6 | module Auth 7 | extend ActiveSupport::Concern 8 | 9 | included do 10 | use OmniAuth::Builder do 11 | provider :browser_id 12 | if Schnitzelpress.env.development? 13 | provider :developer , :fields => [:email], :uid_field => :email 14 | end 15 | end 16 | 17 | post '/auth/:provider/callback' do 18 | auth = request.env['omniauth.auth'] 19 | session[:auth] = {:provider => auth['provider'], :uid => auth['uid']} 20 | 21 | if admin_logged_in? 22 | response.set_cookie('show_admin', :value => true, :path => '/') 23 | redirect '/admin/' 24 | else 25 | redirect '/' 26 | end 27 | end 28 | 29 | get '/login' do 30 | haml :'login' 31 | end 32 | 33 | get '/logout' do 34 | session[:auth] = nil 35 | response.delete_cookie('show_admin') 36 | 37 | redirect '/login' 38 | end 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/schnitzelpress/actions/blog.rb: -------------------------------------------------------------------------------- 1 | module Schnitzelpress 2 | module Actions 3 | module Blog 4 | extend ActiveSupport::Concern 5 | 6 | included do 7 | get '/' do 8 | @show_description = true 9 | if @post = Post.published.pages.where(:slugs => 'home').first 10 | extra_posts = Post.latest.limit(5) 11 | @extra_posts = ['From the Blog:', extra_posts] if extra_posts.any? 12 | render_post 13 | else 14 | render_blog 15 | end 16 | end 17 | 18 | get '/blog/?' do 19 | @show_description = true 20 | render_blog 21 | end 22 | 23 | def render_blog 24 | total_count = Post.latest.count 25 | skipped_count = params[:page].to_i * 10 26 | @posts = Post.latest.skip(skipped_count).limit(10) 27 | 28 | displayed_count = @posts.count(true) 29 | @show_previous_posts_button = total_count > skipped_count + displayed_count 30 | 31 | render_posts 32 | end 33 | 34 | # /posts.atom is now deprecated. 35 | get '/posts.atom' do 36 | redirect '/blog.atom', 301 37 | end 38 | 39 | get '/blog.atom' do 40 | cache_control :public, :must_revalidate, :s_maxage => 2, :max_age => 3.minutes.to_i 41 | 42 | @posts = Post.latest.limit(10) 43 | content_type 'application/atom+xml; charset=utf-8' 44 | haml :atom, :format => :xhtml, :layout => false 45 | end 46 | 47 | get '/feed/?' do 48 | redirect config.blog_feed_url, 307 49 | end 50 | 51 | get %r{^/(\d{4})/(\d{1,2})/(\d{1,2})/?$} do 52 | year, month, day = params[:captures] 53 | @posts = Post.latest.for_day(year.to_i, month.to_i, day.to_i) 54 | render_posts 55 | end 56 | 57 | get %r{^/(\d{4})/(\d{1,2})/?$} do 58 | year, month = params[:captures] 59 | @posts = Post.latest.for_month(year.to_i, month.to_i) 60 | render_posts 61 | end 62 | 63 | get %r{^/(\d{4})/?$} do 64 | year = params[:captures].first 65 | @posts = Post.latest.for_year(year.to_i) 66 | render_posts 67 | end 68 | 69 | get '/:year/:month/:day/:slug/?' do |year, month, day, slug| 70 | @post = Post. 71 | for_day(year.to_i, month.to_i, day.to_i). 72 | where(:slugs => slug).first 73 | 74 | render_post 75 | end 76 | 77 | get '/*/?' do 78 | slug = params[:splat].first 79 | @post = Post.where(:slugs => slug).first 80 | render_post 81 | end 82 | 83 | def render_post(enforce_canonical_url = true) 84 | if @post 85 | # enforce canonical URL 86 | if enforce_canonical_url && request.path != url_for(@post) 87 | redirect url_for(@post) 88 | else 89 | fresh_when :last_modified => @post.updated_at, 90 | :etag => CacheControl.etag(@post.updated_at) 91 | 92 | @show_description = @post.home_page? 93 | 94 | cache_control :public, :must_revalidate, :s_maxage => 2, :max_age => 60 95 | haml :post 96 | end 97 | else 98 | halt 404 99 | end 100 | end 101 | 102 | def render_posts 103 | if freshest_post = @posts.where(:updated_at.ne => nil).desc(:updated_at).first 104 | fresh_when :last_modified => freshest_post.updated_at, 105 | :etag => CacheControl.etag(freshest_post.updated_at) 106 | end 107 | 108 | cache_control :public, :must_revalidate, :s_maxage => 2, :max_age => 60 109 | haml :index 110 | end 111 | end 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /lib/schnitzelpress/app.rb: -------------------------------------------------------------------------------- 1 | require "sinatra/content_for" 2 | 3 | module Schnitzelpress 4 | class App < Sinatra::Base 5 | STATIC_PATHS = ["/favicon.ico", "/img", "/js"] 6 | 7 | set :views, ['./views/', File.expand_path('../../views/', __FILE__)] 8 | set :public_folder, File.expand_path('../../public/', __FILE__) 9 | 10 | use Rack::Cache if Schnitzelpress.env.production? 11 | use Rack::ShowExceptions 12 | use Rack::StaticCache, 13 | :urls => STATIC_PATHS, 14 | :root => File.expand_path('../../public/', __FILE__) 15 | use Rack::MethodOverride 16 | use Rack::Session::Cookie 17 | 18 | helpers Sinatra::ContentFor 19 | helpers Schnitzelpress::Helpers 20 | include Rack::Utils 21 | include Schnitzelpress::Actions::Auth 22 | include Schnitzelpress::Actions::Assets 23 | include Schnitzelpress::Actions::Admin 24 | include Schnitzelpress::Actions::Blog 25 | 26 | configure do 27 | disable :protection 28 | set :logging, true 29 | end 30 | 31 | before do 32 | # Reload configuration before every request. I know this isn't ideal, 33 | # but right now it's the easiest way to get the configuration in synch 34 | # across multiple instances of the app. 35 | # 36 | Config.instance.reload 37 | end 38 | 39 | def fresh_when(options) 40 | last_modified options[:last_modified] 41 | etag options[:etag] 42 | end 43 | 44 | not_found do 45 | haml :"404" 46 | end 47 | 48 | def self.with_local_files 49 | Rack::Cascade.new([ 50 | Rack::StaticCache.new(self, :urls => STATIC_PATHS, :root => './public'), 51 | self 52 | ]) 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/schnitzelpress/cache_control.rb: -------------------------------------------------------------------------------- 1 | module Schnitzelpress 2 | module CacheControl 3 | class << self 4 | def timestamp 5 | Schnitzelpress::Config.get 'cache_timestamp' 6 | end 7 | 8 | def bust! 9 | Schnitzelpress::Config.set 'cache_timestamp', Time.now 10 | end 11 | 12 | def etag(*args) 13 | Digest::MD5.hexdigest("-#{timestamp.to_i}-#{args.join '-'}-") 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/schnitzelpress/cli.rb: -------------------------------------------------------------------------------- 1 | require "thor" 2 | 3 | module Schnitzelpress 4 | class Cli < Thor 5 | include Thor::Actions 6 | 7 | source_root(File.expand_path('../../templates', __FILE__)) 8 | 9 | desc "create NAME", "Creates a new Schnitzelpress blog." 10 | method_option :git, :aliases => "-g", :default => false, :type => :boolean, 11 | :desc => "Initialize a git repository in your blog's directory." 12 | 13 | def create(name) 14 | @name = name 15 | self.destination_root = name 16 | directory 'new_blog', '.' 17 | 18 | in_root do 19 | if options[:git] 20 | run "git init" 21 | run "git add ." 22 | run "git commit -m 'Created new Schnitzelpress blog'" 23 | end 24 | end 25 | end 26 | 27 | desc "update", "Update your blog's bundled Schnitzelpress version." 28 | def update 29 | run "bundle update schnitzelpress" 30 | end 31 | 32 | desc "console", "Run the Schnitzelpress console." 33 | def console 34 | require 'schnitzelpress' 35 | require 'pry' 36 | Schnitzelpress.init! 37 | ARGV.clear 38 | pry Schnitzelpress 39 | end 40 | 41 | desc "mongo_pull", "Pulls contents of remote MongoDB into your local MongoDB" 42 | def mongo_pull 43 | abort "Please set MONGO_URL." unless ENV['MONGO_URL'] 44 | system "heroku mongo:pull" 45 | end 46 | 47 | desc "mongo_push", "Pushes contents of your local MongoDB to remote MongoDB" 48 | def mongo_push 49 | abort "Please set MONGO_URL." unless ENV['MONGO_URL'] 50 | system "heroku mongo:push" 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/schnitzelpress/config.rb: -------------------------------------------------------------------------------- 1 | module Schnitzelpress 2 | class Config 3 | include Mongoid::Document 4 | include Mongoid::Timestamps 5 | identity :type => String 6 | store_in :config 7 | 8 | field :blog_title, :type => String, :default => "A New Schnitzelpress Blog" 9 | field :blog_description, :type => String, :default => "" 10 | field :blog_footer, :type => String, :default => "powered by [Schnitzelpress](http://schnitzelpress.org)" 11 | field :blog_feed_url, :type => String, :default => "/blog.atom" 12 | 13 | field :author_name, :type => String, :default => "Joe Schnitzel" 14 | 15 | field :disqus_id, :type => String 16 | field :google_analytics_id, :type => String 17 | field :gauges_id, :type => String 18 | field :gosquared_id, :type => String 19 | field :twitter_id, :type => String 20 | 21 | field :cache_timestamp, :type => DateTime 22 | 23 | validates :blog_title, :author_name, :presence => true 24 | 25 | class << self 26 | def instance 27 | @@instance ||= find_or_create_by(:id => 'schnitzelpress') 28 | end 29 | 30 | def forget_instance 31 | @@instance = nil 32 | end 33 | 34 | def get(k) 35 | instance.send(k) 36 | end 37 | 38 | def set(k, v) 39 | instance.update_attributes!(k => v) 40 | v 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/schnitzelpress/env.rb: -------------------------------------------------------------------------------- 1 | module Schnitzelpress 2 | def self.env 3 | (ENV['RACK_ENV'] || 'development').inquiry 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/schnitzelpress/helpers.rb: -------------------------------------------------------------------------------- 1 | module Schnitzelpress 2 | module Helpers 3 | def h(*args) 4 | escape_html(*args) 5 | end 6 | 7 | def find_template(views, name, engine, &block) 8 | Array(views).each { |v| super(v, name, engine, &block) } 9 | end 10 | 11 | def base_url 12 | "#{env['rack.url_scheme']}://#{env['HTTP_HOST']}/" 13 | end 14 | 15 | def partial(thing, locals = {}) 16 | name = case thing 17 | when String then thing 18 | else thing.class.to_s.demodulize.underscore 19 | end 20 | 21 | haml :"partials/_#{name}", :locals => { name.to_sym => thing }.merge(locals) 22 | end 23 | 24 | def config 25 | Schnitzelpress::Config.instance 26 | end 27 | 28 | def set_page_title(title) 29 | @page_title = title 30 | end 31 | 32 | def url_for(thing, options = {}) 33 | url = thing.respond_to?(:to_url) ? thing.to_url : thing.to_s 34 | url = "#{base_url.sub(/\/$/, '')}#{url}" if options[:absolute] 35 | url 36 | end 37 | 38 | def show_disqus? 39 | config.disqus_id.present? 40 | end 41 | 42 | def production? 43 | settings.environment.to_sym == :production 44 | end 45 | 46 | def user_logged_in? 47 | session[:auth].present? 48 | end 49 | 50 | def admin_logged_in? 51 | user_logged_in? && (session[:auth][:uid] == ENV['SCHNITZELPRESS_OWNER']) 52 | end 53 | 54 | def admin_only! 55 | redirect '/login' unless admin_logged_in? 56 | end 57 | 58 | def form_field(object, attribute, options = {}) 59 | options = { 60 | :label => attribute.to_s.humanize.titleize, 61 | :value => object.send(attribute), 62 | :errors => object.errors[attribute.to_sym], 63 | :class_name => object.class.to_s.demodulize.underscore 64 | }.merge(options) 65 | 66 | options[:name] ||= "#{options[:class_name]}[#{attribute}]" 67 | options[:id] ||= object.new_record? ? 68 | "new_#{options[:class_name]}_#{attribute}" : 69 | "#{options[:class_name]}_#{object.id}_#{attribute}" 70 | options[:class] ||= "#{options[:class_name]}_#{attribute}" 71 | 72 | options[:type] ||= case options[:value] 73 | when DateTime, Time, Date then :datetime 74 | when Boolean, FalseClass, TrueClass then :boolean 75 | else :text 76 | end 77 | 78 | partial 'form_field', :object => object, :attribute => attribute, :options => options 79 | end 80 | 81 | def icon(name) 82 | map = { 83 | 'glass' => 'f000', 84 | 'music' => 'f001', 85 | 'search' => 'f002', 86 | 'envelope' => 'f003', 87 | 'heart' => 'f004', 88 | 'star' => 'f005', 89 | 'star-empty' => 'f006', 90 | 'user' => 'f007', 91 | 'film' => 'f008', 92 | 'th-large' => 'f009', 93 | 'th' => 'f00a', 94 | 'th-list' => 'f00b', 95 | 'ok' => 'f00c', 96 | 'remove' => 'f00d', 97 | 'zoom-in' => 'f00e', 98 | 'zoom-out' => 'f010', 99 | 'off' => 'f011', 100 | 'signal' => 'f012', 101 | 'cog' => 'f013', 102 | 'trash' => 'f014', 103 | 'home' => 'f015', 104 | 'file' => 'f016', 105 | 'time' => 'f017', 106 | 'road' => 'f018', 107 | 'download-alt' => 'f019', 108 | 'download' => 'f01a', 109 | 'upload' => 'f01b', 110 | 'inbox' => 'f01c', 111 | 'play-circle' => 'f01d', 112 | 'repeat' => 'f01e', 113 | 'refresh' => 'f021', 114 | 'list-alt' => 'f022', 115 | 'lock' => 'f023', 116 | 'flag' => 'f024', 117 | 'headphones' => 'f025', 118 | 'volume-off' => 'f026', 119 | 'volume-down' => 'f027', 120 | 'volume-up' => 'f028', 121 | 'qrcode' => 'f029', 122 | 'barcode' => 'f02a', 123 | 'tag' => 'f02b', 124 | 'tags' => 'f02c', 125 | 'book' => 'f02d', 126 | 'bookmark' => 'f02e', 127 | 'print' => 'f02f', 128 | 'camera' => 'f030', 129 | 'font' => 'f031', 130 | 'bold' => 'f032', 131 | 'italic' => 'f033', 132 | 'text-height' => 'f034', 133 | 'text-width' => 'f035', 134 | 'align-left' => 'f036', 135 | 'align-center' => 'f037', 136 | 'align-right' => 'f038', 137 | 'align-justify' => 'f039', 138 | 'list' => 'f03a', 139 | 'indent-left' => 'f03b', 140 | 'indent-right' => 'f03c', 141 | 'facetime-video' => 'f03d', 142 | 'picture' => 'f03e', 143 | 'pencil' => 'f040', 144 | 'map-marker' => 'f041', 145 | 'adjust' => 'f042', 146 | 'tint' => 'f043', 147 | 'edit' => 'f044', 148 | 'share' => 'f045', 149 | 'check' => 'f046', 150 | 'move' => 'f047', 151 | 'step-backward' => 'f048', 152 | 'fast-backward' => 'f049', 153 | 'backward' => 'f04a', 154 | 'play' => 'f04b', 155 | 'pause' => 'f04c', 156 | 'stop' => 'f04d', 157 | 'forward' => 'f04e', 158 | 'fast-forward' => 'f050', 159 | 'step-forward' => 'f051', 160 | 'eject' => 'f052', 161 | 'chevron-left' => 'f053', 162 | 'chevron-right' => 'f054', 163 | 'plus-sign' => 'f055', 164 | 'minus-sign' => 'f056', 165 | 'remove-sign' => 'f057', 166 | 'ok-sign' => 'f058', 167 | 'question-sign' => 'f059', 168 | 'info-sign' => 'f05a', 169 | 'screenshot' => 'f05b', 170 | 'remove-circle' => 'f05c', 171 | 'ok-circle' => 'f05d', 172 | 'ban-circle' => 'f05e', 173 | 'arrow-left' => 'f060', 174 | 'arrow-right' => 'f061', 175 | 'arrow-up' => 'f062', 176 | 'arrow-down' => 'f063', 177 | 'share-alt' => 'f064', 178 | 'resize-full' => 'f065', 179 | 'resize-small' => 'f066', 180 | 'plus' => 'f067', 181 | 'minus' => 'f068', 182 | 'asterisk' => 'f069', 183 | 'exclamation-sign' => 'f06a', 184 | 'gift' => 'f06b', 185 | 'leaf' => 'f06c', 186 | 'fire' => 'f06d', 187 | 'eye-open' => 'f06e', 188 | 'eye-close' => 'f070', 189 | 'warning-sign' => 'f071', 190 | 'plane' => 'f072', 191 | 'calendar' => 'f073', 192 | 'random' => 'f074', 193 | 'comment' => 'f075', 194 | 'magnet' => 'f076', 195 | 'chevron-up' => 'f077', 196 | 'chevron-down' => 'f078', 197 | 'retweet' => 'f079', 198 | 'shopping-cart' => 'f07a', 199 | 'folder-close' => 'f07b', 200 | 'folder-open' => 'f07c', 201 | 'resize-vertical' => 'f07d', 202 | 'resize-horizontal' => 'f07e', 203 | 'bar-chart' => 'f080', 204 | 'twitter-sign' => 'f081', 205 | 'facebook-sign' => 'f082', 206 | 'camera-retro' => 'f083', 207 | 'key' => 'f084', 208 | 'cogs' => 'f085', 209 | 'comments' => 'f086', 210 | 'thumbs-up' => 'f087', 211 | 'thumbs-down' => 'f088', 212 | 'star-half' => 'f089', 213 | 'heart-empty' => 'f08a', 214 | 'signout' => 'f08b', 215 | 'linkedin-sign' => 'f08c', 216 | 'pushpin' => 'f08d', 217 | 'external-link' => 'f08e', 218 | 'signin' => 'f090', 219 | 'trophy' => 'f091', 220 | 'github-sign' => 'f092', 221 | 'upload-alt' => 'f093', 222 | 'lemon' => 'f094' 223 | } 224 | 225 | char = map[name.to_s] || 'f06a' 226 | 227 | "#{char};" 228 | end 229 | 230 | def link_to(title, target = "", options = {}) 231 | options[:href] = target.respond_to?(:to_url) ? target.to_url : target 232 | options[:data] ||= {} 233 | [:method, :confirm].each { |a| options[:data][a] = options.delete(a) } 234 | haml "%a#{options} #{title}" 235 | end 236 | 237 | def link_to_delete_post(title, post) 238 | link_to title, "/admin/edit/#{post.id}", :method => :delete, :confirm => "Are you sure? This can not be undone." 239 | end 240 | end 241 | end 242 | -------------------------------------------------------------------------------- /lib/schnitzelpress/markdown_renderer.rb: -------------------------------------------------------------------------------- 1 | module Schnitzelpress 2 | class MarkdownRenderer < Redcarpet::Render::HTML 3 | include Redcarpet::Render::SmartyPants 4 | 5 | def block_code(code, language) 6 | CodeRay.highlight(code, language) 7 | end 8 | 9 | def image(link, title, alt_text) 10 | oembed = OEmbed::Providers.get(link) 11 | %q( ) % [oembed.type, oembed.provider_name.parameterize, oembed.html] 12 | rescue OEmbed::NotFound 13 | %q(