Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Initial Commit

  • Loading branch information...
commit 273848ff347dfe0578e5a690b6ff60ad6bccc0c2 0 parents
@chrisparrish chrisparrish authored
Showing with 7,275 additions and 0 deletions.
  1. +21 −0 LICENSE
  2. +52 −0 README
  3. +120 −0 Rakefile
  4. +8 −0 app/controllers/admin/css_controller.rb
  5. +8 −0 app/controllers/admin/js_controller.rb
  6. +23 −0 app/controllers/admin/text_asset_controller.rb
  7. +49 −0 app/controllers/text_asset_site_controller.rb
  8. +2 −0  app/helpers/admin/css_helper.rb
  9. +2 −0  app/helpers/admin/js_helper.rb
  10. +2 −0  app/helpers/text_asset_site_helper.rb
  11. +11 −0 app/models/css_asset.rb
  12. +11 −0 app/models/js_asset.rb
  13. +14 −0 app/models/text_asset.rb
  14. +12 −0 app/models/text_asset_response_cache.rb
  15. +36 −0 app/views/admin/css/edit.html.erb
  16. +36 −0 app/views/admin/css/index.html.erb
  17. +17 −0 app/views/admin/css/remove.html.erb
  18. +37 −0 app/views/admin/js/edit.html.erb
  19. +36 −0 app/views/admin/js/index.html.erb
  20. +17 −0 app/views/admin/js/remove.html.erb
  21. +2 −0  config/environment.rb
  22. +34 −0 custom_settings.rb
  23. +16 −0 db/migrate/001_create_text_assets.rb
  24. +119 −0 lib/cssmin.rb
  25. +233 −0 lib/jsmin.rb
  26. +81 −0 lib/styles_n_scripts/config.rb
  27. +28 −0 lib/tasks/styles_n_scripts_extension_tasks.rake
  28. BIN  public/images/admin/javascript.png
  29. BIN  public/images/admin/new-javascript.png
  30. BIN  public/images/admin/new-stylesheet.png
  31. BIN  public/images/admin/stylesheet.png
  32. +307 −0 spec/controllers/admin/css_and_js_controllers_common_spec.rb
  33. +3 −0  spec/controllers/admin/css_controller_spec.rb
  34. +3 −0  spec/controllers/admin/js_controller_spec.rb
  35. +190 −0 spec/controllers/text_asset_site_controller_spec.rb
  36. +11 −0 spec/helpers/admin/css_helper_spec.rb
  37. +11 −0 spec/helpers/admin/js_helper_spec.rb
  38. +11 −0 spec/helpers/text_asset_site_helper_spec.rb
  39. +180 −0 spec/lib/config_spec.rb
  40. +95 −0 spec/models/css_asset_and_js_asset_common_spec.rb
  41. +34 −0 spec/models/css_asset_minify_spec.rb
  42. +25 −0 spec/models/js_asset_minify_spec.rb
  43. +98 −0 spec/models/text_asset_response_cache_spec.rb
  44. +4,184 −0 spec/samples/prototype.js
  45. +264 −0 spec/samples/prototype.minified.js
  46. +624 −0 spec/samples/radiant.css
  47. +1 −0  spec/samples/radiant.minified.css
  48. +12 −0 spec/samples/test.css
  49. +1 −0  spec/samples/test.minified.css
  50. +47 −0 spec/scenarios/javascripts_scenario.rb
  51. +47 −0 spec/scenarios/stylesheets_scenario.rb
  52. +6 −0 spec/spec.opts
  53. +37 −0 spec/spec_helper.rb
  54. +57 −0 styles_n_scripts_extension.rb
21 LICENSE
@@ -0,0 +1,21 @@
+== MIT License
+
+Copyright (c) 2008, Swank Innovations, LLC.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
52 README
@@ -0,0 +1,52 @@
+= Styles 'n Scripts Extension
+
+The files extension is a proof-of-concept suggested by John Long
+as a means of separating javascripts & stylesheets from other
+site content stored in pages.
+
+
+WHY CHANGE THINGS?
+==================
+As John sees it, the pages tab is really for the main content.
+(Think of it as the list of available destinations for your users)
+Sure, they need stylesheets and such but those are supporting files
+that they need to view your pages.
+
+There are a number of interesting benefits gained by this approach:
+
+ * These files really deserve designer-level permissions no user-
+ level. Well, they just got their own tabs.
+
+ * These files should be cached differently. Rather than the 5
+ minute page cache, we now have essentially infinite caching.
+
+ * This frees up pages to offer fields like <meta keywords...> that
+ make absolutely no sense for, say, a stylesheet page.
+
+ * This opens the door for minification and obfuscation of scripts
+ and stylesheets (I've incorporated JSMin and a stylesheet minifier
+ in the initial release. Future versions may include Dean Edwards Packer or
+ other more advanced compressors).
+
+ * This lays a conceptual foundation for other, non-text assets like images,
+ flash, and the like.
+
+
+
+
+TO-DO
+=====
+Create a way for users to upload stylesheets and javascripts (maybe even many in
+a zipped file) and unpack them into the db.
+
+Improve caching (I'd like to see a scenario where files are cached on save (or
+even just Rails page caching. Maintaining a server-ready cache that bypasses
+ruby wouldn't be all that hard for these items.).
+
+Create <r:stylesheet> (or maybe <r:text_asset>) tag to let users reference
+stylesheets from pages, snippets, and layouts.
+
+Create a tagging mechanism for combining stylesheets or scripts into a single
+file to reduce the number of requests and speed serving.
+
+Figure out what the core team needs to get this puppy baked in with Radiant!
120 Rakefile
@@ -0,0 +1,120 @@
+# I think this is the one that should be moved to the extension Rakefile template
+
+# In rails 1.2, plugins aren't available in the path until they're loaded.
+# Check to see if the rspec plugin is installed first and require
+# it if it is. If not, use the gem version.
+
+# Determine where the RSpec plugin is by loading the boot
+unless defined? RADIANT_ROOT
+ ENV["RAILS_ENV"] = "test"
+ case
+ when ENV["RADIANT_ENV_FILE"]
+ require File.dirname(ENV["RADIANT_ENV_FILE"]) + "/boot"
+ when File.dirname(__FILE__) =~ %r{vendor/radiant/vendor/extensions}
+ require "#{File.expand_path(File.dirname(__FILE__) + "/../../../../../")}/config/boot"
+ else
+ require "#{File.expand_path(File.dirname(__FILE__) + "/../../../")}/config/boot"
+ end
+end
+
+require 'rake'
+require 'rake/rdoctask'
+require 'rake/testtask'
+
+rspec_base = File.expand_path(RADIANT_ROOT + '/vendor/plugins/rspec/lib')
+$LOAD_PATH.unshift(rspec_base) if File.exist?(rspec_base)
+require 'spec/rake/spectask'
+# require 'spec/translator'
+
+# Cleanup the RADIANT_ROOT constant so specs will load the environment
+Object.send(:remove_const, :RADIANT_ROOT)
+
+extension_root = File.expand_path(File.dirname(__FILE__))
+
+task :default => :spec
+task :stats => "spec:statsetup"
+
+desc "Run all specs in spec directory"
+Spec::Rake::SpecTask.new(:spec) do |t|
+ t.spec_opts = ['--options', "\"#{extension_root}/spec/spec.opts\""]
+ t.spec_files = FileList['spec/**/*_spec.rb']
+end
+
+namespace :spec do
+ desc "Run all specs in spec directory with RCov"
+ Spec::Rake::SpecTask.new(:rcov) do |t|
+ t.spec_opts = ['--options', "\"#{extension_root}/spec/spec.opts\""]
+ t.spec_files = FileList['spec/**/*_spec.rb']
+ t.rcov = true
+ t.rcov_opts = ['--exclude', 'spec', '--rails']
+ end
+
+ desc "Print Specdoc for all specs"
+ Spec::Rake::SpecTask.new(:doc) do |t|
+ t.spec_opts = ["--format", "specdoc", "--dry-run"]
+ t.spec_files = FileList['spec/**/*_spec.rb']
+ end
+
+ [:models, :controllers, :views, :helpers].each do |sub|
+ desc "Run the specs under spec/#{sub}"
+ Spec::Rake::SpecTask.new(sub) do |t|
+ t.spec_opts = ['--options', "\"#{extension_root}/spec/spec.opts\""]
+ t.spec_files = FileList["spec/#{sub}/**/*_spec.rb"]
+ end
+ end
+
+ # Hopefully no one has written their extensions in pre-0.9 style
+ # desc "Translate specs from pre-0.9 to 0.9 style"
+ # task :translate do
+ # translator = ::Spec::Translator.new
+ # dir = RAILS_ROOT + '/spec'
+ # translator.translate(dir, dir)
+ # end
+
+ # Setup specs for stats
+ task :statsetup do
+ require 'code_statistics'
+ ::STATS_DIRECTORIES << %w(Model\ specs spec/models)
+ ::STATS_DIRECTORIES << %w(View\ specs spec/views)
+ ::STATS_DIRECTORIES << %w(Controller\ specs spec/controllers)
+ ::STATS_DIRECTORIES << %w(Helper\ specs spec/views)
+ ::CodeStatistics::TEST_TYPES << "Model specs"
+ ::CodeStatistics::TEST_TYPES << "View specs"
+ ::CodeStatistics::TEST_TYPES << "Controller specs"
+ ::CodeStatistics::TEST_TYPES << "Helper specs"
+ ::STATS_DIRECTORIES.delete_if {|a| a[0] =~ /test/}
+ end
+
+ namespace :db do
+ namespace :fixtures do
+ desc "Load fixtures (from spec/fixtures) into the current environment's database. Load specific fixtures using FIXTURES=x,y"
+ task :load => :environment do
+ require 'active_record/fixtures'
+ ActiveRecord::Base.establish_connection(RAILS_ENV.to_sym)
+ (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir.glob(File.join(RAILS_ROOT, 'spec', 'fixtures', '*.{yml,csv}'))).each do |fixture_file|
+ Fixtures.create_fixtures('spec/fixtures', File.basename(fixture_file, '.*'))
+ end
+ end
+ end
+ end
+end
+
+desc 'Generate documentation for the styles_n_scripts extension.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = 'StylesNScriptsExtension'
+ rdoc.options << '--line-numbers' << '--inline-source'
+ rdoc.rdoc_files.include('README')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
+
+# For extensions that are in transition
+desc 'Test the styles_n_scripts extension.'
+Rake::TestTask.new(:test) do |t|
+ t.libs << 'lib'
+ t.pattern = 'test/**/*_test.rb'
+ t.verbose = true
+end
+
+# Load any custom rakefiles for extension
+Dir[File.dirname(__FILE__) + '/tasks/*.rake'].sort.each { |f| require f }
8 app/controllers/admin/css_controller.rb
@@ -0,0 +1,8 @@
+class Admin::CssController < Admin::TextAssetController
+ model_class CssAsset
+
+ only_allow_access_to :index, :new, :edit, :remove,
+ :when => [:developer, :admin],
+ :denied_url => { :controller => 'page', :action => 'index' },
+ :denied_message => 'You must have developer privileges to perform this action.'
+end
8 app/controllers/admin/js_controller.rb
@@ -0,0 +1,8 @@
+class Admin::JsController < Admin::TextAssetController
+ model_class JsAsset
+
+ only_allow_access_to :index, :new, :edit, :remove,
+ :when => [:developer, :admin],
+ :denied_url => { :controller => 'page', :action => 'index' },
+ :denied_message => 'You must have developer privileges to perform this action.'
+end
23 app/controllers/admin/text_asset_controller.rb
@@ -0,0 +1,23 @@
+class Admin::TextAssetController < Admin::AbstractModelController
+
+ def initialize
+ super
+ @cache = TextAssetResponseCache.instance
+ end
+
+ def new
+ self.model = model_class.new
+ render :action => "edit" if handle_new_or_edit_post
+ end
+
+ def remove
+ self.model = model_class.find(params[:id])
+ if request.post?
+ model.destroy
+ announce_removed
+ clear_model_cache
+ redirect_to model_index_url # <-- Added this line to clear cache on remove
+ end
+ end
+
+end
49 app/controllers/text_asset_site_controller.rb
@@ -0,0 +1,49 @@
+class TextAssetSiteController < SiteController
+# This controller adds functionality to site controller to render text_assets
+# (similarly to how it renders pages). This interacts with my
+# TextAssetResponseCache model (which behaves very much like ResponseCache)
+# to cache file items in their own separate cache.
+
+ def text_asset_cache
+ @text_asset_cache ||= TextAssetResponseCache.instance
+ end
+
+
+ def show_text_asset
+ response.headers.delete('Cache-Control')
+
+ filename = params[:filename].join('/')
+ url = "#{params[:directory]}/#{filename}"
+
+ if (request.get? || request.head?) and live? and (text_asset_cache.response_cached?(url))
+ @text_asset_cache.update_response(url, response, request)
+ @performed_render = true
+ else
+ show_uncached_text_asset(filename, params[:asset_class], url)
+ end
+ end
+
+
+ def show_uncached_text_asset(filename, asset_class, url)
+ case asset_class
+ when 'css_asset'
+ text_asset = CssAsset.find_by_filename(filename)
+ mime_type = StylesNScripts::Config[:css_mime_type]
+ when 'js_asset'
+ text_asset = JsAsset.find_by_filename(filename)
+ mime_type = StylesNScripts::Config[:js_mime_type]
+ end
+ unless text_asset.nil?
+ response.headers['Content-Type'] = mime_type
+ response.body = text_asset.content
+
+ # for text_assets, we cache no matter what (there's no status setting for them)
+ @text_asset_cache.cache_response(url, response) if request.get?
+ @performed_render = true
+ else
+ params[:url] = url
+ show_page
+ end
+ end
+
+end
2  app/helpers/admin/css_helper.rb
@@ -0,0 +1,2 @@
+module Admin::CssHelper
+end
2  app/helpers/admin/js_helper.rb
@@ -0,0 +1,2 @@
+module Admin::JsHelper
+end
2  app/helpers/text_asset_site_helper.rb
@@ -0,0 +1,2 @@
+module TextAssetSiteHelper
+end
11 app/models/css_asset.rb
@@ -0,0 +1,11 @@
+class CssAsset < TextAsset
+
+ def content
+ if self.minify?
+ CSSMin.minify(self.raw_content)
+ else
+ super
+ end
+ end
+
+end
11 app/models/js_asset.rb
@@ -0,0 +1,11 @@
+class JsAsset < TextAsset
+
+ def content
+ if self.minify?
+ JSMin.minify(self.raw_content)
+ else
+ super
+ end
+ end
+
+end
14 app/models/text_asset.rb
@@ -0,0 +1,14 @@
+class TextAsset < ActiveRecord::Base
+ set_inheritance_column :class_name
+
+ validates_presence_of :filename, :message => 'required'
+ validates_length_of :filename, :maximum => 100, :message => '%d-character limit'
+ validates_uniqueness_of :filename, :scope => :class_name, :message => 'filename already in use'
+ # the following regexp uses \A and \Z rather than ^ and $ to enforce no "\n" characters
+ validates_format_of :filename, :with => %r{\A[-_.A-Za-z0-9]*\Z}, :message => 'invalid format'
+
+ def content
+ self.raw_content
+ end
+
+end
12 app/models/text_asset_response_cache.rb
@@ -0,0 +1,12 @@
+class TextAssetResponseCache < ResponseCache
+ def initialize(options={})
+ @@defaults[:directory] = "#{RAILS_ROOT}/#{StylesNScripts::Config[:response_cache_directory]}"
+ @@defaults[:expire_time] = 1.year
+ super(options)
+ end
+
+ def self.instance
+ # can't use @@instance as this class is inherited
+ @@tarc_instance ||= new
+ end
+end
36 app/views/admin/css/edit.html.erb
@@ -0,0 +1,36 @@
+<% if @css_asset.new_record? -%>
+<h1>New Stylesheet</h1>
+<% else -%>
+<h1>Edit Stylesheet</h1>
+<% end -%>
+
+<form method="post" action="">
+ <%= hidden_field "css_asset", "lock_version" %>
+ <div class="form-area">
+ <p class="title">
+ <label for="css_asset_filename">Filename</label>
+ <%= text_field "css_asset", "filename", :class => 'textbox', :maxlength => 100 %>
+ </p>
+
+ <p class="content">
+ <label for="css_asset_raw_content">Body</label>
+ <%= text_area "css_asset", "raw_content", :class => "textarea", :style => "width: 100%" %>
+ </p>
+
+ <div class="row">
+ <p>
+ <%= check_box("css_asset", "minify") %>
+ <label for="css_asset_minify">Minify Output?</label>
+ </p>
+ </div>
+
+ <span class="clear">&nbsp;</span>
+
+ <%= updated_stamp @css_asset %>
+ </div>
+ <p class="buttons">
+ <%= submit_tag(@css_asset.new_record? ? 'Create Stylesheet' : 'Save Changes', :class => 'button') %> <%= save_model_and_continue_editing_button(@css_asset) %> or <%= link_to "Cancel", css_asset_index_url %>
+ </p>
+</form>
+
+<%= focus 'css_asset_filename' %>
36 app/views/admin/css/index.html.erb
@@ -0,0 +1,36 @@
+<h1>Stylesheets</h1>
+
+<p>Cascading Stylesheets (CSS) are text assets used by your web pages, layouts, and snippets to stylize your content.</p>
+
+<table id="css_assets" class="index" cellpadding="0" cellspacing="0" border="0">
+ <thead>
+ <tr>
+ <th class="css_asset">Stylesheet</th>
+ <th class="modify">Modify</th>
+ </tr>
+ </thead>
+ <tbody>
+<% unless @css_assets.empty? -%>
+<% for css_asset in @css_assets -%>
+ <tr class="node level-1">
+ <td class="css_asset">
+ <%= image "stylesheet", :class => "icon", :alt => 'stylesheet-icon', :title => '' %>
+ <span><%= link_to css_asset.filename, css_asset_edit_url(:id => css_asset) %></span>
+ </td>
+ <td class="remove"><%= link_to image('remove', :alt => 'Remove Stylesheet'), css_asset_remove_url(:id => css_asset) %></td>
+ </tr>
+<% end -%>
+<% else -%>
+ <tr>
+ <td colspan="2" class="note">No Stylesheets</td>
+ </tr>
+<% end -%>
+ </tbody>
+</table>
+<script type="text/javascript">
+// <![CDATA[
+ new RuledTable('css_assets');
+// ]]>
+</script>
+
+<p><%= link_to image('new-stylesheet', :alt => 'New Css'), css_asset_new_url %></p>
17 app/views/admin/css/remove.html.erb
@@ -0,0 +1,17 @@
+<h1>Remove Stylesheet</h1>
+<p>Are you sure you want to <strong class="warning">permanently remove</strong> the following css?</p>
+
+<table id="css_assets" class="index" cellpadding="0" cellspacing="0" border="0">
+ <tbody>
+ <tr class="node level-1" onmouseover="Element.addClassName(this, 'highlight');" onmouseout="Element.removeClassName(this, 'highlight');">
+ <td class="css_asset">
+ <%= image "stylesheet", :class => "icon", :alt => 'stylesheet-icon', :title => '' %>
+ <%= @css_asset.filename %>
+ </td>
+ </tr>
+ </tbody>
+</table>
+
+<form method="post" action="">
+ <p class="buttons"><%= submit_tag "Delete Stylesheet", :class => 'button' %> or <%= link_to 'Cancel', css_asset_index_url %></p>
+</form>
37 app/views/admin/js/edit.html.erb
@@ -0,0 +1,37 @@
+<% if @js_asset.new_record? -%>
+<h1>New Javascript</h1>
+<% else -%>
+<h1>Edit Javascript</h1>
+<% end -%>
+
+<form method="post" action="">
+ <%= hidden_field "js_asset", "lock_version" %>
+ <div class="form-area">
+ <p class="title">
+ <label for="js_asset_filename">Filename</label>
+ <%= text_field "js_asset", "filename", :class => 'textbox', :maxlength => 100 %>
+ </p>
+
+ <p class="content">
+ <label for="js_asset_raw_content">Body</label>
+ <%= text_area "js_asset", "raw_content", :class => "textarea", :style => "width: 100%" %>
+ </p>
+
+ <div class="row">
+ <p>
+ <%= check_box("js_asset", "minify") %>
+ <label for="js_asset_minify">Minify Output?</label>
+ </p>
+ </div>
+
+ <span class="clear">&nbsp;</span>
+
+ <%= updated_stamp @js_asset %>
+ </div>
+
+ <p class="buttons">
+ <%= submit_tag(@js_asset.new_record? ? 'Create Javascript' : 'Save Changes', :class => 'button') %> <%= save_model_and_continue_editing_button(@js_asset) %> or <%= link_to "Cancel", js_asset_index_url %>
+ </p>
+</form>
+
+<%= focus 'js_asset_filename' %>
36 app/views/admin/js/index.html.erb
@@ -0,0 +1,36 @@
+<h1>Javascripts</h1>
+
+<p>Javascripts are text assets used by your web pages, layouts, and snippets to add dynamic behaviors.</p>
+
+<table id="js_assets" class="index" cellpadding="0" cellspacing="0" border="0">
+ <thead>
+ <tr>
+ <th class="js_asset">Javascript</th>
+ <th class="modify">Modify</th>
+ </tr>
+ </thead>
+ <tbody>
+<% unless @js_assets.empty? -%>
+<% for js_asset in @js_assets -%>
+ <tr class="node level-1">
+ <td class="js_asset">
+ <%= image "javascript", :class => "icon", :alt => 'javascript-icon', :title => '' %>
+ <span><%= link_to js_asset.filename, js_asset_edit_url(:id => js_asset) %></span>
+ </td>
+ <td class="remove"><%= link_to image('remove', :alt => 'Remove Javascript'), js_asset_remove_url(:id => js_asset) %></td>
+ </tr>
+<% end -%>
+<% else -%>
+ <tr>
+ <td colspan="2" class="note">No Javascripts</td>
+ </tr>
+<% end -%>
+ </tbody>
+</table>
+<script type="text/javascript">
+// <![CDATA[
+ new RuledTable('js_assets');
+// ]]>
+</script>
+
+<p><%= link_to image('new-javascript', :alt => 'New Css'), js_asset_new_url %></p>
17 app/views/admin/js/remove.html.erb
@@ -0,0 +1,17 @@
+<h1>Remove Javascript</h1>
+<p>Are you sure you want to <strong class="warning">permanently remove</strong> the following js?</p>
+
+<table id="js_assets" class="index" cellpadding="0" cellspacing="0" border="0">
+ <tbody>
+ <tr class="node level-1" onmouseover="Element.addClassName(this, 'highlight');" onmouseout="Element.removeClassName(this, 'highlight');">
+ <td class="js_asset">
+ <%= image "javascript", :class => "icon", :alt => 'javascript-icon', :title => '' %>
+ <%= @js_asset.filename %>
+ </td>
+ </tr>
+ </tbody>
+</table>
+
+<form method="post" action="">
+ <p class="buttons"><%= submit_tag "Delete Javascript", :class => 'button' %> or <%= link_to 'Cancel', js_asset_index_url %></p>
+</form>
2  config/environment.rb
@@ -0,0 +1,2 @@
+# This file does nothing but help autotest know that this extension
+# is modeled after a rails app.
34 custom_settings.rb
@@ -0,0 +1,34 @@
+# Initializes settings for the extension. By uncommenting lines below, you may
+# customize these settings.
+#
+# Ideally, along with some updating to Radiant, managing these settings would
+# move to their own location in the Radiant Admin UI (probably with admin-only
+# permissions).
+
+module CustomSettings
+ StylesNScripts::Config.restore_defaults
+
+ # Uncomment the items below to customize the locations from which stylesheets
+ # and javascripts are served. The default values are:
+ # * /css
+ # * /js
+ #
+ # These default values are not the Rails-standard 'javascript' and
+ # 'stylesheets' directories because Radiant is already using those.
+ #
+ # Currently, you may use complex paths like:
+ # 'assets/text/css'
+ # but be warned, we may or may not support this in the future.
+
+# StylesNScripts::Config[:css_directory] = 'my_awesome_styles'
+# StylesNScripts::Config[:js_directory] = 'my_nifty_scripts')
+
+
+ # Uncomment the items below to customize the MIME types for stylesheets and
+ # and javascripts are served. The default values are:
+ # * text/css
+ # * text/javascript
+
+# StylesNScripts::Config[:css_mime_type] = "It's a Stylesheet File!"
+# StylesNScripts::Config[:js_mime_type] = "It's a Javascript File!"
+end
16 db/migrate/001_create_text_assets.rb
@@ -0,0 +1,16 @@
+class CreateTextAssets < ActiveRecord::Migration
+ def self.up
+ create_table :text_assets do |t|
+ t.string :class_name, :limit => 25
+ t.string :filename, :limit => 100
+ t.text :raw_content
+ t.integer :created_by, :updated_by, :lock_version
+ t.boolean :minify, :default => false
+ t.timestamps
+ end
+ end
+
+ def self.down
+ drop_table :text_assets
+ end
+end
119 lib/cssmin.rb
@@ -0,0 +1,119 @@
+#--
+# Copyright (c) 2008 Ryan Grove <ryan@wonko.com>
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+# * Neither the name of this project nor the names of its contributors may be
+# used to endorse or promote products derived from this software without
+# specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#++
+
+# = CSSMin
+#
+# Minifies CSS using a fast, safe routine adapted from Julien Lecomte's YUI
+# Compressor, which was in turn adapted from Isaac Schlueter's cssmin PHP
+# script.
+#
+# Author:: Ryan Grove (mailto:ryan@wonko.com)
+# Version:: 1.0.0 (2008-03-22)
+# Copyright:: Copyright (c) 2008 Ryan Grove. All rights reserved.
+# License:: New BSD License (http://opensource.org/licenses/bsd-license.php)
+# Website:: http://github.com/rgrove/cssmin/
+#
+# == Example
+#
+# require 'rubygems'
+# require 'cssmin'
+#
+# File.open('example.css', 'r') {|file| puts CSSMin.minify(file) }
+#
+module CSSMin
+
+ # Reads CSS from +input+ (which can be a String or an IO object) and
+ # returns a String containing minified CSS.
+ def self.minify(input)
+ css = input.is_a?(IO) ? input.read : input.to_s
+
+ # Remove comments.
+ css.gsub!(/\/\*[\s\S]*?\*\//, '')
+
+ # Compress all runs of whitespace to a single space to make things easier
+ # to work with.
+ css.gsub!(/\s+/, ' ')
+
+ # Replace box model hacks with placeholders.
+ css.gsub!(/"\\"\}\\""/, '___BMH___')
+
+ # Remove unnecessary spaces, but be careful not to turn "p :link {...}"
+ # into "p:link{...}".
+ css.gsub!(/(?:^|\})[^\{:]+\s+:+[^\{]*\{/) do |match|
+ match.gsub(':', '___PSEUDOCLASSCOLON___')
+ end
+ css.gsub!(/\s+([!\{\};:>+\(\)\],])/, '\1')
+ css.gsub!('___PSEUDOCLASSCOLON___', ':')
+ css.gsub!(/([!\{\}:;>+\(\[,])\s+/, '\1')
+
+ # Add missing semicolons.
+ css.gsub!(/([^;\}])\}/, '\1;}')
+
+ # Replace 0(%, em, ex, px, in, cm, mm, pt, pc) with just 0.
+ css.gsub!(/([\s:])([+-]?0)(?:%|em|ex|px|in|cm|mm|pt|pc)/i, '\1\2')
+
+ # Replace 0 0 0 0; with 0.
+ css.gsub!(/:(?:0 )+0;/, ':0;')
+
+ # Replace background-position:0; with background-position:0 0;
+ css.gsub!('background-position:0;', 'background-position:0 0;')
+
+ # Replace 0.6 with .6, but only when preceded by : or a space.
+ css.gsub!(/(:|\s)0+\.(\d+)/, '\1.\2')
+
+ # Convert rgb color values to hex values.
+ css.gsub!(/rgb\s*\(\s*([0-9,\s]+)\s*\)/) do |match|
+ '#' << $1.scan(/\d+/).map{|n| n.to_i.to_s(16).rjust(2, '0') }.join
+ end
+
+ # Compress color hex values, making sure not to touch values used in IE
+ # filters, since they would break.
+ css.gsub!(/([^"'=\s])\s*#([0-9a-f])\2([0-9a-f])\3([0-9a-f])\4/i, '\1#\2\3\4')
+
+ # Remove empty rules.
+ css.gsub!(/[^\}]+\{;\}\n/, '')
+
+ # Re-insert box model hacks.
+ css.gsub!('___BMH___', '"\"}\""')
+
+# # Replace 6.0 with 6, but only when preceded by : or a space.
+ css.gsub!(/(:|\s)(\d+)\.0+(?=[^\d])/, '\1\2')
+
+# # Replace : 2em 2em 2em 2em; with :2em;
+ css.gsub!(/(?!:)((?:\d+|\d+\.[1-9]+|\.[1-9]+)(?:%|em|ex|px|in|cm|mm|pt|pc))\s+\1\s+\1\s+\1(?=;|\})/, '\1')
+
+# # Replace : 2em 3px 2em 3px; with :2em 3px;
+ css.gsub!(/(?!:)((?:\d+|\d+\.[1-9]+|\.[1-9]+)+(?:%|em|ex|px|in|cm|mm|pt|pc)\s+(?:\d+|\d+\.[1-9]+|\.[1-9]+)+(?:%|em|ex|px|in|cm|mm|pt|pc))\s+\1(?=;|\})/, '\1')
+
+# # Remove any semicolons before braces ';}' => '}'
+ css.gsub!(/;(\})/, '\1')
+
+ css.strip
+ end
+
+end
233 lib/jsmin.rb
@@ -0,0 +1,233 @@
+#--
+# jsmin.rb - Ruby implementation of Douglas Crockford's JSMin.
+#
+# This is a port of jsmin.c, and is distributed under the same terms, which are
+# as follows:
+#
+# Copyright (c) 2002 Douglas Crockford (www.crockford.com)
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# The Software shall be used for Good, not Evil.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+#++
+
+require 'strscan'
+
+# = JSMin
+#
+# Ruby implementation of Douglas Crockford's JavaScript minifier, JSMin.
+#
+# Author:: Ryan Grove (mailto:ryan@wonko.com)
+# Version:: 1.0.0 (2008-03-22)
+# Copyright:: Copyright (c) 2008 Ryan Grove. All rights reserved.
+# Website:: http://github.com/rgrove/jsmin/
+#
+# == Example
+#
+# require 'rubygems'
+# require 'jsmin'
+#
+# File.open('example.js', 'r') {|file| puts JSMin.minify(file) }
+#
+module JSMin
+ ORD_LF = "\n"[0].freeze
+ ORD_SPACE = ' '[0].freeze
+
+ class << self
+
+ # Reads JavaScript from +input+ (which can be a String or an IO object) and
+ # returns a String containing minified JS.
+ def minify(input)
+ @js = StringScanner.new(input.is_a?(IO) ? input.read : input.to_s)
+
+ @a = "\n"
+ @b = nil
+ @lookahead = nil
+ @output = ''
+
+ action_get
+
+ while !@a.nil? do
+ case @a
+ when ' '
+ if alphanum?(@b)
+ action_output
+ else
+ action_copy
+ end
+
+ when "\n"
+ if @b == ' '
+ action_get
+ elsif @b =~ /[{\[\(+-]/
+ action_output
+ else
+ if alphanum?(@b)
+ action_output
+ else
+ action_copy
+ end
+ end
+
+ else
+ if @b == ' '
+ if alphanum?(@a)
+ action_output
+ else
+ action_get
+ end
+ elsif @b == "\n"
+ if @a =~ /[}\]\)\\"+-]/
+ action_output
+ else
+ if alphanum?(@a)
+ action_output
+ else
+ action_get
+ end
+ end
+ else
+ action_output
+ end
+ end
+ end
+
+ @output
+ end
+
+ private
+
+ # Corresponds to action(1) in jsmin.c.
+ def action_output
+ @output << @a
+ action_copy
+ end
+
+ # Corresponds to action(2) in jsmin.c.
+ def action_copy
+ @a = @b
+
+ if @a == '\'' || @a == '"'
+ loop do
+ @output << @a
+ @a = get
+
+ break if @a == @b
+
+ if @a[0] <= ORD_LF
+ raise "JSMin parse error: unterminated string literal: #{@a}"
+ end
+
+ if @a == '\\'
+ @output << @a
+ @a = get
+
+ if @a[0] <= ORD_LF
+ raise "JSMin parse error: unterminated string literal: #{@a}"
+ end
+ end
+ end
+ end
+
+ action_get
+ end
+
+ # Corresponds to action(3) in jsmin.c.
+ def action_get
+ @b = nextchar
+
+ if @b == '/' && (@a == "\n" || @a =~ /[\(,=:\[!&|?{};]/)
+ @output << @a
+ @output << @b
+
+ loop do
+ @a = get
+
+ if @a == '/'
+ break
+ elsif @a == '\\'
+ @output << @a
+ @a = get
+ elsif @a[0] <= ORD_LF
+ raise "JSMin parse error: unterminated regular expression " +
+ "literal: #{@a}"
+ end
+
+ @output << @a
+ end
+
+ @b = nextchar
+ end
+ end
+
+ # Returns true if +c+ is a letter, digit, underscore, dollar sign,
+ # backslash, or non-ASCII character.
+ def alphanum?(c)
+ c.is_a?(String) && !c.empty? && (c[0] > 126 || c =~ /[0-9a-z_$\\]/i)
+ end
+
+ # Returns the next character from the input. If the character is a control
+ # character, it will be translated to a space or linefeed.
+ def get
+ c = @lookahead.nil? ? @js.getch : @lookahead
+ @lookahead = nil
+
+ return c if c.nil? || c == "\n" || c[0] >= ORD_SPACE
+ return "\n" if c == "\r"
+ return ' '
+ end
+
+ # Gets the next character, excluding comments.
+ def nextchar
+ c = get
+ return c unless c == '/'
+
+ case peek
+ when '/'
+ loop do
+ c = get
+ return c if c[0] <= ORD_LF
+ end
+
+ when '*'
+ get
+ loop do
+ case get
+ when '*'
+ if peek == '/'
+ get
+ return ' '
+ end
+
+ when nil
+ raise 'JSMin parse error: unterminated comment'
+ end
+ end
+
+ else
+ return c
+ end
+ end
+
+ # Gets the next character without getting it.
+ def peek
+ @lookahead = get
+ end
+ end
+end
81 lib/styles_n_scripts/config.rb
@@ -0,0 +1,81 @@
+module StylesNScripts
+ class Config
+
+ # stores the default values for the settings
+ # note: key names *must* be declared as strings -- *not* as symbols
+ @defaults = { 'css_directory' => 'css',
+ 'js_directory' => 'js',
+ 'css_mime_type' => 'text/css',
+ 'js_mime_type' => 'text/javascript',
+ 'response_cache_directory' => 'text_asset_cache'
+ }
+
+ @@live_config = {}
+
+ class << self
+
+ # Getter for the config values. Includes a set of conditionals to
+ # incrementally test whether the live_config and then the Radiant::Config
+ # values are set and, if not, calls in the default values. I would have
+ # preferred to use something more like:
+ # @@live_config[key] ||= Radiant::Config[key] ||= @defaults[key])
+ # but Radiant::Config recasts nil values into empty strings.
+ def [](key)
+ key = key.to_s
+ validate_key(key)
+ if @@live_config[key].blank?
+ @@live_config[key] = Radiant::Config[key]
+ if @@live_config[key].blank?
+ @@live_config[key] = Radiant::Config[key] = @defaults[key]
+ end
+ end
+ @@live_config[key]
+ end
+
+ # Setter for Config key/value pairs. Keys must be limited to valid
+ # settings for the extension.
+ def []=(key, value)
+ # key must be in the form of a string for Radiant:Config
+ key = key.to_s
+
+ case key
+ when 'response_cache_directory'
+ validate_cache_directory(value, key)
+ when /_directory$/
+ validate_directory(value, key)
+ when /_mime_type$/
+ validate_mime_type(value, key)
+ end
+
+ @@live_config[key] = Radiant::Config[key] = value
+ end
+
+ def restore_defaults
+ @defaults.each do |key, value|
+ Radiant::Config[key] = value
+ end
+ @@live_config = {}
+ end
+
+ private
+
+ # determines whether 'key' matches one of the keys in @defaults (
+ def validate_key(key)
+ raise("invalid setting name: #{key.inspect}") unless @defaults.has_key?(key)
+ end
+
+ def validate_cache_directory(directory, directory_label)
+ raise("invalid #{directory_label} value: #{directory.inspect}") unless directory =~ %r{\A[-_A-Za-z0-9]*\Z}
+ end
+
+ def validate_directory(directory, directory_label)
+ raise("invalid #{directory_label} value: #{directory.inspect}") unless directory =~ %r{\A[-_A-Za-z0-9]+(/[-_A-Za-z0-9]+)*\Z}
+ end
+
+ def validate_mime_type(mime_type, mime_type_label)
+ raise("invalid #{mime_type_label} value: #{mime_type.inspect}") unless mime_type =~ %r{\A[-.A-Za-z0-9]+(/[-.A-Za-z0-9]+)*\Z}
+ end
+
+ end
+ end
+end
28 lib/tasks/styles_n_scripts_extension_tasks.rake
@@ -0,0 +1,28 @@
+namespace :radiant do
+ namespace :extensions do
+ namespace :styles_n_scripts do
+
+ desc "Runs the migration of the Styles N Scripts extension"
+ task :migrate => :environment do
+ require 'radiant/extension_migrator'
+ if ENV["VERSION"]
+ StylesNScriptsExtension.migrator.migrate(ENV["VERSION"].to_i)
+ else
+ StylesNScriptsExtension.migrator.migrate
+ end
+ end
+
+ desc "Copies public assets of the Styles N Scripts to the instance public/ directory."
+ task :update => :environment do
+ is_svn_or_dir = proc {|path| path =~ /\.svn/ || File.directory?(path) }
+ Dir[StylesNScriptsExtension.root + "/public/**/*"].reject(&is_svn_or_dir).each do |file|
+ path = file.sub(StylesNScriptsExtension.root, '')
+ directory = File.dirname(path)
+ puts "Copying #{path}..."
+ mkdir_p RAILS_ROOT + directory
+ cp file, RAILS_ROOT + path
+ end
+ end
+ end
+ end
+end
BIN  public/images/admin/javascript.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  public/images/admin/new-javascript.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  public/images/admin/new-stylesheet.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  public/images/admin/stylesheet.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
307 spec/controllers/admin/css_and_js_controllers_common_spec.rb
@@ -0,0 +1,307 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+
+[ { :controller => Admin::CssController,
+ :controller_path => 'admin/css',
+ :model => CssAsset,
+ :name => 'css_asset',
+ :symbol => :css_asset,
+ :main_scenario => :main_css },
+
+ { :controller => Admin::JsController,
+ :controller_path => 'admin/js',
+ :model => JsAsset,
+ :name => 'js_asset',
+ :symbol => :js_asset,
+ :main_scenario => :main_js }
+
+].each do |current_asset|
+
+ describe current_asset[:controller] do
+ # ok, this is weird... I would have just loaded all three scenarios but,
+ # for some *strange* reason, which ever loads 2nd -- javascripts or
+ # stylesheets -- trumps the previous. Everything works fine *except* for
+ # calls to text_assets(:symbolic_name). Don't ask me why but it assumes
+ # that this method should always return a CssAsset or JsAsset (which-
+ # ever is loaded last).
+ scenario :users, :stylesheets if current_asset[:name] == 'css_asset'
+ scenario :users, :javascripts if current_asset[:name] == 'js_asset'
+
+ before :each do
+ login_as :developer
+ end
+
+
+ it "should be an AbstractModelController" do
+ controller.should be_kind_of(Admin::AbstractModelController)
+ end
+
+ it "should handle current_asset[:model]s" do
+ controller.class.model_class.should == current_asset[:model]
+ end
+
+ [:index, :new, :edit, :remove].each do |action|
+ it "should require login to access the #{action} action" do
+ logout
+ lambda { get action }.should require_login
+ end
+
+ it "should allow access to developers" do
+ lambda { get action, :id => text_asset_id(current_asset[:main_scenario]) }.should restrict_access(:allow => [users(:developer)])
+ end
+
+ it "should allow access to admins" do
+ lambda { get action, :id => text_asset_id(current_asset[:main_scenario]) }.should restrict_access(:allow => [users(:admin)])
+ end
+
+ it "should deny non-developers and non-admins" do
+ lambda { get action }.should restrict_access(:deny => [users(:non_admin), users(:existing)])
+ end
+ end
+
+ describe "index action" do
+ before :each do
+ get :index
+ end
+
+ it "should be successful" do
+ response.should be_success
+ end
+
+ it "should render the index template" do
+ response.should render_template("#{current_asset[:controller_path]}/index")
+ end
+
+ it "should load an array of models" do
+ plural_symbol = current_asset[:symbol].to_s.pluralize.intern
+ assigns[plural_symbol].should be_kind_of(Array)
+ assigns[plural_symbol].all? { |i| i.kind_of?(current_asset[:model]) }.should be_true
+ end
+ end
+
+ describe "new action" do
+ describe "via GET" do
+ before :each do
+ get :new
+ end
+
+ it "should be successful" do
+ response.should be_success
+ end
+
+ it "should render the edit template" do
+ response.should render_template("#{current_asset[:controller_path]}/edit")
+ end
+
+ it "should load a new model" do
+ assigns[current_asset[:symbol]].should_not be_nil
+ assigns[current_asset[:symbol]].should be_kind_of(current_asset[:model])
+ assigns[current_asset[:symbol]].should be_new_record
+ end
+ end
+
+ describe "via POST" do
+ describe "when the model validates" do
+ before :each do
+ post :new,
+ current_asset[:symbol] => send("#{current_asset[:name]}_params")
+ @text_asset = current_asset[:model].find_by_filename('Test')
+ end
+
+ it "should redirect to the index" do
+ response.should be_redirect
+ response.should redirect_to(send("#{current_asset[:name]}_index_url"))
+ end
+
+ it "should create the model" do
+ assigns[current_asset[:symbol]].should_not be_new_record
+ end
+
+ it "should add a flash notice" do
+ flash[:notice].should_not be_nil
+ flash[:notice].should =~ /saved/
+ end
+ end
+
+ describe "when the model fails validation" do
+ before :each do
+ post :new,
+ current_asset[:symbol] => send("#{current_asset[:name]}_params",
+ :filename => nil)
+ @text_asset = current_asset[:model].find_by_filename('Test')
+ end
+
+ it "should render the edit template" do
+ response.should render_template("#{current_asset[:controller_path]}/edit")
+ end
+
+ it "should not create the model" do
+ assigns[current_asset[:symbol]].should be_new_record
+ end
+
+ it "should add a flash error" do
+ flash[:error].should_not be_nil
+ flash[:error].should =~ /error/
+ end
+ end
+
+ describe "when 'Save and Continue Editing' was clicked" do
+ before :each do
+ post :new,
+ current_asset[:symbol] => send("#{current_asset[:name]}_params",
+ :filename => 'Test'),
+ :continue => 'Save and Continue Editing'
+ @text_asset = current_asset[:model].find_by_filename('Test')
+ end
+
+ it "should redirect to the edit action" do
+ response.should be_redirect
+ response.should redirect_to(send("#{current_asset[:name]}_edit_url", :id => @text_asset))
+ end
+ end
+
+ end
+ end
+
+
+ describe "edit action" do
+ describe "via GET" do
+ before :each do
+ get :edit, :id => text_asset_id(current_asset[:main_scenario])
+ end
+
+ it "should be successful" do
+ response.should be_success
+ end
+
+ it "should render the edit template" do
+ response.should render_template("#{current_asset[:controller_path]}/edit")
+ end
+
+ it "should load the existing model" do
+ assigns[current_asset[:symbol]].should_not be_nil
+ assigns[current_asset[:symbol]].should be_kind_of(current_asset[:model])
+ assigns[current_asset[:symbol]].should == text_assets(current_asset[:main_scenario])
+ end
+ end
+
+ describe "via POST" do
+ describe "when the model validates" do
+ before :each do
+ post :edit,
+ :id => text_asset_id(current_asset[:main_scenario]),
+ current_asset[:symbol] => send("#{current_asset[:name]}_params")
+ end
+
+ it "should redirect to the index" do
+ response.should be_redirect
+ response.should redirect_to(send("#{current_asset[:name]}_index_url"))
+ end
+
+ it "should save the model" do
+ assigns[current_asset[:symbol]].should be_valid
+ end
+
+ it "should add a flash notice" do
+ flash[:notice].should_not be_nil
+ flash[:notice].should =~ /saved/
+ end
+
+ # it "should clear the model cache" do
+ # @cache.should be_cleared
+ # end
+ end
+
+ describe "when the model fails validation" do
+ before :each do
+ post :edit,
+ :id => text_asset_id(current_asset[:main_scenario]),
+ current_asset[:symbol] => send("#{current_asset[:name]}_params",
+ :filename => nil)
+ end
+
+ it "should render the edit template" do
+ response.should render_template("#{current_asset[:controller_path]}/edit")
+ end
+
+ it "should not save the model" do
+ assigns[current_asset[:symbol]].should_not be_valid
+ end
+
+ it "should add a flash error" do
+ flash[:error].should_not be_nil
+ flash[:error].should =~ /error/
+ end
+
+ # it "should not clear the model cache" do
+ # @cache.should_not be_cleared
+ # end
+ end
+
+ describe "when 'Save and Continue Editing' was clicked" do
+ before :each do
+ post :edit,
+ :id => text_asset_id(current_asset[:main_scenario]),
+ current_asset[:symbol] => send("#{current_asset[:name]}_params"),
+ :continue => 'Save and Continue Editing'
+ end
+
+ it "should redirect to the edit action" do
+ response.should be_redirect
+ response.should redirect_to(send("#{current_asset[:name]}_edit_url",
+ :id => text_asset_id(current_asset[:main_scenario])))
+ end
+ end
+ end
+
+ end
+
+
+ describe "remove action" do
+ describe "via GET" do
+ before :each do
+ get :remove, :id => text_asset_id(current_asset[:main_scenario])
+ end
+
+ it "should be successful" do
+ response.should be_success
+ end
+
+ it "should render the remove template" do
+ response.should render_template("#{current_asset[:controller_path]}/remove")
+ end
+
+ it "should load the specified model" do
+ assigns[current_asset[:symbol]].should == text_assets(current_asset[:main_scenario])
+ end
+ end
+
+ describe "via POST" do
+ before :each do
+ post :remove, :id => text_asset_id(current_asset[:main_scenario])
+ end
+
+ it "should destroy the model" do
+ current_asset[:model].find_by_filename(main_scenario_filename(current_asset[:main_scenario])).should be_nil
+ end
+
+ it "should redirect to the index action" do
+ response.should be_redirect
+ response.should redirect_to(send("#{current_asset[:name]}_index_url"))
+ end
+
+ it "should add a flash notice" do
+ flash[:notice].should_not be_nil
+ flash[:notice].should =~ /deleted/
+ end
+ end
+ end
+
+
+ end
+end
+
+
+# reverse calculates the filename created by the scenario (main_js -> main.js)
+def main_scenario_filename(main_scenario_symbolic_name)
+ main_scenario_symbolic_name.to_s.gsub('_', '.')
+end
3  spec/controllers/admin/css_controller_spec.rb
@@ -0,0 +1,3 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+
+describe Admin::CssController do; end
3  spec/controllers/admin/js_controller_spec.rb
@@ -0,0 +1,3 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+
+describe Admin::JsController do; end
190 spec/controllers/text_asset_site_controller_spec.rb
@@ -0,0 +1,190 @@
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe TextAssetSiteController, "routes text_asset requests" do
+ # Pages scenario is used for two reasons. First, we test for conditions where
+ # pages have been created that conflict with css_ or js_directory
+ # values. Secondly, at least one page must exist when SiteController goes
+ # to find an uncached page or else it redirects the user to the login screen.
+ scenario :javascripts, :stylesheets, :pages
+
+ before(:each) do
+ # make sure the css_ and js_directories are the default ones
+ StylesNScripts::Config.restore_defaults
+ ActionController::Routing::Routes.reload!
+
+ # don't bork results with stale cache items
+ controller.text_asset_cache.clear
+ end
+
+ it "should be a SiteController" do
+ controller.should be_kind_of(SiteController)
+ end
+
+ it "should offer a #text_asset_cache method to access TextAssetResponseCache" do
+ controller.text_asset_cache.should be_kind_of(TextAssetResponseCache)
+ end
+
+ [ { :name => 'css',
+ :asset_class => 'css_asset',
+ :default_directory => 'css',
+ :main_scenario => 'main.css' },
+
+ { :name => 'js',
+ :asset_class => 'js_asset',
+ :default_directory => 'js',
+ :main_scenario => 'main.js' }
+
+ ].each do |current_asset|
+ describe current_asset[:asset_class] do
+
+ it "should route urls based on a simple, customized setting for :#{current_asset[:name]}_directory" do
+ # change the css_ or js_direectory and recreate routes to use them
+ StylesNScripts::Config["#{current_asset[:name]}_directory"] = 'foo'
+ ActionController::Routing::Routes.reload!
+
+ params_from(:get, "/foo/#{current_asset[:main_scenario]}").should ==
+ { :controller => 'text_asset_site',
+ :action => 'show_text_asset',
+ :filename => [current_asset[:main_scenario]],
+ :directory => 'foo',
+ :asset_class => current_asset[:asset_class] }
+ end
+
+ it "should route urls based on a multi-level, customized setting for :#{current_asset[:name]}_directory" do
+ # change the css_ or js_direectory and recreate routes to use them
+ StylesNScripts::Config["#{current_asset[:name]}_directory"] = 'foo/bar/baz'
+ ActionController::Routing::Routes.reload!
+
+ params_from(:get, "/foo/bar/baz/#{current_asset[:main_scenario]}").should ==
+ { :controller => 'text_asset_site',
+ :action => 'show_text_asset',
+ :filename => [current_asset[:main_scenario]],
+ :directory => 'foo/bar/baz',
+ :asset_class => current_asset[:asset_class] }
+ end
+
+ it "should route urls based on the default when no custom settings for :#{current_asset[:name]}_directory" do
+ params_from(:get, "/#{current_asset[:default_directory]}/#{current_asset[:main_scenario]}").should ==
+ { :controller => 'text_asset_site',
+ :action => 'show_text_asset',
+ :filename => [current_asset[:main_scenario]],
+ :directory => current_asset[:default_directory],
+ :asset_class => current_asset[:asset_class] }
+ end
+
+ it "should find and render an existing asset" do
+ get :show_text_asset,
+ :filename => [current_asset[:main_scenario]],
+ :directory => current_asset[:default_directory],
+ :asset_class => current_asset[:asset_class]
+# For SOME reason, the response.header does not include a 'status' key so it is
+# not possible to check for success.
+# puts response.headers.keys.sort.inspect
+# response.should be_success
+ response.body.should == "Main #{current_asset[:asset_class]} content"
+ end
+
+ it "should find and render an existing asset on the default dev site" do
+ request.host = "dev.site.com"
+ get :show_text_asset,
+ :filename => [current_asset[:main_scenario]],
+ :directory => current_asset[:default_directory],
+ :asset_class => current_asset[:asset_class]
+# For SOME reason, the response.header does not include a 'status' key so it is
+# not possible to check for success.
+# response.should be_success
+ response.body.should == "Main #{current_asset[:asset_class]} content"
+ end
+
+ it "should render a 404 page for a non-existing asset" do
+ get :show_text_asset,
+ :filename => ['non-existent.file'],
+ :directory => current_asset[:default_directory],
+ :asset_class => current_asset[:asset_class]
+ response.should render_template('site/not_found')
+ response.headers["Status"].should == "404 Not Found"
+ end
+
+ it "should render a 404 page for #{current_asset[:name]}_directory (/#{current_asset[:default_directory]}/)" do
+ get :show_text_asset,
+ :filename => [],
+ :directory => current_asset[:default_directory],
+ :asset_class => current_asset[:asset_class]
+ response.should render_template('site/not_found')
+ response.headers["Status"].should == "404 Not Found"
+ end
+
+ it "should render a 404 page if url includes a deeper path than :#{current_asset[:name]}_directory" do
+ get :show_text_asset,
+ :filename => ['bogus', 'extra', 'path', 'segments', [current_asset[:main_scenario]]],
+ :directory => current_asset[:default_directory],
+ :asset_class => current_asset[:asset_class]
+ response.headers["Status"].should == "404 Not Found"
+ response.should render_template('site/not_found')
+ end
+
+
+ # This is sort of dumb. Really, users should not go anywhere near creating
+ # a page with the same name as the css directory. If they do, here's what
+ # should happen.
+ it "should render a page that is competing with :#{current_asset[:name]}_directory" do
+ create_page current_asset[:default_directory]
+ get :show_text_asset,
+ :filename => [],
+ :directory => current_asset[:default_directory],
+ :asset_class => current_asset[:asset_class]
+ response.should be_success
+ response.body.should == "#{current_asset[:default_directory]} body."
+ end
+
+ # Nor should they be putting child pages inside that competing page...
+ it "should render a page inside :#{current_asset[:name]}_directory (immediate child of /#{current_asset[:default_directory]}/)" do
+ create_page current_asset[:default_directory] do
+ create_page 'page-inside'
+ end
+ get :show_text_asset,
+ :filename => ['page-inside'],
+ :directory => current_asset[:default_directory],
+ :asset_class => current_asset[:asset_class]
+# WHY DOES THIS NEXT LINE FAIL????? AAAARRRRGGGHHH!!
+ response.should be_success
+ response.body.should == 'page-inside body.'
+ end
+
+ # Or grandchildren -- but we'll test for 'em anyway.
+ it "should render a page inside :#{current_asset[:name]}_directory (grandchild of /#{current_asset[:default_directory]}/)" do
+ create_page current_asset[:default_directory] do
+ create_page 'page-inside' do
+ create_page 'another-page'
+ end
+ end
+
+ get :show_text_asset,
+ :filename => ['page-inside', 'another-page'],
+ :directory => current_asset[:default_directory],
+ :asset_class => current_asset[:asset_class]
+ response.should be_success
+ response.body.should == 'another-page body.'
+ end
+
+ # But, if there's a text_asset and page -- both with the same url, then
+ # the asset wins.
+ it "should render the text_asset and not the page if both have the same url" do
+ create_page current_asset[:default_directory] do
+ create_page 'abc.123'
+ end
+
+ send("create_#{current_asset[:asset_class]}", 'abc.123')
+
+ get :show_text_asset,
+ :filename => ['abc.123'],
+ :directory => current_asset[:default_directory],
+ :asset_class => current_asset[:asset_class]
+# response.should be_success
+ response.body.should == 'dummy content'
+ end
+
+
+ end
+ end
+end
11 spec/helpers/admin/css_helper_spec.rb
@@ -0,0 +1,11 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+
+describe Admin::CssHelper do
+
+ #Delete this example and add some real ones or delete this file
+ it "should include the Admin::CssHelper" do
+ included_modules = self.metaclass.send :included_modules
+ included_modules.should include(Admin::CssHelper)
+ end
+
+end
11 spec/helpers/admin/js_helper_spec.rb
@@ -0,0 +1,11 @@
+require File.dirname(__FILE__) + '/../../spec_helper'
+
+describe Admin::JsHelper do
+
+ #Delete this example and add some real ones or delete this file
+ it "should include the Admin::JsHelper" do
+ included_modules = self.metaclass.send :included_modules
+ included_modules.should include(Admin::JsHelper)
+ end
+
+end
11 spec/helpers/text_asset_site_helper_spec.rb
@@ -0,0 +1,11 @@
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe TextAssetSiteHelper do
+
+ #Delete this example and add some real ones or delete this file
+ it "should include the TextAssetSiteHelper" do
+ included_modules = self.metaclass.send :included_modules
+ included_modules.should include(TextAssetSiteHelper)
+ end
+
+end
180 spec/lib/config_spec.rb
@@ -0,0 +1,180 @@
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe StylesNScripts::Config do
+
+ it 'should not accept declarations for invalid setting names' do
+ lambda{StylesNScripts::Config['bogus setting name'] = "whatever"}.should
+ raise_error(RuntimeError,
+ 'text asset response cache location is not settable'
+ )
+ end
+
+
+ it 'should return known settings without having to query the database again' do
+ end
+
+
+ [ :css_directory,
+ :js_directory,
+ :css_mime_type,
+ :js_mime_type,
+ :response_cache_directory
+ ].each do |setting_name|
+ describe setting_name do
+
+ it "should accept declarations where the key name is a symbol" do
+ StylesNScripts::Config[setting_name] = 'a-test-value'
+ StylesNScripts::Config[setting_name].should eql('a-test-value')
+ end
+
+ it "should accept declarations where the key name is a string" do
+ StylesNScripts::Config[setting_name.to_s] = 'a-test-value'
+ StylesNScripts::Config[setting_name.to_s].should eql('a-test-value')
+ end
+
+ end
+ end
+
+
+ { :css_directory => 'css',
+ :js_directory => 'js',
+ :css_mime_type => 'text/css',
+ :js_mime_type => 'text/javascript',
+ :response_cache_directory => 'text_asset_cache'
+ }.each do |setting_name, default_value|
+ describe setting_name do
+
+ it "should set default value when starting with an empty db" do
+ # need to figure out a way to remove our settings from the Config table
+ # we can then call StylesNScripts::Config.restore_defaults to clear our
+ # cache of values
+ end
+
+
+ it "should return default value after calling the #restore_defaults method" do
+ StylesNScripts::Config[setting_name] = 'a-test-value'
+ StylesNScripts::Config.restore_defaults
+ StylesNScripts::Config[setting_name].should eql(default_value)
+ end
+
+ end
+ end
+
+
+ # the following specs apply both to css and js directories...
+ [:css_directory, :js_directory].each do |setting_name|
+ describe setting_name do
+
+ # declare an array of values with alphanumeric-characters, iterate through each and test
+ ['abc', 'ABC', 'aBc', 'AbC123'].each do |alpha_num_value|
+ it "should allow alphanumeric chars (like: #{alpha_num_value.inspect})" do
+ StylesNScripts::Config[setting_name] = alpha_num_value
+ StylesNScripts::Config[setting_name].should eql(alpha_num_value)
+ end
+ end
+
+ # declare an array of values with slashes, iterate through each and test
+ ['aBc-123', 'AbC_123', 'a-B_c-1_23'].each do |hyphen_underscore_value|
+ it "should allow hyphens, and/or underscores (like: #{hyphen_underscore_value.inspect})" do
+ StylesNScripts::Config[setting_name] = hyphen_underscore_value
+ StylesNScripts::Config[setting_name].should eql(hyphen_underscore_value)
+ end
+ end
+
+ # declare an array of values with slashes, iterate through each and test
+ ['aBc/123', 'Ab-C/1_23', 'a/B_c-1/2_3'].each do |slash_value|
+ it "should allow slashes (like: #{slash_value.inspect})" do
+ StylesNScripts::Config[setting_name] = slash_value
+ StylesNScripts::Config[setting_name].should eql(slash_value)
+ end
+ end
+
+ # declare an array of invalid characters, inject them into valid values and test
+ %w[! @ # $ % ^ & * ( ) { } < > + = ? , : ; ' " \\ \t \ \n \r \[ \]].each do |invalid_char|
+ it "should reject invalid characters (like:#{invalid_char.inspect})" do
+ lambda{StylesNScripts::Config[setting_name] = "aBc#{invalid_char}123"}.should raise_error(
+ RuntimeError,
+ "invalid #{setting_name} value: #{('aBc' + invalid_char + '123').inspect}"
+ )
+ end
+ end
+
+ # declare an array of values with bad slashes, iterate through each and test
+ ['/abc/123', 'abc/123/', '/abc/123/','abc//123'].each do |slash_value|
+ it "should reject invalid use of slashes (like: #{slash_value.inspect})" do
+ lambda{StylesNScripts::Config[setting_name] = slash_value}.should raise_error(
+ RuntimeError,
+ "invalid #{setting_name} value: #{(slash_value).inspect}"
+ )
+ end
+ end
+
+
+ end
+ end
+
+
+ # the following specs apply both to css and js mime_types...
+ [:css_mime_type, :js_mime_type].each do |setting_name|
+ describe setting_name do
+
+
+ ['text/javascript', 'text/javascript1.0', 'text/x-javascript', 'text/css',
+ 'application/x-ecmascript'].each do |mime_example|
+ it "should allow valid mime-types (like: #{mime_example.inspect})" do
+ StylesNScripts::Config[setting_name] = mime_example
+ StylesNScripts::Config[setting_name].should eql(mime_example)
+ end
+ end
+
+ # declare an array of invalid characters, inject them into valid values and test
+ %w[! @ # $ % ^ & * ( ) { } < > + = ? , : ; ' " _ \\ \t \ \n \r \[ \]].each do |invalid_char|
+ it "should reject invalid characters (like: #{invalid_char.inspect})" do
+ lambda{StylesNScripts::Config[setting_name] = "text/x-#{invalid_char}javascript"}.should raise_error(
+ RuntimeError,
+ "invalid #{setting_name} value: #{('text/x-' + invalid_char + 'javascript').inspect}"
+ )
+ end
+ end
+
+ it 'should reject mime-types ending in a "/"' do
+ lambda{StylesNScripts::Config[setting_name] = "text/"}.should raise_error(
+ RuntimeError,
+ "invalid #{setting_name} value: #{('text/').inspect}"
+ )
+ end
+
+ end
+ end
+
+
+ describe 'response_cache_directory' do
+
+ # declare an array of values with alphanumeric-characters, iterate through each and test
+ ['abc', 'ABC', 'aBc', 'AbC123'].each do |alpha_num_value|
+ it "should allow a #{:response_cache_directory} with alphanumeric chars (#{alpha_num_value.inspect})" do
+ StylesNScripts::Config[:response_cache_directory] = alpha_num_value
+ StylesNScripts::Config[:response_cache_directory].should eql(alpha_num_value)
+ end
+ end
+
+ # declare an array of values with slashes, iterate through each and test
+ ['aBc-123', 'AbC_123', 'a-B_c-1_23'].each do |hyphen_underscore_value|
+ it "should allow hyphens, and/or underscores (like: #{hyphen_underscore_value.inspect})" do
+ StylesNScripts::Config[:response_cache_directory] = hyphen_underscore_value
+ StylesNScripts::Config[:response_cache_directory].should eql(hyphen_underscore_value)
+ end
+ end
+
+ # declare an array of invalid characters, inject them into valid values and test
+ %w[! @ # $ % ^ & * ( ) { } < > + = ? , : ; ' " \\ \t \ \n \r \[ \] /].each do |invalid_char|
+ it "should reject invalid characters (like:#{invalid_char.inspect})" do
+ lambda{StylesNScripts::Config[:response_cache_directory] = "aBc#{invalid_char}123"}.should raise_error(
+ RuntimeError,
+ "invalid response_cache_directory value: #{('aBc' + invalid_char + '123').inspect}"
+ )
+ end
+ end
+
+ end
+end
95 spec/models/css_asset_and_js_asset_common_spec.rb
@@ -0,0 +1,95 @@
+require File.dirname(__FILE__) + '/../spec_helper'
+
+[ CssAsset, JsAsset].each do |current_asset|
+ describe current_asset do
+
+ before(:each) do
+ @record = current_asset.new
+ end
+
+
+ it 'should limit the filename to 100 characters' do
+ @record.filename = 'a' * 100
+ @record.should be_valid
+
+ @record.filename = 'a' * 101
+ @record.should_not be_valid
+ end
+
+
+ it 'should not allow an empty filename' do
+ @record.filename = ''
+ @record.should_not be_valid
+ end
+
+
+ it 'should require unique filenames (within same subclass)' do
+ @record.filename = 'abc.123'
+ @record.save!
+
+ @invalid_record = current_asset.new(:filename => 'abc.123')
+ @invalid_record.should_not be_valid
+ @invalid_record.should have(1).error_on(:filename)
+ @invalid_record.errors.on(:filename).should == 'filename already in use'
+ end
+
+
+ it 'should permit the same filename across different subclasses' do
+ @record.filename = 'abc.123'
+ @record.save!
+
+ if current_asset == CssAsset
+ @record_of_other_subclass = JsAsset.new(:filename => 'abc.123')
+ elsif current_asset == JsAsset
+ @record_of_other_subclass = CssAsset.new(:filename => 'abc.123')
+ end
+ @record_of_other_subclass.should be_valid
+ end
+
+
+ it 'should allow filenames with alphanumeric chars, underscores, periods, & hyphens' do
+ @record.filename = 'abc'
+ @record.should be_valid
+
+ @record.filename = 'ABC'
+ @record.should be_valid
+
+ @record.filename = 'Abc123'
+ @record.should be_valid
+
+ @record.filename = 'aBc.123'
+ @record.should be_valid
+
+ @record.filename = 'aBc_123'
+ @record.should be_valid
+
+ @record.filename = 'aBc-123'
+ @record.should be_valid
+
+ @record.filename = 'a.B-c.1_2-3'
+ @record.should be_valid
+ end
+
+
+ # declare an array of invalid characters then iterate through each and test
+ (
+ %w[! @ # $ % ^ & * ( ) { } \ / < > + = ? , : ; ' "] +
+ [' ', "\t", "\n", "\r", '[', ']']
+ ).each do |invalid_char|
+ it "should not allow filenames with invalid characters (#{invalid_char.inspect})" do
+ @record.filename = "abc#{invalid_char}123"
+ @record.should_not be_valid
+ @record.should have(1).error_on(:filename)
+ @record.errors.on(:filename).should == 'invalid format'
+ end
+ end
+
+
+ it 'should yeild the same output for #content and #raw_content methods' do
+ @record.filename = "valid.filename"
+ @record.raw_content = "my raw content"
+ @record.content.should == "my raw content"
+ end
+
+ end
+end
34 spec/models/css_asset_minify_spec.rb
@@ -0,0 +1,34 @@
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe CssAsset do
+ scenario :stylesheets
+
+ it "should compress stylesheets when 'minify' is set to TRUE" do
+ create_css_asset('radiant.css',
+ :minify => true,
+ :raw_content => sample('radiant.css'))
+ css_assets('radiant_css').content.should == sample('radiant.minified.css')
+ end
+
+ it "should not compress stylesheets when 'minify' is set to FALSE" do
+ create_css_asset('radiant.css',
+ :minify => false,
+ :raw_content => sample('radiant.css'))
+ css_assets('radiant_css').content.should == sample('radiant.css')
+ end
+
+ it "should properly remove trailing semicolons, combine 4 common dimensions into one and 2 sets of 2 dimensions into 2 (see samples for more info)" do
+ create_css_asset('test.css',
+ :minify => true,
+ :raw_content => sample('test.css'))
+ puts "calculated minified: " + css_assets('test_css').content.length.to_s
+ css_assets('test_css').content.should == sample('test.minified.css')
+ end
+
+
+ private
+ def sample(filename)
+ open(File.dirname(__FILE__) + '/../samples/' + filename) { |f| f.read }
+ end
+
+end
25 spec/models/js_asset_minify_spec.rb
@@ -0,0 +1,25 @@
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe JsAsset do
+ scenario :javascripts
+
+ it "should compress javascripts when 'minify' is set to TRUE" do
+ create_js_asset('prototype.js',
+ :minify => true,
+ :raw_content => sample('prototype.js'))
+ js_assets('prototype_js').content.should == sample('prototype.minified.js')
+ end
+
+ it "should not compress javascripts when 'minify' is set to FALSE" do
+ create_js_asset('prototype.js',
+ :minify => false,
+ :raw_content => sample('prototype.js'))
+ js_assets('prototype_js').content.should == sample('prototype.js')
+ end
+
+ private
+ def sample(filename)
+ open(File.dirname(__FILE__) + '/../samples/' + filename) { |f| f.read }
+ end
+
+end
98 spec/models/text_asset_response_cache_spec.rb
@@ -0,0 +1,98 @@
+require File.dirname(__FILE__) + '/../spec_helper'
+
+describe TextAssetResponseCache do
+
+ class TestResponse < ActionController::TestResponse
+ def initialize(body = '', headers = {})
+ self.body = body
+ self.headers = headers
+ end
+ end
+
+
+ before :all do
+ @dir = File.expand_path("#{RAILS_ROOT}/test/text_asset_cache")
+ @baddir = File.expand_path("#{RAILS_ROOT}/test/bad_text_asset_cache")
+ @old_perform_caching = TextAssetResponseCache.defaults[:perform_caching]
+ TextAssetResponseCache.defaults[:perform_caching] = true
+ end
+
+ before(:each) do
+ FileUtils.rm_rf @baddir
+ @cache = TextAssetResponseCache.new(
+ :directory => @dir,
+ :perform_caching => true
+ )
+ @cache.clear
+ end
+
+ after :each do
+ FileUtils.rm_rf @dir if File.exists? @dir
+ end
+
+ after :all do
+ TextAssetResponseCache.defaults[:perform_caching] = @old_preform_caching
+ end
+
+
+
+ it 'should inherit from the Radiant ResponseCache' do
+ @cache.should be_kind_of(ResponseCache)
+ end
+
+ it 'should set up its own instance (be independent of the Radiant ResponseCache' do
+ @cache.should_not === ResponseCache.instance
+ end
+
+ it 'should have its default directory equal to StylesNScripts::Config setting' do
+ # change the config setting, re-init the TextAssetResponseCache (a Singleton)
+ # and make sure it slurped up the new setting.
+ StylesNScripts::Config[:response_cache_directory] = 'foo'
+ Singleton.send :__init__, TextAssetResponseCache
+ TextAssetResponseCache.instance.defaults[:directory].should == "#{RAILS_ROOT}/foo"
+
+ # reset the config settings, re-init the TextAssetResponseCache (a Singleton)
+ # and make sure it aquired the default setting this time.
+ StylesNScripts::Config.restore_defaults
+ Singleton.send :__init__, TextAssetResponseCache
+ TextAssetResponseCache.instance.defaults[:directory].should == "#{RAILS_ROOT}/text_asset_cache"
+ end
+
+ it 'should have a default cache expiration of 1 year' do
+ @cache.defaults[:expire_time].should == 1.year
+ end
+
+
+# These specs are stolen directly from the response_cache_spec. Mostly these
+# are redundant tests but I at least wanted to ensure that it actually caches
+ ['test/me', '/test/me', 'test/me/', '/test/me/', 'test//me'].each do |url|
+ it "should cache response for url: #{url.inspect}" do
+ @cache.clear
+ response = response('content', 'Last-Modified' => 'Tue, 27 Feb 2007 06:13:43 GMT')
+ response.cache_timeout = Time.gm(2007, 2, 8, 17, 37, 9)
+ @cache.cache_response(url, response)
+ name = "#{@dir}/test/me.yml"
+ File.exists?(name).should == true
+ file(name).should == "--- \nexpires: 2007-02-08 17:37:09 Z\nheaders: \n Last-Modified: Tue, 27 Feb 2007 06:13:43 GMT\n"
+ data_name = "#{@dir}/test/me.data"
+ file(data_name).should == "content"
+ end
+ end
+
+ it 'cache response with extension' do
+ @cache.cache_response("styles.css", response('content'))
+ File.exists?("#{@dir}/styles.css.yml").should == true
+ end
+
+ private
+
+ def file(filename)
+ open(filename) { |f| f.read } rescue ''
+ end
+
+ def response(*args)
+ TestResponse.new(*args)
+ end
+
+end
+
4,184 spec/samples/prototype.js
4,184 additions, 0 deletions not shown
264 spec/samples/prototype.minified.js
@@ -0,0 +1,264 @@
+
+var Prototype={Version:'1.6.0',Browser:{IE:!!(window.attachEvent&&!window.opera),Opera:!!window.opera,WebKit:navigator.userAgent.indexOf('AppleWebKit/')>-1,Gecko:navigator.userAgent.indexOf('Gecko')>-1&&navigator.userAgent.indexOf('KHTML')==-1,MobileSafari:!!navigator.userAgent.match(/Apple.*Mobile.*Safari/)},BrowserFeatures:{XPath:!!document.evaluate,ElementExtensions:!!window.HTMLElement,SpecificElementExtensions:document.createElement('div').__proto__&&document.createElement('div').__proto__!==document.createElement('form').__proto__},ScriptFragment:'<script[^>]*>([\\S\\s]*?)<\/script>',JSONFilter:/^\/\*-secure-([\s\S]*)\*\/\s*$/,emptyFunction:function(){},K:function(x){return x}};if(Prototype.Browser.MobileSafari)
+Prototype.BrowserFeatures.SpecificElementExtensions=false;if(Prototype.Browser.WebKit)
+Prototype.BrowserFeatures.XPath=false;var Class={create:function(){var parent=null,properties=$A(arguments);if(Object.isFunction(properties[0]))
+parent=properties.shift();function klass(){this.initialize.apply(this,arguments);}
+Object.extend(klass,Class.Methods);klass.superclass=parent;klass.subclasses=[];if(parent){var subclass=function(){};subclass.prototype=parent.prototype;klass.prototype=new subclass;parent.subclasses.push(klass);}
+for(var i=0;i<properties.length;i++)
+klass.addMethods(properties[i]);if(!klass.prototype.initialize)
+klass.prototype.initialize=Prototype.emptyFunction;klass.prototype.constructor=klass;return klass;}};Class.Methods={addMethods:function(source){var ancestor=this.superclass&&this.superclass.prototype;var properties=Object.keys(source);if(!Object.keys({toString:true}).length)
+properties.push("toString","valueOf");for(var i=0,length=properties.length;i<length;i++){var property=properties[i],value=source[property];if(ancestor&&Object.isFunction(value)&&value.argumentNames().first()=="$super"){var method=value,value=Object.extend((function(m){return function(){return ancestor[m].apply(this,arguments)};})(property).wrap(method),{valueOf:function(){return method},toString:function(){return method.toString()}});}
+this.prototype[property]=value;}
+return this;}};var Abstract={};Object.extend=function(destination,source){for(var property in source)
+destination[property]=source[property];return destination;};Object.extend(Object,{inspect:function(object){try{if(object===undefined)return'undefined';if(object===null)return'null';return object.inspect?object.inspect():object.toString();}catch(e){if(e instanceof RangeError)return'...';throw e;}},toJSON:function(object){var type=typeof object;switch(type){case'undefined':case'function':case'unknown':return;case'boolean':return object.toString();}
+if(object===null)return'null';if(object.toJSON)return object.toJSON();if(Object.isElement(object))return;var results=[];for(var property in object){var value=Object.toJSON(object[property]);if(value!==undefined)
+results.push(property.toJSON()+': '+value);}
+return'{'+results.join(', ')+'}';},toQueryString:function(object){return $H(object).toQueryString();},toHTML:function(object){return object&&object.toHTML?object.toHTML():String.interpret(object);},keys:function(object){var keys=[];for(var property in object)
+keys.push(property);return keys;},values:function(object){var values=[];for(var property in object)
+values.push(object[property]);return values;},clone:function(object){return Object.extend({},object);},isElement:function(object){return object&&object.nodeType==1;},isArray:function(object){return object&&object.constructor===Array;},isHash:function(object){return object instanceof Hash;},isFunction:function(object){return typeof object=="function";},isString:function(object){return typeof object=="string";},isNumber:function(object){return typeof object=="number";},isUndefined:function(object){return typeof object=="undefined";}});Object.extend(Function.prototype,{argumentNames:function(){var names=this.toString().match(/^[\s\(]*function[^(]*\((.*?)\)/)[1].split(",").invoke("strip");return names.length==1&&!names[0]?[]:names;},bind:function(){if(arguments.length<2&&arguments[0]===undefined)return this;var __method=this,args=$A(arguments),object=args.shift();return function(){return __method.apply(object,args.concat($A(arguments)));}},bindAsEventListener:function(){var __method=this,args=$A(arguments),object=args.shift();return function(event){return __method.apply(object,[event||window.event].concat(args));}},curry:function(){if(!arguments.length)return this;var __method=this,args=$A(arguments);return function(){return __method.apply(this,args.concat($A(arguments)));}},delay:function(){var __method=this,args=$A(arguments),timeout=args.shift()*1000;return window.setTimeout(function(){return __method.apply(__method,args);},timeout);},wrap:function(wrapper){var __method=this;return function(){return wrapper.apply(this,[__method.bind(this)].concat($A(arguments)));}},methodize:function(){if(this._methodized)return this._methodized;var __method=this;return this._methodized=function(){return __method.apply(null,[this].concat($A(arguments)));};}});Function.prototype.defer=Function.prototype.delay.curry(0.01);Date.prototype.toJSON=function(){return'"'+this.getUTCFullYear()+'-'+
+(this.getUTCMonth()+1).toPaddedString(2)+'-'+
+this.getUTCDate().toPaddedString(2)+'T'+
+this.getUTCHours().toPaddedString(2)+':'+
+this.getUTCMinutes().toPaddedString(2)+':'+
+this.getUTCSeconds().toPaddedString(2)+'Z"';};var Try={these:function(){var returnValue;for(var i=0,length=arguments.length;i<length;i++){var lambda=arguments[i];try{returnValue=lambda();break;}catch(e){}}
+return returnValue;}};RegExp.prototype.match=RegExp.prototype.test;RegExp.escape=function(str){return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g,'\\$1');};var PeriodicalExecuter=Class.create({initialize:function(callback,frequency){this.callback=callback;this.frequency=frequency;this.currentlyExecuting=false;this.registerCallback();},registerCallback:function(){this.timer=setInterval(this.onTimerEvent.bind(this),this.frequency*1000);},execute:function(){this.callback(this);},stop:function(){if(!this.timer)return;clearInterval(this.timer);this.timer=null;},onTimerEvent:function(){if(!this.currentlyExecuting){try{this.currentlyExecuting=true;this.execute();}finally{this.currentlyExecuting=false;}}}});Object.extend(String,{interpret:function(value){return value==null?'':String(value);},specialChar:{'\b':'\\b','\t':'\\t','\n':'\\n','\f':'\\f','\r':'\\r','\\':'\\\\'}});Object.extend(String.prototype,{gsub:function(pattern,replacement){var result='',source=this,match;replacement=arguments.callee.prepareReplacement(replacement);while(source.length>0){if(match=source.match(pattern)){result+=source.slice(0,match.index);result+=String.interpret(replacement(match));source=source.slice(match.index+match[0].length);}else{result+=source,source='';}}
+return result;},sub:function(pattern,replacement,count){replacement=this.gsub.prepareReplacement(replacement);count=count===undefined?1:count;return this.gsub(pattern,function(match){if(--count<0)return match[0];return replacement(match);});},scan:function(pattern,iterator){this.gsub(pattern,iterator);return String(this);},truncate:function(length,truncation){length=length||30;truncation=truncation===undefined?'...':truncation;return this.length>length?this.slice(0,length-truncation.length)+truncation:String(this);},strip:function(){return this.replace(/^\s+/,'').replace(/\s+$/,'');},stripTags:function(){return this.replace(/<\/?[^>]+>/gi,'');},stripScripts:function(){return this.replace(new RegExp(Prototype.ScriptFragment,'img'),'');},extractScripts:function(){var matchAll=new RegExp(Prototype.ScriptFragment,'img');var matchOne=new RegExp(Prototype.ScriptFragment,'im');return(this.match(matchAll)||[]).map(function(scriptTag){return(scriptTag.match(matchOne)||['',''])[1];});},evalScripts:function(){return this.extractScripts().map(function(script){return eval(script)});},escapeHTML:function(){var self=arguments.callee;self.text.data=this;return self.div.innerHTML;},unescapeHTML:function(){var div=new Element('div');div.innerHTML=this.stripTags();return div.childNodes[0]?(div.childNodes.length>1?$A(div.childNodes).inject('',function(memo,node){return memo+node.nodeValue}):div.childNodes[0].nodeValue):'';},toQueryParams:function(separator){var match=this.strip().match(/([^?#]*)(#.*)?$/);if(!match)return{};return match[1].split(separator||'&').inject({},function(hash,pair){if((pair=pair.split('='))[0]){var key=decodeURIComponent(pair.shift());var value=pair.length>1?pair.join('='):pair[0];if(value!=undefined)value=decodeURIComponent(value);if(key in hash){if(!Object.isArray(hash[key]))hash[key]=[hash[key]];hash[key].push(value);}
+else hash[key]=value;}
+return hash;});},toArray:function(){return this.split('');},succ:function(){return this.slice(0,this.length-1)+
+String.fromCharCode(this.charCodeAt(this.length-1)+1);},times:function(count){return count<1?'