skim 0.8.0

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.
data/.gitignore ADDED
@@ -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
+ bin
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "https://rubygems.org"
2
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 John Firebaugh
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.
data/README.md ADDED
@@ -0,0 +1,125 @@
1
+ Skim
2
+ ====
3
+
4
+ Take the fat out of your client-side templates with Skim. Skim is the [Slim](http://slim-lang.com/) templating engine
5
+ with embedded CoffeeScript. It compiles to JavaScript templates (`.jst`), which can then be served by Rails or any other
6
+ Sprockets-based asset pipeline.
7
+
8
+ # Usage
9
+
10
+ `gem install skim`, or add `skim` to your `Gemfile` in the `:assets` group:
11
+
12
+ group :assets do
13
+ gem 'skim'
14
+ end
15
+
16
+ Create template files with the extension `.jst.skim`. For example, `test.jst.skim`:
17
+
18
+ p Hello #{@world}!
19
+
20
+ In your JavaScript or CoffeeScript, render the result, passing a context object:
21
+
22
+ $("body").html(JST["test"]({world: "World"}));
23
+
24
+ Order up a skinny latte and enjoy!
25
+
26
+ # Caveats
27
+
28
+ Skim is an early proof-of-concept. Some Slim features are still missing:
29
+
30
+ * Skim does not currently support embedded engines. Being a client-side templating languages, it will only be able to
31
+ support embedded engines with a client-side implementation.
32
+ * Skim does not currently support HTML pretty-printing (Slim's `:pretty` option). This is low priority, as
33
+ pretty-printing is even less important client-side than server-side.
34
+ * Skim does not currently support backslash line continuations.
35
+
36
+ # Language reference
37
+
38
+ Skim supports the following Slim language features:
39
+
40
+ * doctype declarations (`doctype`)
41
+ * HTML Comments (`/!`) and conditional comments (`/[...]`)
42
+ * static content (same line and nested)
43
+ * dynamic content, escaped and not (`=` and `==`)
44
+ * control logic (`-`)
45
+ * string interpolation, escaped and not (`#{}` and `#{{}}`)
46
+ * id and class attribute shortcuts (`#` and `.`)
47
+ * attribute and attribute value wrappers
48
+ * logic-less (sections) mode
49
+
50
+ Several Coffee/JavaScript considerations are specific to Skim:
51
+
52
+ * When interpolating the results of evaluating code, Skim will replace `null` and `undefined` results with an empty
53
+ string.
54
+ * You will typically want to use the fat arrow `=>` function definition to create callbacks, to preserve the binding of
55
+ `this` analogously to how `self` behaves in a Ruby block.
56
+
57
+ ## The context object
58
+
59
+ The context object you pass to the compiled template function becomes the value of this inside your template. You can
60
+ use CoffeeScript's `@` sigil to easily access properties and call helper methods on the context object.
61
+
62
+ ## Escaping and unescaping
63
+
64
+ Like Slim, Skim escapes dynamic output by default, and it supports the same `==` and `#{{}}` syntaxes for bypassing
65
+ escaping. In addition, the special `safe` method on the context object tells Skim that the string can be output without
66
+ being escaped. You can use this in conjunction with `escape` context method to selectively sanitize parts of the string.
67
+ For example, given the template:
68
+
69
+ = @linkTo(@project)
70
+
71
+ you could render it with the following context:
72
+
73
+ JST["my_template"]
74
+ project: { id: 4, name: "Crate & Barrel" }
75
+ linkTo: (project) ->
76
+ url = "/projects/#{project.id}"
77
+ name = @escape project.name
78
+ @safe "<a href='#{url}'>#{name}</a>"
79
+
80
+ to produce:
81
+
82
+ <a href='/projects/4'>Crate &amp; Barrel</a>
83
+
84
+ ## The Skim asset
85
+
86
+ By default, all you need to do to start using Skim is add it to your Gemfile. Skim will embed a small amount of
87
+ supporting code in each generated template asset. You can remove this duplication by manually including Skim's asset,
88
+ and setting Skim's `:use_asset` option to true.
89
+
90
+ In Rails, this can be done by adding the following to `application.js`:
91
+
92
+ //= require skim
93
+
94
+ And the following in an initializer:
95
+
96
+ Skim::Engine.default_options[:use_asset] = true
97
+
98
+ # License (MIT)
99
+
100
+ Copyright (c) 2012 John Firebaugh
101
+
102
+ Permission is hereby granted, free of charge, to any person obtaining
103
+ a copy of this software and associated documentation files (the
104
+ "Software"), to deal in the Software without restriction, including
105
+ without limitation the rights to use, copy, modify, merge, publish,
106
+ distribute, sublicense, and/or sell copies of the Software, and to
107
+ permit persons to whom the Software is furnished to do so, subject to
108
+ the following conditions:
109
+
110
+ The above copyright notice and this permission notice shall be
111
+ included in all copies or substantial portions of the Software.
112
+
113
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
114
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
115
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
116
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
117
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
118
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
119
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
120
+
121
+ # Special Thanks
122
+
123
+ * Andrew Stone, for Slim
124
+ * Magnus Holm, for Temple
125
+ * Daniel Mendler, for maintenance of both
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new('test') do |t|
4
+ t.libs << 'lib' << 'test'
5
+ t.test_files = FileList['test/test_*.rb']
6
+ t.verbose = true
7
+ end
8
+
9
+ task :default => 'test'
@@ -0,0 +1,36 @@
1
+ module Skim
2
+ class Compiler < Slim::Compiler
3
+ def on_slim_control(code, content)
4
+ [:multi,
5
+ [:code, code],
6
+ [:indent, compile(content)]]
7
+ end
8
+
9
+ def on_slim_attr(name, escape, code)
10
+ value = case code
11
+ when 'true'
12
+ escape = false
13
+ [:static, name]
14
+ when 'false', 'null'
15
+ escape = false
16
+ [:multi]
17
+ else
18
+ tmp = unique_name
19
+ [:multi,
20
+ [:code, "#{tmp} = #{code}"],
21
+ [:case, tmp,
22
+ ['true', [:static, name]],
23
+ ['false, null', [:multi]],
24
+ [:else,
25
+ [:dynamic,
26
+ #if delimiter = options[:attr_delimiter][name]
27
+ # "#{tmp}.respond_to?(:join) ? #{tmp}.flatten.compact.join(#{delimiter.inspect}) : #{tmp}"
28
+ #else
29
+ tmp
30
+ #end
31
+ ]]]]
32
+ end
33
+ [:html, :attr, name, [:escape, escape, value]]
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,24 @@
1
+ module Skim
2
+ class Engine < Temple::Engine
3
+ set_default_options :pretty => false,
4
+ :attr_wrapper => '"',
5
+ :attr_delimiter => {'class' => ' '},
6
+ :generator => Temple::CoffeeScript::Generator,
7
+ :use_asset => false
8
+
9
+ use Slim::Parser, :file, :tabsize, :encoding, :default_tag
10
+ use Slim::EmbeddedEngine, :enable_engines, :disable_engines, :pretty
11
+ use Slim::Interpolation
12
+ use Skim::Sections, :sections
13
+ use Skim::Compiler, :disable_capture, :attr_delimiter
14
+ use Temple::CoffeeScript::AttributeMerger, :attr_delimiter
15
+ use Temple::HTML::AttributeSorter
16
+ use Temple::CoffeeScript::AttributeRemover
17
+ use Temple::HTML::Fast, :format, :attr_wrapper
18
+ use Temple::CoffeeScript::Filters::Escapable, :disable_escape
19
+ use Temple::CoffeeScript::Filters::ControlFlow
20
+ filter :MultiFlattener
21
+ use(:Optimizer) { Temple::Filters::StaticMerger.new }
22
+ use(:Generator) { options[:generator].new(options) }
23
+ end
24
+ end
data/lib/skim/rails.rb ADDED
@@ -0,0 +1,4 @@
1
+ module Skim
2
+ class RailsEngine < ::Rails::Engine
3
+ end
4
+ end
@@ -0,0 +1,32 @@
1
+ require "multi_json"
2
+
3
+ module Skim
4
+ class Sections < Slim::Sections
5
+ def call(exp)
6
+ if options[:sections]
7
+ compile(exp)
8
+ else
9
+ exp
10
+ end
11
+ end
12
+
13
+ protected
14
+
15
+ def on_slim_inverted_section(name, content)
16
+ [:if, "not #{access(name)}", compile(content)]
17
+ end
18
+
19
+ def on_slim_section(name, content)
20
+ tmp1, tmp2 = unique_name, unique_name
21
+ [:if, "#{tmp1} = #{access(name)}",
22
+ [:block, "for #{tmp2} in #{tmp1}",
23
+ [:block, "Skim.withContext.call @, #{tmp2}, ->", compile(content)]]]
24
+ end
25
+
26
+ private
27
+
28
+ def access(name)
29
+ "Skim.access.call(@, #{MultiJson.encode(name.to_s)})"
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,31 @@
1
+ module Skim
2
+ Template = Temple::Templates::Tilt(Skim::Engine, :register_as => :skim)
3
+
4
+ class Template
5
+ def coffee_script_src
6
+ engine = self.class.build_engine({
7
+ :streaming => false, # Overwrite option: No streaming support in Tilt
8
+ :file => eval_file,
9
+ :indent => 2 }, options)
10
+ <<SRC
11
+ return (context = {}) ->
12
+ #{self.class.skim_src unless engine.options[:use_asset]}
13
+ Skim.withContext.call {}, context, ->
14
+ #{engine.call(data)}
15
+ SRC
16
+ end
17
+
18
+ def prepare
19
+ @src = CoffeeScript.compile(coffee_script_src)
20
+ end
21
+
22
+ def evaluate(scope, locals, &block)
23
+ precompiled_template
24
+ end
25
+
26
+ def self.skim_src
27
+ @@skim_src ||=
28
+ File.read(File.expand_path("../../../vendor/assets/javascripts/skim.js.coffee", __FILE__))
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,3 @@
1
+ module Skim
2
+ VERSION = "0.8.0"
3
+ end
data/lib/skim.rb ADDED
@@ -0,0 +1,15 @@
1
+ require "temple"
2
+ require "temple/coffee_script"
3
+
4
+ require "slim"
5
+ require "skim/compiler"
6
+ require "skim/sections"
7
+ require "skim/engine"
8
+ require "skim/template"
9
+ require "skim/version"
10
+
11
+ require "skim/rails" if Object.const_defined?(:Rails)
12
+
13
+ require "sprockets"
14
+ Sprockets::Engines # force autoload
15
+ Sprockets.register_engine ".skim", Skim::Template
@@ -0,0 +1,33 @@
1
+ module Temple
2
+ module CoffeeScript
3
+ class AttributeMerger < Filter
4
+ include Temple::HTML::Dispatcher
5
+
6
+ default_options[:attr_delimiter] = {'id' => '_', 'class' => ' '}
7
+
8
+ def on_html_attrs(*attrs)
9
+ names = []
10
+ result = {}
11
+
12
+ attrs.each do |html, attr, name, value|
13
+ raise(InvalidExpression, 'Attribute is not a html attr') if html != :html || attr != :attr
14
+ name = name.to_s
15
+ if delimiter = options[:attr_delimiter][name]
16
+ if current = result[name]
17
+ current << [:static, delimiter] << value
18
+ else
19
+ result[name] = [:multi, value]
20
+ names << name
21
+ end
22
+ else
23
+ raise "Multiple #{name} attributes specified" if result[name]
24
+ result[name] = value
25
+ names << name
26
+ end
27
+ end
28
+
29
+ [:html, :attrs, *names.map {|name| [:html, :attr, name, result[name]]}]
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,28 @@
1
+ module Temple
2
+ module CoffeeScript
3
+ class AttributeRemover < Filter
4
+ include Temple::HTML::Dispatcher
5
+
6
+ default_options[:remove_empty_attrs] = true
7
+
8
+ def on_html_attrs(*attrs)
9
+ [:multi, *(options[:remove_empty_attrs] ?
10
+ attrs.map {|attr| compile(attr) } : attrs)]
11
+ end
12
+
13
+ def on_html_attr(name, value)
14
+ if empty_exp?(value)
15
+ value
16
+ elsif contains_static?(value)
17
+ [:html, :attr, name, value]
18
+ else
19
+ tmp = unique_name
20
+ [:multi,
21
+ [:capture, tmp, compile(value)],
22
+ [:if, "#{tmp}.length > 0",
23
+ [:html, :attr, name, [:dynamic, tmp]]]]
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,37 @@
1
+ module Temple
2
+ module CoffeeScript
3
+ module Filters
4
+ class ControlFlow < Filter
5
+ def on_if(condition, yes, no = nil)
6
+ result = [:multi, [:code, "if #{condition}"], [:indent, compile(yes)]]
7
+ while no && no.first == :if
8
+ result << [:code, "else if #{no[1]}"] << [:indent, compile(no[2])]
9
+ no = no[3]
10
+ end
11
+ result << [:code, 'else'] << [:indent, compile(no)] if no
12
+ result
13
+ end
14
+
15
+ def on_case(arg, *cases)
16
+ result = [:multi, [:code, arg ? "switch (#{arg})" : 'switch'], [:indent, [:multi]]]
17
+ cases.map do |c|
18
+ condition, *exps = c
19
+ result[2][1] << [:code, condition == :else ? 'else' : "when #{condition}"]
20
+ exps.each {|e| result[2][1] << [:indent, compile(e)] }
21
+ end
22
+ result
23
+ end
24
+
25
+ def on_cond(*cases)
26
+ on_case(nil, *cases)
27
+ end
28
+
29
+ def on_block(code, exp)
30
+ [:multi,
31
+ [:code, code],
32
+ [:indent, compile(exp)]]
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,34 @@
1
+ module Temple
2
+ module CoffeeScript
3
+ module Filters
4
+ class Escapable < Filter
5
+ # Activate the usage of html_safe? if it is available (for Rails 3 for example)
6
+ set_default_options :use_html_safe => ''.respond_to?(:html_safe?),
7
+ :disable_escape => false
8
+
9
+ def initialize(opts = {})
10
+ super
11
+ @escape_code = "::Temple::Utils.escape_html#{options[:use_html_safe] ? '_safe' : ''}((%s))"
12
+ @escaper = eval("proc {|v| #{@escape_code % 'v'} }")
13
+ @escape = false
14
+ end
15
+
16
+ def on_escape(flag, exp)
17
+ old = @escape
18
+ @escape = flag && !options[:disable_escape]
19
+ compile(exp)
20
+ ensure
21
+ @escape = old
22
+ end
23
+
24
+ def on_static(value)
25
+ [:static, @escape ? @escaper[value] : value]
26
+ end
27
+
28
+ def on_dynamic(value)
29
+ [:dynamic, @escape ? "@escape(#{value})" : value]
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,4 @@
1
+ require "temple/coffee_script/filters/attribute_merger"
2
+ require "temple/coffee_script/filters/attribute_remover"
3
+ require "temple/coffee_script/filters/control_flow"
4
+ require "temple/coffee_script/filters/escapable"
@@ -0,0 +1,53 @@
1
+ require "multi_json"
2
+
3
+ module Temple
4
+ module CoffeeScript
5
+ class Generator < Temple::Generator
6
+ default_options[:indent] = 0
7
+
8
+ def call(exp)
9
+ @indent = options[:indent]
10
+
11
+ compile [:multi,
12
+ [:code, "#{buffer} = []"],
13
+ exp,
14
+ [:code, "#{buffer}.join('')"]]
15
+ end
16
+
17
+ def on_multi(*exp)
18
+ exp.map {|e| compile(e) }.join("\n")
19
+ end
20
+
21
+ def on_static(text)
22
+ concat(MultiJson.encode(text))
23
+ end
24
+
25
+ def on_dynamic(code)
26
+ concat(code)
27
+ end
28
+
29
+ def on_code(code)
30
+ indent(code)
31
+ end
32
+
33
+ def on_indent(exp)
34
+ @indent += 1
35
+ compile(exp)
36
+ ensure
37
+ @indent -= 1
38
+ end
39
+
40
+ def on_capture(name, exp)
41
+ self.class.new(:buffer => name, :indent => @indent).call(exp)
42
+ end
43
+
44
+ def concat(str)
45
+ indent("#{buffer}.push(#{str})")
46
+ end
47
+
48
+ def indent(str, indent = @indent)
49
+ " " * indent + str
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,3 @@
1
+ require "temple/mixins/indent_dispatcher.rb"
2
+ require "temple/coffee_script/filters"
3
+ require "temple/coffee_script/generators"
@@ -0,0 +1,13 @@
1
+ module Temple
2
+ module Mixins
3
+ module IndentDispatcher
4
+ def on_indent(exp)
5
+ [:indent, compile(exp)]
6
+ end
7
+ end
8
+
9
+ module Dispatcher
10
+ include IndentDispatcher
11
+ end
12
+ end
13
+ end
data/skim.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/skim/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["John Firebaugh"]
6
+ gem.email = ["john.firebaugh@gmail.com"]
7
+ gem.description = %q{Fat-free client-side templates with Slim and CoffeeScript}
8
+ gem.summary = %q{Take the fat out of your client-side templates with Skim. Skim is the Slim templating engine
9
+ with embedded CoffeeScript. It compiles to JavaScript templates (.jst), which can then be served by Rails or any other
10
+ Sprockets-based asset pipeline.}
11
+ gem.homepage = ""
12
+
13
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
14
+ gem.files = `git ls-files`.split("\n")
15
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ gem.name = "skim"
17
+ gem.require_paths = ["lib"]
18
+ gem.version = Skim::VERSION
19
+
20
+ gem.add_dependency "slim", "~> 1.1.0"
21
+ gem.add_dependency "temple", "~> 0.4.0"
22
+ gem.add_dependency "coffee-script"
23
+ gem.add_dependency "multi_json"
24
+ gem.add_dependency "sprockets"
25
+
26
+ gem.add_development_dependency "rake"
27
+ gem.add_development_dependency "execjs"
28
+ gem.add_development_dependency "minitest-reporters"
29
+ gem.add_development_dependency "therubyracer"
30
+ end
@@ -0,0 +1,82 @@
1
+ class Context
2
+ constructor: ->
3
+ @var = 'instance'
4
+ @x = 0
5
+
6
+ id_helper: ->
7
+ "notice"
8
+
9
+ hash: ->
10
+ a: 'The letter a', b: 'The letter b'
11
+
12
+ show_first: (show = false) ->
13
+ show
14
+
15
+ define_macro: (name, block) ->
16
+ @macro ||= {}
17
+ @macro[name] = block
18
+ ''
19
+
20
+ call_macro: (name, args...) ->
21
+ @macro[name](args...)
22
+
23
+ hello_world: (text = "Hello World from @env", opts = {}) ->
24
+ text + ("#{key} #{value}" for key, value of opts).join(" ")
25
+
26
+ callback: (text, block) ->
27
+ if block
28
+ "#{text} #{block()} #{text}"
29
+ else
30
+ text
31
+
32
+ message: (args...) ->
33
+ args.join(' ')
34
+
35
+ action_path: (args...) ->
36
+ "/action-#{args.join('-')}"
37
+
38
+ in_keyword: ->
39
+ "starts with keyword"
40
+
41
+ evil_method: ->
42
+ "<script>do_something_evil();</script>"
43
+
44
+ method_which_returns_true: ->
45
+ true
46
+
47
+ output_number: ->
48
+ 1337
49
+
50
+ succ_x: ->
51
+ @x = @x + 1
52
+
53
+ person: ->
54
+ [{name: 'Joe'}, {name: 'Jack'}]
55
+
56
+ people: ->
57
+ ['Andy', 'Fred', 'Daniel'].map (n) -> new Person(n)
58
+
59
+ cities: ->
60
+ ['Atlanta', 'Melbourne', 'Karlsruhe']
61
+
62
+ people_with_locations: ->
63
+ @people().map (p, i) =>
64
+ p.location = new Location(@cities()[i])
65
+ p
66
+
67
+ constant_object: ->
68
+ @_constant_object ||= {a: 1, b: 2}
69
+
70
+ constant_object_size: ->
71
+ i = 0
72
+ i++ for k, v of @constant_object()
73
+ i
74
+
75
+ class Person
76
+ constructor: (@_name) ->
77
+ name: => @_name
78
+ city: => @location.city()
79
+
80
+ class Location
81
+ constructor: (@_city) ->
82
+ city: => @_city