limitedrun-themekit 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +41 -0
  7. data/Rakefile +11 -0
  8. data/bin/limitedrun-themekit +7 -0
  9. data/lib/limitedrun-themekit.rb +10 -0
  10. data/lib/limitedrun-themekit/config.rb +23 -0
  11. data/lib/limitedrun-themekit/renderer.rb +88 -0
  12. data/lib/limitedrun-themekit/server.rb +70 -0
  13. data/lib/limitedrun-themekit/version.rb +5 -0
  14. data/lib/liquid/filters.rb +57 -0
  15. data/lib/liquid/tags/captcha.rb +20 -0
  16. data/lib/liquid/tags/contact_form.rb +16 -0
  17. data/lib/liquid/tags/lr_include.rb +143 -0
  18. data/lib/liquid/tags/paginate.rb +16 -0
  19. data/limitedrun-themekit.gemspec +29 -0
  20. data/spec/assets/skeleton-theme/README.md +11 -0
  21. data/spec/assets/skeleton-theme/configs/default.json +314 -0
  22. data/spec/assets/skeleton-theme/javascripts/default.js +0 -0
  23. data/spec/assets/skeleton-theme/layouts/default.html +150 -0
  24. data/spec/assets/skeleton-theme/store.json +101 -0
  25. data/spec/assets/skeleton-theme/stylesheets/default.css +888 -0
  26. data/spec/assets/skeleton-theme/templates/404.html +3 -0
  27. data/spec/assets/skeleton-theme/templates/category.html +37 -0
  28. data/spec/assets/skeleton-theme/templates/contact.html +62 -0
  29. data/spec/assets/skeleton-theme/templates/event.html +121 -0
  30. data/spec/assets/skeleton-theme/templates/events.html +43 -0
  31. data/spec/assets/skeleton-theme/templates/gallery.html +13 -0
  32. data/spec/assets/skeleton-theme/templates/history.html +58 -0
  33. data/spec/assets/skeleton-theme/templates/index.html +37 -0
  34. data/spec/assets/skeleton-theme/templates/maintenance.html +31 -0
  35. data/spec/assets/skeleton-theme/templates/news-item.html +27 -0
  36. data/spec/assets/skeleton-theme/templates/news.html +37 -0
  37. data/spec/assets/skeleton-theme/templates/order.html +119 -0
  38. data/spec/assets/skeleton-theme/templates/product.html +105 -0
  39. data/spec/assets/skeleton-theme/templates/roster-item.html +136 -0
  40. data/spec/assets/skeleton-theme/templates/roster.html +41 -0
  41. data/spec/assets/skeleton-theme/templates/search.html +33 -0
  42. data/spec/features/themekit_spec.rb +22 -0
  43. data/spec/spec_helper.rb +25 -0
  44. metadata +211 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a51b3b1624ba7e219bd81a6cd9f8b30afc84e06a
4
+ data.tar.gz: 6a866143943bb7a2c8fad259df260b74d464fc0a
5
+ SHA512:
6
+ metadata.gz: 0d904f53e355e680c9bc833d8561d1e5c6f056514ba743e95db579ac64d0ed66a059ee6747596b5c85c56f707fab5667a551886fceb0dcd485cf47a46049dc87
7
+ data.tar.gz: 7772fd381af595a8079aa62d6cd9dc2cc07fee02b80ea43d7534337f60074d360ae898e3fc45744adbfeaa3c88ba74e05f8634fc6e4056eb3089c7aafad9ebba
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .DS_Store
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in limitedrun-themekit.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Howard Wilson
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,41 @@
1
+ # limitedrun-themekit
2
+
3
+ [![Build Status](http://img.shields.io/travis/watsonbox/limitedrun-themekit.svg?style=flat)](https://travis-ci.org/watsonbox/limitedrun-themekit)
4
+
5
+ Toolkit for developing [Limited Run](http://www.limitedrun.com/) themes locally. Because Limited Run themes are made up of [liquid](http://liquidmarkup.org/) templates, they can't easily be worked on offline. This gem renders the templates locally using mock data to speed up development.
6
+
7
+ Extracted from my [telescope-pinna](https://github.com/watsonbox/telescope-pinna) theme for [Pinna Records](http://www.pinnarecords.com/).
8
+
9
+ At the moment this is a work in progress but should be fairly extensible. Please open a pull request if you add functionality.
10
+
11
+
12
+ ## Installation
13
+
14
+ $ gem install limitedrun-themekit
15
+
16
+
17
+ ## Usage
18
+
19
+ Get hold of a responsive theme from Limited Run (export from the admin interface), and add a `store.json` to its root. See [store.json](https://github.com/watsonbox/limitedrun-themekit/blob/master/spec/assets/skeleton-theme/store.json) for an example as used in the specs.
20
+
21
+ Alternatively, clone [one of my themes](https://github.com/watsonbox/telescope-pinna) as a test.
22
+
23
+ $ limitedrun-themekit
24
+
25
+ This command fires up Sinatra on http://localhost:4567/ with a preview of the site.
26
+
27
+
28
+ ## Contributing
29
+
30
+ 1. Fork it
31
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
32
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
33
+ 4. Push to the branch (`git push origin my-new-feature`)
34
+ 5. Create new Pull Request
35
+
36
+
37
+ # Todo
38
+
39
+ * Implement pagination
40
+ * Add more specs
41
+ * Static site generation
@@ -0,0 +1,11 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ begin
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task :default => :spec
9
+ rescue LoadError
10
+ # no rspec available
11
+ end
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'limitedrun-themekit'
4
+
5
+ Limitedrun::Themekit::Config.theme_path = ARGV[0] if ARGV[0]
6
+
7
+ Limitedrun::Themekit::Server.run!
@@ -0,0 +1,10 @@
1
+ require "limitedrun-themekit/version"
2
+ require "limitedrun-themekit/config"
3
+ require "limitedrun-themekit/renderer"
4
+ require "limitedrun-themekit/server"
5
+
6
+ module Limitedrun
7
+ module Themekit
8
+ # Your code goes here...
9
+ end
10
+ end
@@ -0,0 +1,23 @@
1
+ module Limitedrun
2
+ module Themekit
3
+ class Config
4
+ class << self
5
+ attr_writer :theme_path
6
+
7
+ def theme_path
8
+ @theme_path || '.'
9
+ end
10
+
11
+ DIRS = [:configs_dir, :layouts_dir, :templates_dir, :stylesheets_dir, :javascripts_dir, :snippets_dir]
12
+ attr_accessor *DIRS
13
+
14
+ # Define accessors for directories along with defaults e.g. #configs_dir -> 'configs'
15
+ DIRS.each do |dir|
16
+ define_method dir do
17
+ instance_variable_get("@#{dir}") || dir.to_s.gsub('_dir', '')
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,88 @@
1
+ require 'active_support/core_ext/hash/keys'
2
+ require 'liquid'
3
+ require 'liquid/tags/paginate'
4
+ require 'liquid/tags/contact_form'
5
+ require 'liquid/tags/captcha'
6
+ require 'liquid/tags/lr_include'
7
+ require 'liquid/filters'
8
+ require 'hashie'
9
+ require 'json'
10
+
11
+ module Limitedrun
12
+ module Themekit
13
+ class Renderer < Struct.new(:theme_path, :request_path)
14
+ def render(template, template_assigns = nil)
15
+ assigns = global_assigns
16
+
17
+ if template_assigns
18
+ template_assigns.each_key do |k|
19
+ unless template_assigns[k].is_a?(Hashie::Mash)
20
+ template_assigns[k] = Hashie::Mash.new(instance_eval &template_assigns[k])
21
+ end
22
+ end
23
+
24
+ assigns.merge!(template_assigns.stringify_keys)
25
+ end
26
+
27
+ layout = parse_template File.join(Limitedrun::Themekit::Config.layouts_dir, 'default.html')
28
+ template = parse_template File.join(Limitedrun::Themekit::Config.templates_dir, template)
29
+
30
+ layout.render(assigns.merge('content' => template.render(assigns)))
31
+ end
32
+
33
+ def render_string(string)
34
+ Liquid::Template.parse(string).render('config' => theme_config)
35
+ end
36
+
37
+ protected
38
+
39
+ # Assignments for all liquid templates
40
+ def global_assigns
41
+ { 'config' => theme_config, 'store' => store }
42
+ end
43
+
44
+ # Theme configuration extracted from config/defaults.js and store.config overrides
45
+ def theme_config
46
+ return @theme_config if @theme_config
47
+
48
+ settings = JSON.parse(
49
+ File.read(
50
+ File.join(
51
+ Limitedrun::Themekit::Config.theme_path,
52
+ Limitedrun::Themekit::Config.configs_dir,
53
+ 'default.json'
54
+ )
55
+ )
56
+ )['settings']
57
+
58
+ config = {}
59
+
60
+ settings.each do |k, v|
61
+ config[k] = v['default'] if v['default']
62
+ end
63
+
64
+ if store && store.config
65
+ store.config.each { |k, v| config[k] = v }
66
+ end
67
+
68
+ @config = config
69
+ end
70
+
71
+ # Store mock data model loaded from store.json
72
+ def store
73
+ @store ||= Hashie::Mash.new(JSON.parse(File.read(File.join(Limitedrun::Themekit::Config.theme_path, 'store.json'))))
74
+ end
75
+
76
+ def parse_template(template)
77
+ Liquid::Template.parse(
78
+ File.read(
79
+ File.join(
80
+ Limitedrun::Themekit::Config.theme_path,
81
+ template
82
+ )
83
+ )
84
+ )
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,70 @@
1
+ require 'sinatra'
2
+
3
+ module Limitedrun
4
+ module Themekit
5
+ class Server < Sinatra::Application
6
+ get '/' do
7
+ render 'index.html'
8
+ end
9
+
10
+ get '/news' do
11
+ render 'news.html', news: Proc.new { store.news }
12
+ end
13
+
14
+ get '/news/posts/:item' do
15
+ render 'news-item.html', item: Proc.new { store.news.items.first }
16
+ end
17
+
18
+ get '/store' do
19
+ # Put all products in all categories
20
+ render 'category.html', category: Proc.new { store.categories.first.merge('products' => store.products) }
21
+ end
22
+
23
+ get '/categories/:category' do
24
+ # Put all products in all categories
25
+ render 'category.html', category: Proc.new { store.categories.find { |c| c.url == request_path }.merge('products' => store.products) }
26
+ end
27
+
28
+ get '/products/:slug' do
29
+ render 'product.html', product: Proc.new { store.products.find { |p| p.url == request_path } }
30
+ end
31
+
32
+ get '/artists' do
33
+ render 'roster.html', roster: Proc.new { store.roster }
34
+ end
35
+
36
+ get '/artists/:item' do
37
+ # Associate all products with all roster items
38
+ render 'roster-item.html', item: Proc.new {
39
+ store.roster.items.find { |i| i.url == request_path }.merge('products' => store.products)
40
+ }
41
+ end
42
+
43
+ get '/contact' do
44
+ render 'contact.html'
45
+ end
46
+
47
+ get '/stylesheets/:file' do
48
+ content_type :css
49
+
50
+ css = File.read(File.join(Config.theme_path, Config.stylesheets_dir, params[:file]))
51
+ Renderer.new(Config.theme_path).render_string(css)
52
+ end
53
+
54
+ get '/javascripts/:file' do
55
+ send_file File.join(Config.theme_path, Config.javascripts_dir, params[:file])
56
+ end
57
+
58
+ private
59
+
60
+ def render(template, assigns = nil)
61
+ # For the include tag
62
+ Liquid::Template.file_system = Liquid::LocalFileSystem.new(
63
+ File.join(Limitedrun::Themekit::Config.theme_path, Limitedrun::Themekit::Config.snippets_dir)
64
+ )
65
+
66
+ Renderer.new(Config.theme_path, request.path_info).render(template, assigns)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,5 @@
1
+ module Limitedrun
2
+ module Themekit
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,57 @@
1
+ module Liquid
2
+ module Filters
3
+ # Extracted from locomotivecms/wagon
4
+ # Write the url of a theme stylesheet
5
+ # input: name of the css file
6
+ def stylesheet_url(input)
7
+ return '' if input.nil?
8
+
9
+ if input =~ /^https?:/
10
+ input
11
+ else
12
+ input = "/stylesheets/#{input}" unless input =~ /^\//
13
+ input = "#{input}.css" unless input[-4..-1] == '.css'
14
+ input
15
+ end
16
+ end
17
+
18
+ # Extracted from locomotivecms/wagon
19
+ # Write the link to a stylesheet resource
20
+ # input: url of the css file
21
+ def stylesheet_tag(input, media = 'screen')
22
+ return '' if input.nil?
23
+
24
+ input = stylesheet_url(input)
25
+
26
+ %{<link href="#{input}" media="#{media}" rel="stylesheet" type="text/css" />}
27
+ end
28
+
29
+ def script_tag(input)
30
+ return '' if input.nil?
31
+
32
+ %{<script src="/#{Limitedrun::Themekit::Config.javascripts_dir + "/" + input}" type="text/javascript"></script>}
33
+ end
34
+
35
+ def img_tag(input, klass)
36
+ %{<img src="#{input}" class="#{klass}" />}
37
+ end
38
+
39
+ def link_to_news_item(input)
40
+ %{<a href="/news/posts/first">#{input['title']}</a>}
41
+ end
42
+
43
+ def link_to_page(input)
44
+ %{<a href="#{input['path']}">#{input['title']}</a>}
45
+ end
46
+
47
+ def link_to_category(input)
48
+ %{<a href="#{input[:url]}">#{input[:name]}</a>}
49
+ end
50
+
51
+ def link_to_roster_item(input)
52
+ %{<a href="#{input[:url]}">#{input[:name]}</a>}
53
+ end
54
+ end
55
+ end
56
+
57
+ ::Liquid::Template.register_filter(Liquid::Filters)
@@ -0,0 +1,20 @@
1
+ module Liquid
2
+ module Tags
3
+ class Captcha < Liquid::Tag
4
+ def initialize(tag_name, args, tokens)
5
+ super
6
+ end
7
+
8
+ def render(context)
9
+ %{
10
+ <script type="text/javascript">
11
+ var RecaptchaOptions = {"theme":"clean"};
12
+ </script><script type="text/javascript" src="http://www.google.com/recaptcha/api/challenge?k=6LdWsM8SAAAAAH-DqFDbCl656cx5203ZoGpZxGLs">
13
+ </script>
14
+ }
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ Liquid::Template.register_tag('captcha', Liquid::Tags::Captcha)
@@ -0,0 +1,16 @@
1
+ module Liquid
2
+ module Tags
3
+ class ContactForm < Liquid::Block
4
+ def initialize(tag_name, markup, tokens)
5
+ super
6
+ end
7
+
8
+ def render(context)
9
+ %{<form>#{super}</form>}
10
+ super
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ Liquid::Template.register_tag('contact_form', Liquid::Tags::ContactForm)
@@ -0,0 +1,143 @@
1
+ # encoding: UTF-8
2
+ # Extracted with a few mods from https://github.com/jekyll/jekyll/blob/master/lib/jekyll/tags/include.rb
3
+ # Liquid's default include tag does not support special chars or extensions in filenames
4
+
5
+ module Liquid
6
+ module Tags
7
+ class IncludeTagError < StandardError
8
+ attr_accessor :path
9
+
10
+ def initialize(msg, path)
11
+ super(msg)
12
+ @path = path
13
+ end
14
+ end
15
+
16
+ class Include < Liquid::Tag
17
+
18
+ SYNTAX_EXAMPLE = "{% include 'file.ext' param='value' param2='value' %}"
19
+
20
+ VALID_SYNTAX = /([\w-]+)\s*=\s*(?:"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'|([\w\.-]+))/
21
+ VARIABLE_SYNTAX = /(?<variable>[^{]*\{\{\s*(?<name>[\w\-\.]+)\s*(\|.*)?\}\}[^\s}]*)(?<params>.*)/
22
+
23
+ def includes_dir
24
+ File.join(File.realpath(Limitedrun::Themekit::Config.theme_path), Limitedrun::Themekit::Config.snippets_dir)
25
+ end
26
+
27
+ def initialize(tag_name, markup, tokens)
28
+ super
29
+ matched = markup.strip.match(VARIABLE_SYNTAX)
30
+ if matched
31
+ @file = matched['variable'].strip
32
+ @params = matched['params'].strip
33
+ else
34
+ @file, @params = markup.strip.split(' ', 2);
35
+ end
36
+ validate_params if @params
37
+ end
38
+
39
+ def parse_params(context)
40
+ params = {}
41
+ markup = @params
42
+
43
+ while match = VALID_SYNTAX.match(markup) do
44
+ markup = markup[match.end(0)..-1]
45
+
46
+ value = if match[2]
47
+ match[2].gsub(/\\"/, '"')
48
+ elsif match[3]
49
+ match[3].gsub(/\\'/, "'")
50
+ elsif match[4]
51
+ context[match[4]]
52
+ end
53
+
54
+ params[match[1]] = value
55
+ end
56
+ params
57
+ end
58
+
59
+ def validate_file_name(file)
60
+ if file !~ /^'[a-zA-Z0-9_\/\.-]+'$/ || file =~ /\.\// || file =~ /\/\./
61
+ raise ArgumentError.new <<-eos
62
+ Invalid syntax for include tag. File contains invalid characters or sequences:
63
+
64
+ #{file}
65
+
66
+ Valid syntax:
67
+
68
+ #{SYNTAX_EXAMPLE}
69
+
70
+ eos
71
+ end
72
+ end
73
+
74
+ def validate_params
75
+ full_valid_syntax = Regexp.compile('\A\s*(?:' + VALID_SYNTAX.to_s + '(?=\s|\z)\s*)*\z')
76
+ unless @params =~ full_valid_syntax
77
+ raise ArgumentError.new <<-eos
78
+ Invalid syntax for include tag:
79
+
80
+ #{@params}
81
+
82
+ Valid syntax:
83
+
84
+ #{SYNTAX_EXAMPLE}
85
+
86
+ eos
87
+ end
88
+ end
89
+
90
+ # Render the variable if required
91
+ def render_variable(context)
92
+ if @file.match(VARIABLE_SYNTAX)
93
+ partial = Liquid::Template.parse(@file)
94
+ partial.render!(context)
95
+ end
96
+ end
97
+
98
+ def render(context)
99
+ dir = includes_dir
100
+
101
+ file = render_variable(context) || @file
102
+ validate_file_name(file)
103
+
104
+ path = File.join(dir, file[1...-1])
105
+ validate_path(path, dir, true)
106
+
107
+ begin
108
+ partial = Liquid::Template.parse(source(path, context))
109
+
110
+ context.stack do
111
+ context['include'] = parse_params(context) if @params
112
+ partial.render!(context)
113
+ end
114
+ rescue => e
115
+ raise IncludeTagError.new e.message, File.join(INCLUDES_DIR, @file)
116
+ end
117
+ end
118
+
119
+ def validate_path(path, dir, safe)
120
+ if safe && !realpath_prefixed_with?(path, dir)
121
+ raise IOError.new "The included file '#{path}' should exist and should not be a symlink"
122
+ elsif !File.exist?(path)
123
+ raise IOError.new "Included file '#{path_relative_to_source(dir, path)}' not found"
124
+ end
125
+ end
126
+
127
+ def path_relative_to_source(dir, path)
128
+ File.join(INCLUDES_DIR, path.sub(Regexp.new("^#{dir}"), ""))
129
+ end
130
+
131
+ def realpath_prefixed_with?(path, dir)
132
+ File.exist?(path) && File.realpath(path).start_with?(dir)
133
+ end
134
+
135
+ # This method allows to modify the file content by inheriting from the class.
136
+ def source(file, context)
137
+ File.read(file)
138
+ end
139
+ end
140
+ end
141
+ end
142
+
143
+ Liquid::Template.register_tag('include', Liquid::Tags::Include)