From a71ce1f9c9f58c2d706b0a7966d59a65c7dddbfa Mon Sep 17 00:00:00 2001 From: Alan Harper Date: Tue, 26 Aug 2008 11:09:49 +1000 Subject: [PATCH] Initial checkin of code and patches spread across the interwebs --- LICENSE | 20 +++ README | 202 ++++++++++++++++++++++ generators/theme/USAGE | 15 ++ generators/theme/templates/about.markdown | 5 + generators/theme/templates/layout.liquid | 11 ++ generators/theme/templates/layout.rhtml | 11 ++ generators/theme/templates/preview.png | Bin 0 -> 3649 bytes generators/theme/templates/theme.css | 7 + generators/theme/templates/theme.yml | 4 + generators/theme/templates/views_readme | 13 ++ generators/theme/theme_generator.rb | 27 +++ init.rb | 18 ++ lib/helpers/liquid_theme_tags.rb | 31 ++++ lib/helpers/rhtml_theme_tags.rb | 66 +++++++ lib/patches/actioncontroller_ex.rb | 59 +++++++ lib/patches/actionmailer_ex.rb | 27 +++ lib/patches/actionview_ex.rb | 45 +++++ lib/patches/routeset_ex.rb | 26 +++ lib/theme.rb | 58 +++++++ lib/theme_controller.rb | 61 +++++++ lib/theme_error.rb | 4 + lib/theme_helper.rb | 3 + tasks/themes.rake | 23 +++ 23 files changed, 736 insertions(+) create mode 100755 LICENSE create mode 100755 README create mode 100755 generators/theme/USAGE create mode 100755 generators/theme/templates/about.markdown create mode 100755 generators/theme/templates/layout.liquid create mode 100755 generators/theme/templates/layout.rhtml create mode 100755 generators/theme/templates/preview.png create mode 100755 generators/theme/templates/theme.css create mode 100755 generators/theme/templates/theme.yml create mode 100755 generators/theme/templates/views_readme create mode 100755 generators/theme/theme_generator.rb create mode 100755 init.rb create mode 100755 lib/helpers/liquid_theme_tags.rb create mode 100755 lib/helpers/rhtml_theme_tags.rb create mode 100755 lib/patches/actioncontroller_ex.rb create mode 100755 lib/patches/actionmailer_ex.rb create mode 100755 lib/patches/actionview_ex.rb create mode 100755 lib/patches/routeset_ex.rb create mode 100755 lib/theme.rb create mode 100755 lib/theme_controller.rb create mode 100755 lib/theme_error.rb create mode 100755 lib/theme_helper.rb create mode 100755 tasks/themes.rake diff --git a/LICENSE b/LICENSE new file mode 100755 index 0000000..1fef251 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2005 Matt McCray, based on code from Typo by Tobias Luetke + +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 SOFTWAR \ No newline at end of file diff --git a/README b/README new file mode 100755 index 0000000..64d0e6a --- /dev/null +++ b/README @@ -0,0 +1,202 @@ +This theme_support version has been updated by Damien Le Berrigaud for Rails 2.0 +http://www.webdrivenblog.com/ + += Theme Support for Rails Applications + +This plugin provides support for themes to the rails application environment. +It supports theme specific images, stylesheets, javascripts, and views. The +views can be in ERb (rhtml) or liquid formats. Optionally, you can configure +the theme system to ignore any templates except liquid ones. + + +== Usage + +This plugin automatically makes any patches needed for theme support. You can +use the theme_generator to create the file structure needed, or create it +yourself. + +It expects the following theme folder structure. + + $app_root + themes/ + [theme_name] + layouts/ <- layout .rhtml or .liquid templates + images/ + stylesheets/ + javascripts/ + views/ <- you can override application views + about.markdown + preview.png + +When run in production mode, it will automatically cache the theme files so that +the web-server will deliver them in subsequent requests. + +It bears noting that, like Typo, this will mean your apache/fcgi process will need +write permissions. This could be a possible security vulnerability. + +With that in mind, it is best to pre-cache all of the theme files by using the +included rake tasks: + + $ rake theme_create_cache + +The theme file cache generates the following file structure: + + $app_root + public/ + themes/ + [theme_name]/ + images/ + stylesheets/ + javascripts/ + +There are other rake tasks available: + + - theme_create_cache + - theme_remove_cache + - theme_update_cache + +You specify which theme to use in your controller by using the 'theme' helper. +It's used just like the 'layout' helper. In fact, you can use them both +simultaneously. The following will render actions using the 'default' layout +in the 'blue_bird' theme (theme/blue_bird/layouts/default.rhtml): + + class ApplicationController < ActionController::Base + layout 'default' + + theme 'blue_bird' + + ... + end + +You can also defer the theme lookup to a controller method: + + class ApplicationController < ActionController::Base + layout 'default' + + theme :get_theme + + def get_theme + + # If you let the user choose their own theme, you might + # add a User#theme field... + + current_user.theme + end + + ... + end + + +Note: By setting the theme in the ApplicationController you can set +the theme for the whole application. + +In your application views, there are theme specific helper tags +available to you. For ERb templates they are: + + - theme_image_tag + - theme_image_path + - theme_javascript_include_tag + - theme_javascript_path + - theme_stylesheet_link_tag + - theme_stylesheet_path + +For liquid templates there is a single helper, themeitem, that will determine +the path base on the theme file extension. Here's how you'd use it: + + + ... + + +The output from those two calls are: + + + ... + + +New in version 1.4 is ActionMailer support. Note, this is still experimental. However, +if you would like your themes to be able to override your ActionMailer views, you can +send the theme in your deliver_* method call. For example, assuming we have an ActionMailer +class named Mailer, and have implemented theme_support as shown above, here's how you would +allowing themes in emails: + + + def send_email + Mailer.deliver_my_email( 'a variable', :theme=>get_theme ) + end + + +== Contributors + +The theme_support pluging includs patches from the following: + +* agkr +* D.J. Vogel + +Thanks guys! + +== Changelog + + 1.4.0 - Better support for Rails 1.1+. Updated the liquid themeitem tag. + Liquid layouts are no longer generated by default.Added a couple + of patches. One allows theme sub-directories. For example, you + can have: + + [theme_dir] + stylesheets/ + admin/ + main.css + + Added experimental support for themeing ActionMailer classes. + They work as normal, if you want to all theme's to override the + .rhtml|.liquid views, send the theme in the deliver_* method. For + example: + + Mailer.deliver_signup( user, :theme=>get_theme() ) + + In that example, `get_theme` is a method on the controller and at + the top we've used: + + layout 'default' + theme :get_theme + + 1.3.0 - The theme_system component is no longer needed. All of the + theme support is driven by a single plugin named, oddly enough, + 'theme_support'. Also improved theme route support. Instead of + overriding RouteSet#reload, RouteSet#draw is overridden, making + the theme support entirely transparent -- hopefully ;-) + + 1.2.2 - More Bug-fixes. + + 1.2.1 - Bug-fixes and documentation clean up. + + 1.2.0 - Updated actionview_ex with the new render_file additions from + Typo. Renamed the rake tasks so that they all start with + 'theme' (theme_create_cache, theme_remove_cache, + theme_update_cache). You can update the system files by running: + + $ ./script/generate theme _system_ + + Full support for Liquid templates, as well as an option to only + allow Liquid templates in a theme. + + 1.1.1 - Added rake tasks for pre-caching the themes, and removing the + cached themes. + + 1.1.0 - [Breaking] Abstraction of the Typo theme system. The themes are + now the same as is used in Typo. The theme engine itself is a + combination of plugins and a component. No more symlinks, thank + goodness. + + 1.0.2 - The current_theme is now retrieved from the controller. Where + symlinks are created on *nix systems, shortcuts are created + on Windows if Win32Utils is installed. + + 1.0.1 - Added 'themes' directory, theme definition file, and symlinks + to the appropriate directories on supported platforms. + + 1.0.0 - Initial release + + +--- +Copyright (c) 2005 Matt McCray, based on code from Typo by Tobias Luetke +released under the MIT license diff --git a/generators/theme/USAGE b/generators/theme/USAGE new file mode 100755 index 0000000..6927cbd --- /dev/null +++ b/generators/theme/USAGE @@ -0,0 +1,15 @@ +NAME + theme - Creates the folder structure for a new theme + +SYNOPSIS + theme [theme_name] + +DESCRIPTION + This generator creates the folder structure for a new theme. It creates all of + the folders for your theme content (images, stylesheets, javascripts, layouts, + and views). + +EXAMPLE + ./script/generate theme default + + This will generate the file structure for a theme named 'default'. diff --git a/generators/theme/templates/about.markdown b/generators/theme/templates/about.markdown new file mode 100755 index 0000000..fb3b3c9 --- /dev/null +++ b/generators/theme/templates/about.markdown @@ -0,0 +1,5 @@ +### <%= class_name.underscore.humanize.titleize %> + +Author: *Me* + +This description can be found in themes/<%= file_name %>/about.markdown diff --git a/generators/theme/templates/layout.liquid b/generators/theme/templates/layout.liquid new file mode 100755 index 0000000..b24718c --- /dev/null +++ b/generators/theme/templates/layout.liquid @@ -0,0 +1,11 @@ + + + + + + + + {{ content_for_layout }} + + \ No newline at end of file diff --git a/generators/theme/templates/layout.rhtml b/generators/theme/templates/layout.rhtml new file mode 100755 index 0000000..a74995c --- /dev/null +++ b/generators/theme/templates/layout.rhtml @@ -0,0 +1,11 @@ + + + + + <%%= theme_stylesheet_link_tag '<%= file_name %>' %> + + + <%%= @content_for_layout %> + + \ No newline at end of file diff --git a/generators/theme/templates/preview.png b/generators/theme/templates/preview.png new file mode 100755 index 0000000000000000000000000000000000000000..182c1f71b9db23ad030d71a6cb7bdd81af699fba GIT binary patch literal 3649 zcmb_fc{J4D-~XftDPjiMLmCWOBayO`HH=9V#?n~E&LmkI+gS2dM5GdBX3&IeW1k^o z3o$f}K~&7w7g@{po9{W#dH#N$=REh^_xpYBy??yUeZSxLbzV2!-q!piuNW@?04FUi zOm48_N4Dj2bFe+A&EA$Bj)ogqI&pJz(`W5w*h`)ei<{v9z`Odd9Wf$kJ!Nm6i!g;p zI0pMhM0 z1d2@wC-8UkiC(;&V`61q5oTq9)Omil!P*-~8qM>sui?Q*C>s<8!fI-biWoLvw>GU# zE2;=Z%l5Jf{izDyd%Ok2{^4x=?a=)7>f_>EbK=9~A7fOpl-VJHE2ZK8lqBoS>z+~@ zFfDW2;#9kH9x5!yDU8kKz)~y{DPFHTywq=a5JeT=!81W{s2T77{@C8{!I1~<1ECUM zg>}e?9js61IS}uN-P+3lWBne3r0pDLIp_$`J!}6pN|GOXQ$PUxGRV}y!~3sBo6PpD z6$2^5Fm>J2KZW*9%1ve`-^j?Oocjs`nL2{;C}16%{f+FUz8IXg zCKSKF=~6>;baoa$yM$RAa2~p!q35*#$};n5Ym z4DB7q*uD)hEOm7@BrVi#VnH*`bgYg-q3}yzd12WY9_s~}Sb3fi4XxrzPQ(q7UQlBt ze>UH)f*~DvfSv-Wu30t76-BR+$B%v_xa6~FH4UyR)_3ZD3YuBR`X$(}9-k1|AbACytXKsj>4M}bD_r{ySQRU$!*hG>RYPm6x1Cc!q% z(_mRlv90E-)YI5&g`bpMPIRHa7G8s9!(AkJovP*06;j2#XKJkP<`x-(sm*W&-Lxu4 zHGi;z&UZ_UU)x_z{%N^=ml(U3;?$T9zP{avV732hzZoR#qoWa5=-?3wm2r;8{D0no z@jeIrp@BD}%8mf|l+Y=ftRyo%(jG!m28HaRRLx5}v4LBY?}3@wg98ko?Q6|K?XgVO zpRe69^QISXDLw&arofG5E0>8&<4OmtqaE?@r{Zr4qoi*MFSkGHOt-4sP%60J*d8xe zaxk!Vri-RMVcE2&RFJ3sYpJKFWO;c-O5cJ&01_C1R(iP~yZ86~vcth$;2k$8fX*AH z;m6mX8+;%zH3|vUhXTAlk^2qS-_f91g<5~n=*ed?!sn|qQ=_syNzZF3You^3c9~7 z+T5hPVnNZE%PF2ui4P;;&G>2RBqpqxAp*yJ)$RxV14mAjBRrGsPNTYGJqoaK>Z@2TK2$ zos?#-_1xMMhoUPGPzZksMM*d`=MwDlZPkM;IMc7TzY+vES(R(XOU4g-ugXou9PAZ- z3p9w&-aS#VxCW{+OFm^Aglc|nBcB)jpbSb$b}AXm&Kaz zIle2z9hhj`Q_DCR>fWoC&QG&m-z+naszrjai`HEHSQV9AY1NACGqgxP`dxl%T{F=^ z@yXx5LhEt|{#Ex6-2Zs(xDuSZ;q)&t4K257#$UjMN?&zd2SIYJUj;c*1T6`Kr)2|S zQW4Eol@ZhP5ezK{7#>biw<=q-{!YU1T|i80@ez(^tDK;?Y7RcFF4*3h2J>!64To^8Ky+uYmfv__t@#$AH)y!nDzyA_fq7Ov-M4vr09=KG}4c7QMyW)CE@RS z|IJfAYK51R5217ND6k6cU(^ERVX(dYg4#XF=^5xDiXee+hWg)I# zcXYRu&%>uAnl=>l!?pSJ-F@N>D_A?Od||0wWDdDhM&PTH{gs~|uC-YV%rtbp5eav zp=J8qpRQ))E9H$u0<(q2ptf%K$!E_VZTnvU&e{zM=BGR^cKG#Cw0tue4{(V6()9K@}?3%M)eIBG} z|7;Yl^1|go3g1h*vio z|LwDswQv0ec30wJzw5HS8~qCh&@xr5h>ww;AKKdLZV0YK5N;@y&=&QYjFqn*t3ki1 znes5FhABnfQ2i!(v#8E4D9cql6IRCNq)q?L$@dBohISphGU(kf@Qe5DIkT|86J$sc z4>&Oi>6?e~f1ZG?$YB9l2Hfw)oWPIxSPrg zuZzWB#n=?-khb8ogi>WQR<#R$61PmC8)(@{_AZRW!+&9R2AE3p`?2J+>@Qu}F1~8(< zIyrKTZ1hxWbgf^mds_cNOyJEbGvmFB+2+1t^Pzf@v`8wda&T}RRm|-F5xv6fJyo^f zq*TqCWW@WL7nSndAPe?Kg&G<_SC-Q~zmX>1HdZ6#MA#&Gu4SaLw$*?|8PMatxyeXA zF4TrwAhP-oV^8kG*GK%dY@sa8-BJ>N()P2~hTffb#w_Y4V$j@;W@yNVmiC5SN}7HN ze$DPe?*CUM_f!>jmn_mK*7GAxBMp{$z3cRU>qOt=Uo-RFz5>{Og7OeLQv2n>mb|bs zrNctb)Zx?duL7*-H4nR5BeBmPf_Ov*jwO_p$t`HT9P22bG-cpQ-_Z#F19AV*D*jySeWeM(i!Ia zmX;GhQH81O>Bxu5cRM921@2^)ZS3w_m5;JEFHnR~Gn=#1w zziwrOQOg(O(ODIM=ziC1NCSO;eb`V$L?qI^J#N35uuXPTGB-C*O-*G(A5UOEcM$d< zjM;cAM9=GdpVXG&-5>&%wQ9ibQ4AG5{@LOWO^p53z z^7*Z;yx>_p1oaaBSb*ZgO=gv5yiO~};VO{HogQkUFAO%I2HL8^;@{)lq92 zUc||+E{+}*URMy&MBfyumj{_(3%OINz|~#0q|_2y-rk+7eG9@a#FIJPGUaf%QR(XP zM$>Cp+57;npNd|J8FRX}x)MI96ePgn&?}cRbmEC*%ouel(cWI6V!!h{0H2{uHnNzj)iL@Dh`Uo zKtEzq8d}JXv?yz(;M=h#j+t3FEKhy}X*{cEVu*Z@Na59b1)p0={#mFT-3eFPP!i_O z*L{!rt24U`{hr$T' theme +*/ + +BODY { + +} \ No newline at end of file diff --git a/generators/theme/templates/theme.yml b/generators/theme/templates/theme.yml new file mode 100755 index 0000000..f067d16 --- /dev/null +++ b/generators/theme/templates/theme.yml @@ -0,0 +1,4 @@ +enabled: true +title: <%= class_name.underscore.humanize.titleize %> +author: Matt McCray +description: This is the description... All light and fluffy. diff --git a/generators/theme/templates/views_readme b/generators/theme/templates/views_readme new file mode 100755 index 0000000..548ce6b --- /dev/null +++ b/generators/theme/templates/views_readme @@ -0,0 +1,13 @@ +# Overriding Views + +You can override views by putting custom `rhtml` or `liquid` +templates in this directory. You use the same folder structure +as Rails: + + views/ + [controller_name]/ + _overriden_partial.rhtml + overriden_action.rhtml + +*Note:* These are overrides! They will only work if they have +a matching view in the main rails `app/views` folder. \ No newline at end of file diff --git a/generators/theme/theme_generator.rb b/generators/theme/theme_generator.rb new file mode 100755 index 0000000..170a0e9 --- /dev/null +++ b/generators/theme/theme_generator.rb @@ -0,0 +1,27 @@ +class ThemeGenerator < Rails::Generator::NamedBase + + def manifest + record do |m| + # Theme folder(s) + m.directory File.join( "themes", file_name ) + # theme content folders + m.directory File.join( "themes", file_name, "images" ) + m.directory File.join( "themes", file_name, "javascript" ) + m.directory File.join( "themes", file_name, "layouts" ) + m.directory File.join( "themes", file_name, "views" ) + m.directory File.join( "themes", file_name, "stylesheets" ) + # Default files... + # about + m.template 'about.markdown', File.join( 'themes', file_name, 'about.markdown' ) + # image + m.file 'preview.png', File.join( 'themes', file_name, 'images', 'preview.png' ) + # stylesheet + m.template "theme.css", File.join( "themes", file_name, "stylesheets", "#{file_name}.css" ) + # layouts + m.template 'layout.rhtml', File.join( 'themes', file_name, 'layouts', 'default.rhtml' ) + #m.template 'layout.liquid', File.join( 'themes', file_name, 'layouts', 'default.liquid' ) + # view readme + m.template 'views_readme', File.join( 'themes', file_name, 'views', 'views_readme.txt' ) + end + end +end \ No newline at end of file diff --git a/init.rb b/init.rb new file mode 100755 index 0000000..e5d87a9 --- /dev/null +++ b/init.rb @@ -0,0 +1,18 @@ +# Initializes theme support by extending some of the core Rails classes +require 'patches/actionview_ex' +require 'patches/actioncontroller_ex' +require 'patches/actionmailer_ex' +require 'patches/routeset_ex' + +# Add the tag helpers for rhtml and, optionally, liquid templates +require 'helpers/rhtml_theme_tags' + +# Commented out to remove the message +# "Liquid doesn't seem to be loaded... uninitialized constant Liquid" + +#begin +# require 'helpers/liquid_theme_tags' +#rescue +# # I guess Liquid isn't being used... +# STDERR.puts "Liquid doesn't seem to be loaded... #{$!}" +#end diff --git a/lib/helpers/liquid_theme_tags.rb b/lib/helpers/liquid_theme_tags.rb new file mode 100755 index 0000000..51a30b0 --- /dev/null +++ b/lib/helpers/liquid_theme_tags.rb @@ -0,0 +1,31 @@ +# A Liquid Tag for retrieving path information for theme specific media +# +# Returns the path based on the file extension +# +class Themeitem < Liquid::Block + + @@image_exts = %w( .png .jpg .jpeg .jpe .gif ) + @@stylesheet_exts = %w( .css ) + @@javascript_exts = %w( .js .htc ) + + def render(context) + # Which, if either, of these are correct? + base_url = context['request'].relative_url_root || ActionController::Base.asset_host.to_s + theme_name = @theme_name || context['active_theme'] + + filename = @nodelist.join('').strip + ext = File.extname( filename ) + + if @@image_exts.include?( ext ) + "#{base_url}/themes/#{theme_name}/images/#{filename}" + + elsif @@stylesheet_exts.include?( ext ) + "#{base_url}/themes/#{theme_name}/stylesheets/#{filename}" + + elsif @@javascript_exts.include?( ext ) + "#{base_url}/themes/#{theme_name}/javascript/#{filename}" + end + end +end + +Liquid::Template.register_tag( 'themeitem', Themeitem ) \ No newline at end of file diff --git a/lib/helpers/rhtml_theme_tags.rb b/lib/helpers/rhtml_theme_tags.rb new file mode 100755 index 0000000..bbd16f2 --- /dev/null +++ b/lib/helpers/rhtml_theme_tags.rb @@ -0,0 +1,66 @@ +# +# These are theme helper tags +# +module ActionView::Helpers::AssetTagHelper + + # returns the public path to a theme stylesheet + def theme_stylesheet_path( source=nil, theme=nil ) + theme = theme || controller.current_theme + compute_public_path(source || "theme", "themes/#{theme}/stylesheets", 'css') + end + + # returns the path to a theme image + def theme_image_path( source, theme=nil ) + theme = theme || controller.current_theme + compute_public_path(source, "themes/#{theme}/images", 'png') + end + + # returns the path to a theme javascript + def theme_javascript_path( source, theme=nil ) + theme = theme || controller.current_theme + compute_public_path(source, "themes/#{theme}/javascript", 'js') + end + + + # This tag it will automatially include theme specific css files + def theme_stylesheet_link_tag(*sources) + sources.uniq! + options = sources.last.is_a?(Hash) ? sources.pop.stringify_keys : { } + sources.collect { |source| + source = theme_stylesheet_path(source) + tag("link", { "rel" => "Stylesheet", "type" => "text/css", "media" => "screen", "href" => source }.merge(options)) + }.join("\n") + end + + # This tag will return a theme-specific IMG + def theme_image_tag(source, options = {}) + options.symbolize_keys + + options[:src] = theme_image_path(source) + options[:alt] ||= File.basename(options[:src], '.*').split('.').first.capitalize + + if options[:size] + options[:width], options[:height] = options[:size].split("x") + options.delete :size + end + + tag("img", options) + end + + # This tag can be used to return theme-specific javscripts + def theme_javascript_include_tag(*sources) + options = sources.last.is_a?(Hash) ? sources.pop.stringify_keys : { } + if sources.include?(:defaults) + sources = sources[0..(sources.index(:defaults))] + + @@javascript_default_sources.dup + + sources[(sources.index(:defaults) + 1)..sources.length] + sources.delete(:defaults) + sources << "application" if defined?(RAILS_ROOT) && File.exists?("#{RAILS_ROOT}/public/javascripts/application.js") + end + sources.collect { |source| + source = theme_javascript_path(source) + content_tag("script", "", { "type" => "text/javascript", "src" => source }.merge(options)) + }.join("\n") + end + +end \ No newline at end of file diff --git a/lib/patches/actioncontroller_ex.rb b/lib/patches/actioncontroller_ex.rb new file mode 100755 index 0000000..6cc9bc6 --- /dev/null +++ b/lib/patches/actioncontroller_ex.rb @@ -0,0 +1,59 @@ +# Extend the Base ActionController to support themes +ActionController::Base.class_eval do + + attr_accessor :current_theme + attr_accessor :force_liquid_template + + # Use this in your controller just like the layout macro. + # Example: + # + # theme 'theme_name' + # + # -or- + # + # theme :get_theme + # + # def get_theme + # 'theme_name' + # end + def self.theme(theme_name, conditions = {}) + # TODO: Allow conditions... (?) + write_inheritable_attribute "theme", theme_name + end + + # Set force_liquid to true in your controlelr to only allow + # Liquid template in themes. + # Example: + # + # force_liquid true + def self.force_liquid(force_liquid_value, conditions = {}) + # TODO: Allow conditions... (?) + write_inheritable_attribute "force_liquid", force_liquid_value + end + + # Retrieves the current set theme + def current_theme(passed_theme=nil) + theme = passed_theme || self.class.read_inheritable_attribute("theme") + + @active_theme = case theme + when Symbol then send(theme) + when Proc then theme.call(self) + when String then theme + end + end + + # Retrieves the force liquid flag + def force_liquid_template(passed_value=nil) + force_liquid = passed_value || self.class.read_inheritable_attribute("force_liquid") + + force_liquid_template = case force_liquid + when Symbol then send(force_liquid) + when Proc then force_liquid.call(self) + when String then force_liquid == 'true' + when TrueClass then force_liquid + when FalseClass then force_liquid + when Fixnum then force_liquid == 1 + end + end + +end \ No newline at end of file diff --git a/lib/patches/actionmailer_ex.rb b/lib/patches/actionmailer_ex.rb new file mode 100755 index 0000000..c42db66 --- /dev/null +++ b/lib/patches/actionmailer_ex.rb @@ -0,0 +1,27 @@ +# Extend the Base ActionController to support themes +ActionMailer::Base.class_eval do + + alias_method :__render, :render + alias_method :__initialize, :initialize + + @current_theme = nil + + attr_reader :current_theme + + def initialize(method_name=nil, *parameters) + if parameters[-1].is_a? Hash and (parameters[-1].include? :theme) + @current_theme = parameters[-1][:theme] + parameters[-1].delete :theme + parameters[-1][:current_theme] = @current_theme + end + create!(method_name, *parameters) if method_name + end + + def render(opts) + body = opts.delete(:body) + body[:current_theme] = @current_theme + opts[:file] = "#{mailer_name}/#{opts[:file]}" + initialize_template_class(body).render(opts) + end + +end \ No newline at end of file diff --git a/lib/patches/actionview_ex.rb b/lib/patches/actionview_ex.rb new file mode 100755 index 0000000..96c6992 --- /dev/null +++ b/lib/patches/actionview_ex.rb @@ -0,0 +1,45 @@ +# Extending ActionView::Base to support rendering themes +# +module ActionView + + # Extending ActionView::Base to support rendering themes + class Base + alias_method :theme_support_old_render_file, :render_file + # Overrides the default Base#render_file to allow theme-specific views + def render_file(template_path, use_full_path = false, local_assigns = {}) + + search_path = [ + "#{RAILS_ROOT}/themes/#{controller.current_theme}/views", # for components + "#{RAILS_ROOT}/themes/#{controller.current_theme}", # for layouts + ] + + @finder.prepend_view_path(search_path) + local_assigns['active_theme'] = get_current_theme(local_assigns) + theme_support_old_render_file(template_path, use_full_path, local_assigns) + + end + private + + def force_liquid? + unless controller.nil? + if controller.respond_to?('force_liquid_template') + controller.force_liquid_template + end + else + false + end + end + + def get_current_theme(local_assigns) + unless controller.nil? + if controller.respond_to?('current_theme') + return controller.current_theme || false + end + end + # Used with ActionMailers + if local_assigns.include? :current_theme + return local_assigns.delete :current_theme + end + end + end +end diff --git a/lib/patches/routeset_ex.rb b/lib/patches/routeset_ex.rb new file mode 100755 index 0000000..9617f2d --- /dev/null +++ b/lib/patches/routeset_ex.rb @@ -0,0 +1,26 @@ +# Extends ActionController::Routing::RouteSet to automatically add the theme routes +class ActionController::Routing::RouteSet + + alias_method :__draw, :draw + + # Overrides the default RouteSet#draw to automatically + # include the routes needed by the ThemeController + def draw + clear! + map = Mapper.new(self) + + create_theme_routes(map) + yield map + + named_routes.install + end + + # Creates the required routes for the ThemeController... + def create_theme_routes(map) + map.theme_images "/themes/:theme/images/*filename", :controller=>'theme', :action=>'images' + map.theme_stylesheets "/themes/:theme/stylesheets/*filename", :controller=>'theme', :action=>'stylesheets' + map.theme_javascript "/themes/:theme/javascript/*filename", :controller=>'theme', :action=>'javascript' + map.connect "/themes/*whatever", :controller=>'theme', :action=>'error' + end + +end \ No newline at end of file diff --git a/lib/theme.rb b/lib/theme.rb new file mode 100755 index 0000000..4562784 --- /dev/null +++ b/lib/theme.rb @@ -0,0 +1,58 @@ +class Theme + cattr_accessor :cache_theme_lookup + @@cache_theme_lookup = false + + attr_accessor :name, :title, :description, :preview_image + + def initialize(name) + @name = name + @title = name.underscore.humanize.titleize + @description_html = nil + end + + def description + if @description_html.nil? + @description_html = RedCloth.new(File.read( File.join(Theme.path_to_theme(name), "about.markdown") )).to_html(:markdown, :textile) rescue "#{title}" + end + @description_html + end + + def has_preview? + File.exists?( File.join( Theme.path_to_theme(name), 'images', 'preview.png' ) ) rescue false + end + + def preview_image + 'preview.png' + end + + def self.find_all + installed_themes.inject([]) do |array, path| + array << theme_from_path(path) + end + end + +private + + def self.themes_root + File.join(RAILS_ROOT, "themes") + end + + def self.path_to_theme(theme) + File.join(themes_root, theme) + end + + def self.theme_from_path(path) + name = path.scan(/[-\w]+$/i).flatten.first + self.new(name) + end + + def self.installed_themes + cache_theme_lookup ? @theme_cache ||= search_theme_directory : search_theme_directory + end + + def self.search_theme_directory + Dir.glob("#{themes_root}/[-_a-zA-Z0-9]*").collect do |file| + file if File.directory?(file) + end.compact + end +end diff --git a/lib/theme_controller.rb b/lib/theme_controller.rb new file mode 100755 index 0000000..76d7ba2 --- /dev/null +++ b/lib/theme_controller.rb @@ -0,0 +1,61 @@ +# The controller for serving/cacheing theme content... +# +class ThemeController < ActionController::Base + + after_filter :cache_theme_files + + def stylesheets + render_theme_item(:stylesheets, params[:filename].to_s, params[:theme], 'text/css') + end + + def javascript + render_theme_item(:javascript, params[:filename].to_s, params[:theme], 'text/javascript') + end + + def images + render_theme_item(:images, params[:filename].to_s, params[:theme]) + end + + def error + render :nothing => true, :status => 404 + end + + private + + def render_theme_item(type, file, theme, mime = mime_for(file)) + render :text => "Not Found", :status => 404 and return if file.split(%r{[\\/]}).include?("..") + send_file "#{Theme.path_to_theme(theme)}/#{type}/#{file}", :type => mime, :disposition => 'inline', :stream => false + end + + def cache_theme_files + path = request.request_uri + begin + ThemeController.cache_page( response.body, path ) + rescue + STERR.puts "Cache Exception: #{$!}" + end + end + + + def mime_for(filename) + case filename.downcase + when /\.js$/ + 'text/javascript' + when /\.css$/ + 'text/css' + when /\.gif$/ + 'image/gif' + when /(\.jpg|\.jpeg)$/ + 'image/jpeg' + when /\.png$/ + 'image/png' + when /\.swf$/ + 'application/x-shockwave-flash' + else + 'application/binary' + end + end + +end + + diff --git a/lib/theme_error.rb b/lib/theme_error.rb new file mode 100755 index 0000000..85e4e3e --- /dev/null +++ b/lib/theme_error.rb @@ -0,0 +1,4 @@ +# ThemeError is thrown when force_liquid is true, and +# a .liquid template isn't found. +class ThemeError < StandardError +end diff --git a/lib/theme_helper.rb b/lib/theme_helper.rb new file mode 100755 index 0000000..61e429e --- /dev/null +++ b/lib/theme_helper.rb @@ -0,0 +1,3 @@ +# this is here so that Rails doesn't complain about a missing default helper... +module ThemeHelper +end \ No newline at end of file diff --git a/tasks/themes.rake b/tasks/themes.rake new file mode 100755 index 0000000..260dd68 --- /dev/null +++ b/tasks/themes.rake @@ -0,0 +1,23 @@ + +desc "Creates the cached (public) theme folders" +task :theme_create_cache do + for theme in Dir.glob("#{RAILS_ROOT}/themes/*") + theme_name = theme.split( File::Separator )[-1] + puts "Creating #{RAILS_ROOT}/public/themes/#{theme_name}" + + FileUtils.mkdir_p "#{RAILS_ROOT}/public/themes/#{theme_name}" + + FileUtils.cp_r "#{theme}/images", "#{RAILS_ROOT}/public/themes/#{theme_name}/images", :verbose => true + FileUtils.cp_r "#{theme}/stylesheets", "#{RAILS_ROOT}/public/themes/#{theme_name}/stylesheets", :verbose => true + FileUtils.cp_r "#{theme}/javascript", "#{RAILS_ROOT}/public/themes/#{theme_name}/javascript", :verbose => true + end +end + +desc "Removes the cached (public) theme folders" +task :theme_remove_cache do + puts "Removing #{RAILS_ROOT}/public/themes" + FileUtils.rm_r "#{RAILS_ROOT}/public/themes", :force => true +end + +desc "Updates the cached (public) theme folders" +task :theme_update_cache => [:theme_remove_cache, :theme_create_cache] \ No newline at end of file