From 32225cb93ee91622469702b4974fccf125a09f85 Mon Sep 17 00:00:00 2001 From: Jon Bracy Date: Thu, 27 Feb 2014 12:35:59 -0800 Subject: [PATCH 01/16] First pass at allowing "Sub-templating" or "Blocks" --- README.md | 19 +++++++++++++++++++ lib/ejs.rb | 43 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b5db320..c4ea210 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,25 @@ The EJS tag syntax is as follows: its string value into the template output. * `<%- ... %>` behaves like `<%= ... %>` but HTML-escapes its output. +If a evalation tag (`<%=`) ends with an open function, the function +return a compiled template. For example: + +```erb +<% formTag = function(template) { return '
\n'+template()+'\n
'; } %> + +<%= formTag(function () { %> + +<% }) %> +``` + +generates: + +```html +
+ +
+``` + If you have the [ExecJS](https://github.com/sstephenson/execjs/) library and a suitable JavaScript runtime installed, you can pass a template and an optional hash of local variables to `EJS.evaluate`: diff --git a/lib/ejs.rb b/lib/ejs.rb index 686649a..737d35d 100644 --- a/lib/ejs.rb +++ b/lib/ejs.rb @@ -19,6 +19,7 @@ module EJS class << self attr_accessor :evaluation_pattern attr_accessor :interpolation_pattern + attr_accessor :interpolation_with_subtemplate_pattern attr_accessor :escape_pattern # Compiles an EJS template to a JavaScript function. The compiled @@ -36,10 +37,22 @@ def compile(source, options = {}) js_escape!(source) replace_escape_tags!(source, options) + replace_interpolation_with_subtemplate_tags!(source, options) replace_interpolation_tags!(source, options) replace_evaluation_tags!(source, options) - "function(obj){var __p=[],print=function(){__p.push.apply(__p,arguments);};" + - "with(obj||{}){__p.push('#{source}');}return __p.join('');}" + + <<-EJS + function(locals) { + var __p = []; + var print = function() { __p.push.apply(__p,arguments); }; + + with(locals||{}) { + __p.push('#{source}'); + } + + return __p.join(''); + } + EJS end # Evaluates an EJS template with the given local variables and @@ -73,6 +86,31 @@ def replace_escape_tags!(source, options) end end + def replace_interpolation_with_subtemplate_tags!(source, options) + source.gsub!(options[:interpolation_with_subtemplate_pattern] || interpolation_with_subtemplate_pattern) do + lines = [] + matches = [$1, $2, $3] + + replace_escape_tags!(matches[1], options) + replace_interpolation_with_subtemplate_tags!(matches[1], options) + replace_interpolation_tags!(matches[1], options) + replace_evaluation_tags!(matches[1], options) + + lines << "', #{matches[0]}" + lines << <<-EJS + var __p = []; + var print = function() { __p.push.apply(__p,arguments); }; + + __p.push('#{matches[1]}'); + + return __p.join(''); + EJS + lines << "#{matches[2]},'" + + lines.join("\n") + end + end + def replace_evaluation_tags!(source, options) source.gsub!(options[:evaluation_pattern] || evaluation_pattern) do "'); #{js_unescape!($1)}; __p.push('" @@ -97,5 +135,6 @@ def escape_function self.evaluation_pattern = /<%([\s\S]+?)%>/ self.interpolation_pattern = /<%=([\s\S]+?)%>/ + self.interpolation_with_subtemplate_pattern = /<%=((?:(?!%>)[\s\S])+{)\s*%>((?:(?!<%\s*})[\s\S])+)<%\s*((?:(?!%>)[\s\S])+)%>/m self.escape_pattern = /<%-([\s\S]+?)%>/ end From 0923cba9abaef34d1f1e1a8819bd6b84b88c55d0 Mon Sep 17 00:00:00 2001 From: Jon Bracy Date: Mon, 17 Mar 2014 21:50:37 -0700 Subject: [PATCH 02/16] Change to a recursive regex to allow sub-templates --- lib/ejs.rb | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/lib/ejs.rb b/lib/ejs.rb index 737d35d..0f8a213 100644 --- a/lib/ejs.rb +++ b/lib/ejs.rb @@ -87,9 +87,11 @@ def replace_escape_tags!(source, options) end def replace_interpolation_with_subtemplate_tags!(source, options) - source.gsub!(options[:interpolation_with_subtemplate_pattern] || interpolation_with_subtemplate_pattern) do + regex = options[:interpolation_with_subtemplate_pattern] || interpolation_with_subtemplate_pattern + source.gsub!(regex) do |str| + match_data = regex.match(str) lines = [] - matches = [$1, $2, $3] + matches = [match_data[:start], match_data[:middle], match_data[:end]] replace_escape_tags!(matches[1], options) replace_interpolation_with_subtemplate_tags!(matches[1], options) @@ -135,6 +137,24 @@ def escape_function self.evaluation_pattern = /<%([\s\S]+?)%>/ self.interpolation_pattern = /<%=([\s\S]+?)%>/ - self.interpolation_with_subtemplate_pattern = /<%=((?:(?!%>)[\s\S])+{)\s*%>((?:(?!<%\s*})[\s\S])+)<%\s*((?:(?!%>)[\s\S])+)%>/m self.escape_pattern = /<%-([\s\S]+?)%>/ + self.interpolation_with_subtemplate_pattern = %r{ + <%=(?(?:(?!%>)[\s\S])+\{)\s*%> + (? + ( + (? + <%=?(?:(?!%>)[\s\S])+\{\s*%> + (?: + \g + | + .{1} + )* + <%\s*\}(?:(?!%>)[\s\S])+%> + ) + | + .{1} + )* + ) + <%\s*(?\}(?:(?!%>)[\s\S])+)%> + }xm end From 8c49d2ac346cbbcec1578b962db5f4bf0b88d683 Mon Sep 17 00:00:00 2001 From: Jon Bracy Date: Fri, 21 Mar 2014 11:29:18 -0700 Subject: [PATCH 03/16] Updated regex for if "<% } else { %>" --- lib/ejs.rb | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/ejs.rb b/lib/ejs.rb index 0f8a213..34d5b25 100644 --- a/lib/ejs.rb +++ b/lib/ejs.rb @@ -143,18 +143,20 @@ def escape_function (? ( (? - <%=?(?:(?!%>)[\s\S])+\{\s*%> + <%=?[\s\S]+?\{\s*%> (?: + <%\s*\}[\s\S]+?\{\s*%> + | \g | - .{1} - )* - <%\s*\}(?:(?!%>)[\s\S])+%> + (?:(?!<%\s*\}[\s\S]+?%>)).{1} + )*? + <%\s*\}[^\{]+?%> ) | - .{1} - )* + (?:(?!<%\s*\}[\s\S]+?%>)).{1} + )*? ) - <%\s*(?\}(?:(?!%>)[\s\S])+)%> + <%\s*(?\}[\s\S]+?)%> }xm end From 31c912b3ab29b42d5dcc2b3f6f7e9b61826b8c28 Mon Sep 17 00:00:00 2001 From: Jon Bracy Date: Fri, 21 Mar 2014 12:02:05 -0700 Subject: [PATCH 04/16] Fixed issue no escaping javascript --- lib/ejs.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ejs.rb b/lib/ejs.rb index 34d5b25..8027fa6 100644 --- a/lib/ejs.rb +++ b/lib/ejs.rb @@ -98,7 +98,7 @@ def replace_interpolation_with_subtemplate_tags!(source, options) replace_interpolation_tags!(matches[1], options) replace_evaluation_tags!(matches[1], options) - lines << "', #{matches[0]}" + lines << "', #{js_unescape!(matches[0])}" lines << <<-EJS var __p = []; var print = function() { __p.push.apply(__p,arguments); }; @@ -107,7 +107,7 @@ def replace_interpolation_with_subtemplate_tags!(source, options) return __p.join(''); EJS - lines << "#{matches[2]},'" + lines << "#{js_unescape!(matches[2])},'" lines.join("\n") end From 4dd75acc5376f828fa1fc85ed14e9f13243d9d2a Mon Sep 17 00:00:00 2001 From: Jon Bracy Date: Fri, 21 Mar 2014 12:48:13 -0700 Subject: [PATCH 05/16] Another regex fix --- lib/ejs.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/ejs.rb b/lib/ejs.rb index 8027fa6..90d6a95 100644 --- a/lib/ejs.rb +++ b/lib/ejs.rb @@ -143,18 +143,18 @@ def escape_function (? ( (? - <%=?[\s\S]+?\{\s*%> + <%=?(?:(?!%>)[\s\S])+\{\s*%> (?: - <%\s*\}[\s\S]+?\{\s*%> + <%\s*\}(?:(?!\{\s*%>)[\s\S])+\{\s*%> | \g | - (?:(?!<%\s*\}[\s\S]+?%>)).{1} + .{1} )*? <%\s*\}[^\{]+?%> ) | - (?:(?!<%\s*\}[\s\S]+?%>)).{1} + .{1} )*? ) <%\s*(?\}[\s\S]+?)%> From 621d6fef4b75e395edf199da18dcb695e22c859e Mon Sep 17 00:00:00 2001 From: Jon Bracy Date: Sun, 26 Oct 2014 11:42:15 -0700 Subject: [PATCH 06/16] Moved to using MiniTest --- Rakefile | 4 +- test/{test_ejs.rb => ejs_test.rb} | 70 +++++++++++++++++++++++-------- test/test_helper.rb | 57 +++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 19 deletions(-) rename test/{test_ejs.rb => ejs_test.rb} (84%) create mode 100644 test/test_helper.rb diff --git a/Rakefile b/Rakefile index 2b9add8..4f01799 100644 --- a/Rakefile +++ b/Rakefile @@ -3,6 +3,6 @@ require "rake/testtask" task :default => :test Rake::TestTask.new do |t| - t.libs << "test" - t.warning = true + t.libs << 'test' + t.test_files = FileList['test/**/*_test.rb'] end diff --git a/test/test_ejs.rb b/test/ejs_test.rb similarity index 84% rename from test/test_ejs.rb rename to test/ejs_test.rb index ed760da..8f1ba7f 100644 --- a/test/test_ejs.rb +++ b/test/ejs_test.rb @@ -1,31 +1,68 @@ -require "ejs" -require "test/unit" +require 'test_helper' -FUNCTION_PATTERN = /^function\s*\(.*?\)\s*\{(.*?)\}$/ +FUNCTION_PATTERN = /\A\s*function\s*\(.*?\)\s*\{(.*?)\}\Z/m BRACE_SYNTAX = { :evaluation_pattern => /\{\{([\s\S]+?)\}\}/, :interpolation_pattern => /\{\{=([\s\S]+?)\}\}/, - :escape_pattern => /\{\{-([\s\S]+?)\}\}/ + :escape_pattern => /\{\{-([\s\S]+?)\}\}/, + :interpolation_with_subtemplate_pattern => %r{ + \{\{=(?(?:(?!\}\})[\s\S])+\{)\s*\}\} + (? + ( + (? + \{\{=?(?:(?!\}\})[\s\S])+\{\s*\}\} + (?: + \{\{\s*\}(?:(?!\{\s*\}\})[\s\S])+\{\s*\}\} + | + \g + | + .{1} + )*? + \{\{\s*\}[^\{]+?\}\} + ) + | + .{1} + )*? + ) + \{\{\s*(?\}[\s\S]+?)\}\} + }xm } QUESTION_MARK_SYNTAX = { :evaluation_pattern => /<\?([\s\S]+?)\?>/, :interpolation_pattern => /<\?=([\s\S]+?)\?>/, - :escape_pattern => /<\?-([\s\S]+?)\?>/ -} + :escape_pattern => /<\?-([\s\S]+?)\?>/, + :interpolation_with_subtemplate_pattern => %r{ + <\?=(?(?:(?!\?>)[\s\S])+\{)\s*\?> + (? + ( + (? + <\?=?(?:(?!\?>)[\s\S])+\{\s*\?> + (?: + <\?\s*\}(?:(?!\{\s*\?>)[\s\S])+\{\s*\?> + | + \g + | + .{1} + )*? + <\?\s*\}[^\{]+?\?> + ) + | + .{1} + )*? + ) + <\?\s*(?\}[\s\S]+?)\?> + }xm -module TestHelper - def test(name, &block) - define_method("test #{name.inspect}", &block) - end -end +} -class EJSCompilationTest < Test::Unit::TestCase - extend TestHelper +class EJSCompilationTest < Minitest::Test + test "compile" do result = EJS.compile("Hello <%= name %>") + assert_match FUNCTION_PATTERN, result assert_no_match(/Hello \<%= name %\>/, result) end @@ -37,10 +74,10 @@ class EJSCompilationTest < Test::Unit::TestCase assert_match FUNCTION_PATTERN, braced_result assert_equal standard_result, braced_result end + end -class EJSCustomPatternTest < Test::Unit::TestCase - extend TestHelper +class EJSCustomPatternTest < Minitest::Test def setup @original_evaluation_pattern = EJS.evaluation_pattern @@ -69,8 +106,7 @@ def teardown end end -class EJSEvaluationTest < Test::Unit::TestCase - extend TestHelper +class EJSEvaluationTest < Minitest::Test test "quotes" do template = "<%= thing %> is gettin' on my noives!" diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000..643ced7 --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,57 @@ +# # To make testing/debugging easier, test within this source tree versus an +# # installed gem +# dir = File.dirname(__FILE__) +# root = File.expand_path(File.join(dir, '..')) +# lib = File.expand_path(File.join(root, 'lib')) +# +# $LOAD_PATH << lib + +require "minitest/autorun" +require 'minitest/reporters' +# require "mocha/setup" +# require 'wankel' +require "ejs" + +Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new + +# File 'lib/active_support/testing/declarative.rb', somewhere in rails.... +class Minitest::Test + def self.test(name, &block) + test_name = "test_#{name.gsub(/\s+/,'_')}".to_sym + defined = instance_method(test_name) rescue false + raise "#{test_name} is already defined in #{self}" if defined + if block_given? + define_method(test_name, &block) + else + define_method(test_name) do + flunk "No implementation provided for #{name}" + end + end + end + + # test/unit backwards compatibility methods + alias :assert_raise :assert_raises + alias :assert_not_empty :refute_empty + alias :assert_not_equal :refute_equal + alias :assert_not_in_delta :refute_in_delta + alias :assert_not_in_epsilon :refute_in_epsilon + alias :assert_not_includes :refute_includes + alias :assert_not_instance_of :refute_instance_of + alias :assert_not_kind_of :refute_kind_of + alias :assert_no_match :refute_match + alias :assert_not_nil :refute_nil + alias :assert_not_operator :refute_operator + alias :assert_not_predicate :refute_predicate + alias :assert_not_respond_to :refute_respond_to + alias :assert_not_same :refute_same + + # Fails if the block raises an exception. + # + # assert_nothing_raised do + # ... + # end + def assert_nothing_raised(*args) + yield + end + +end \ No newline at end of file From da9edc2896e2c4458e825fd1269b1f1dc4d589fa Mon Sep 17 00:00:00 2001 From: Jon Bracy Date: Sun, 26 Oct 2014 11:45:23 -0700 Subject: [PATCH 07/16] Broke test out into separate files --- test/compilation_test.rb | 20 +++++ test/custom_pattern_test.rb | 30 +++++++ test/{ejs_test.rb => evaluation_test.rb} | 110 +---------------------- test/test_helper.rb | 67 +++++++++++--- 4 files changed, 109 insertions(+), 118 deletions(-) create mode 100644 test/compilation_test.rb create mode 100644 test/custom_pattern_test.rb rename test/{ejs_test.rb => evaluation_test.rb} (64%) diff --git a/test/compilation_test.rb b/test/compilation_test.rb new file mode 100644 index 0000000..fa64746 --- /dev/null +++ b/test/compilation_test.rb @@ -0,0 +1,20 @@ +require 'test_helper' + +class CompilationTest < Minitest::Test + + test "compile" do + result = EJS.compile("Hello <%= name %>") + + assert_match FUNCTION_PATTERN, result + assert_no_match(/Hello \<%= name %\>/, result) + end + + test "compile with custom syntax" do + standard_result = EJS.compile("Hello <%= name %>") + braced_result = EJS.compile("Hello {{= name }}", BRACE_SYNTAX) + + assert_match FUNCTION_PATTERN, braced_result + assert_equal standard_result, braced_result + end + +end diff --git a/test/custom_pattern_test.rb b/test/custom_pattern_test.rb new file mode 100644 index 0000000..8d71475 --- /dev/null +++ b/test/custom_pattern_test.rb @@ -0,0 +1,30 @@ +require 'test_helper' + +class CustomPatternTest < Minitest::Test + + def setup + @original_evaluation_pattern = EJS.evaluation_pattern + @original_interpolation_pattern = EJS.interpolation_pattern + EJS.evaluation_pattern = BRACE_SYNTAX[:evaluation_pattern] + EJS.interpolation_pattern = BRACE_SYNTAX[:interpolation_pattern] + end + + def teardown + EJS.interpolation_pattern = @original_interpolation_pattern + EJS.evaluation_pattern = @original_evaluation_pattern + end + + test "compile" do + result = EJS.compile("Hello {{= name }}") + assert_match FUNCTION_PATTERN, result + assert_no_match(/Hello \{\{= name \}\}/, result) + end + + test "compile with custom syntax" do + standard_result = EJS.compile("Hello {{= name }}") + question_result = EJS.compile("Hello ", QUESTION_MARK_SYNTAX) + + assert_match FUNCTION_PATTERN, question_result + assert_equal standard_result, question_result + end +end diff --git a/test/ejs_test.rb b/test/evaluation_test.rb similarity index 64% rename from test/ejs_test.rb rename to test/evaluation_test.rb index 8f1ba7f..baf6eda 100644 --- a/test/ejs_test.rb +++ b/test/evaluation_test.rb @@ -1,112 +1,6 @@ require 'test_helper' -FUNCTION_PATTERN = /\A\s*function\s*\(.*?\)\s*\{(.*?)\}\Z/m - -BRACE_SYNTAX = { - :evaluation_pattern => /\{\{([\s\S]+?)\}\}/, - :interpolation_pattern => /\{\{=([\s\S]+?)\}\}/, - :escape_pattern => /\{\{-([\s\S]+?)\}\}/, - :interpolation_with_subtemplate_pattern => %r{ - \{\{=(?(?:(?!\}\})[\s\S])+\{)\s*\}\} - (? - ( - (? - \{\{=?(?:(?!\}\})[\s\S])+\{\s*\}\} - (?: - \{\{\s*\}(?:(?!\{\s*\}\})[\s\S])+\{\s*\}\} - | - \g - | - .{1} - )*? - \{\{\s*\}[^\{]+?\}\} - ) - | - .{1} - )*? - ) - \{\{\s*(?\}[\s\S]+?)\}\} - }xm -} - -QUESTION_MARK_SYNTAX = { - :evaluation_pattern => /<\?([\s\S]+?)\?>/, - :interpolation_pattern => /<\?=([\s\S]+?)\?>/, - :escape_pattern => /<\?-([\s\S]+?)\?>/, - :interpolation_with_subtemplate_pattern => %r{ - <\?=(?(?:(?!\?>)[\s\S])+\{)\s*\?> - (? - ( - (? - <\?=?(?:(?!\?>)[\s\S])+\{\s*\?> - (?: - <\?\s*\}(?:(?!\{\s*\?>)[\s\S])+\{\s*\?> - | - \g - | - .{1} - )*? - <\?\s*\}[^\{]+?\?> - ) - | - .{1} - )*? - ) - <\?\s*(?\}[\s\S]+?)\?> - }xm - -} - - -class EJSCompilationTest < Minitest::Test - - test "compile" do - result = EJS.compile("Hello <%= name %>") - - assert_match FUNCTION_PATTERN, result - assert_no_match(/Hello \<%= name %\>/, result) - end - - test "compile with custom syntax" do - standard_result = EJS.compile("Hello <%= name %>") - braced_result = EJS.compile("Hello {{= name }}", BRACE_SYNTAX) - - assert_match FUNCTION_PATTERN, braced_result - assert_equal standard_result, braced_result - end - -end - -class EJSCustomPatternTest < Minitest::Test - - def setup - @original_evaluation_pattern = EJS.evaluation_pattern - @original_interpolation_pattern = EJS.interpolation_pattern - EJS.evaluation_pattern = BRACE_SYNTAX[:evaluation_pattern] - EJS.interpolation_pattern = BRACE_SYNTAX[:interpolation_pattern] - end - - def teardown - EJS.interpolation_pattern = @original_interpolation_pattern - EJS.evaluation_pattern = @original_evaluation_pattern - end - - test "compile" do - result = EJS.compile("Hello {{= name }}") - assert_match FUNCTION_PATTERN, result - assert_no_match(/Hello \{\{= name \}\}/, result) - end - - test "compile with custom syntax" do - standard_result = EJS.compile("Hello {{= name }}") - question_result = EJS.compile("Hello ", QUESTION_MARK_SYNTAX) - - assert_match FUNCTION_PATTERN, question_result - assert_equal standard_result, question_result - end -end - -class EJSEvaluationTest < Minitest::Test +class EvaluationTest < Minitest::Test test "quotes" do template = "<%= thing %> is gettin' on my noives!" @@ -233,4 +127,4 @@ class EJSEvaluationTest < Minitest::Test template = "" assert_equal "'Foo Bar'", EJS.evaluate(template, { :foobar => "'Foo Bar'" }, QUESTION_MARK_SYNTAX) end -end +end \ No newline at end of file diff --git a/test/test_helper.rb b/test/test_helper.rb index 643ced7..cd02c7f 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,17 +1,64 @@ -# # To make testing/debugging easier, test within this source tree versus an -# # installed gem -# dir = File.dirname(__FILE__) -# root = File.expand_path(File.join(dir, '..')) -# lib = File.expand_path(File.join(root, 'lib')) -# -# $LOAD_PATH << lib - require "minitest/autorun" require 'minitest/reporters' -# require "mocha/setup" -# require 'wankel' require "ejs" +FUNCTION_PATTERN = /\A\s*function\s*\(.*?\)\s*\{(.*?)\}\Z/m + +BRACE_SYNTAX = { + :evaluation_pattern => /\{\{([\s\S]+?)\}\}/, + :interpolation_pattern => /\{\{=([\s\S]+?)\}\}/, + :escape_pattern => /\{\{-([\s\S]+?)\}\}/, + :interpolation_with_subtemplate_pattern => %r{ + \{\{=(?(?:(?!\}\})[\s\S])+\{)\s*\}\} + (? + ( + (? + \{\{=?(?:(?!\}\})[\s\S])+\{\s*\}\} + (?: + \{\{\s*\}(?:(?!\{\s*\}\})[\s\S])+\{\s*\}\} + | + \g + | + .{1} + )*? + \{\{\s*\}[^\{]+?\}\} + ) + | + .{1} + )*? + ) + \{\{\s*(?\}[\s\S]+?)\}\} + }xm +} + +QUESTION_MARK_SYNTAX = { + :evaluation_pattern => /<\?([\s\S]+?)\?>/, + :interpolation_pattern => /<\?=([\s\S]+?)\?>/, + :escape_pattern => /<\?-([\s\S]+?)\?>/, + :interpolation_with_subtemplate_pattern => %r{ + <\?=(?(?:(?!\?>)[\s\S])+\{)\s*\?> + (? + ( + (? + <\?=?(?:(?!\?>)[\s\S])+\{\s*\?> + (?: + <\?\s*\}(?:(?!\{\s*\?>)[\s\S])+\{\s*\?> + | + \g + | + .{1} + )*? + <\?\s*\}[^\{]+?\?> + ) + | + .{1} + )*? + ) + <\?\s*(?\}[\s\S]+?)\?> + }xm + +} + Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new # File 'lib/active_support/testing/declarative.rb', somewhere in rails.... From 4506b1b7bbab538fdcc10999d77b5724e4f6fd84 Mon Sep 17 00:00:00 2001 From: Jon Bracy Date: Sun, 26 Oct 2014 12:04:27 -0700 Subject: [PATCH 08/16] Added subtemplate test --- test/subtemplate_test.rb | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 test/subtemplate_test.rb diff --git a/test/subtemplate_test.rb b/test/subtemplate_test.rb new file mode 100644 index 0000000..dab644a --- /dev/null +++ b/test/subtemplate_test.rb @@ -0,0 +1,25 @@ +require 'test_helper' + +class SubtemplateTest < Minitest::Test + + test "quotes" do + template = <<-DATA + <% formTag = function(template) { return '
\\n'+template()+'\\n
'; } %> + + <%= formTag(function () { %> + + <% }) %> + DATA + + assert_equal <<-DATA, EJS.evaluate(template) + + +
+ + + +
+ DATA + end + +end \ No newline at end of file From 61495925cb80fe5226831037460810a5179c3671 Mon Sep 17 00:00:00 2001 From: Jon Bracy Date: Wed, 17 Dec 2014 12:10:29 -0800 Subject: [PATCH 09/16] Added Gemfile --- Gemfile | 4 ++++ Gemfile.lock | 32 ++++++++++++++++++++++++++++++++ Rakefile | 2 ++ ejs.gemspec | 6 +++++- 4 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 Gemfile create mode 100644 Gemfile.lock diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..cba44f6 --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +source "https://rubygems.org" + +# Specify your gem's dependencies in cookie_store.gemspec +gemspec diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..f657166 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,32 @@ +PATH + remote: . + specs: + ejs (1.1.1) + +GEM + remote: https://rubygems.org/ + specs: + ansi (1.4.3) + builder (3.2.2) + execjs (0.4.0) + multi_json (~> 1.0) + minitest (5.5.0) + minitest-reporters (1.0.8) + ansi + builder + minitest (>= 5.0) + ruby-progressbar + multi_json (1.10.1) + rake (10.4.2) + ruby-progressbar (1.7.0) + +PLATFORMS + ruby + +DEPENDENCIES + bundler + ejs! + execjs + minitest + minitest-reporters + rake diff --git a/Rakefile b/Rakefile index 4f01799..67b47de 100644 --- a/Rakefile +++ b/Rakefile @@ -1,3 +1,5 @@ +require 'bundler/setup' +require "bundler/gem_tasks" require "rake/testtask" task :default => :test diff --git a/ejs.gemspec b/ejs.gemspec index 7db0406..c896e00 100644 --- a/ejs.gemspec +++ b/ejs.gemspec @@ -6,7 +6,11 @@ Gem::Specification.new do |s| s.files = Dir["README.md", "LICENSE", "lib/**/*.rb"] - s.add_development_dependency "execjs", "~> 0.4" + s.add_development_dependency "execjs" + s.add_development_dependency "rake" + s.add_development_dependency "bundler" + s.add_development_dependency "minitest" + s.add_development_dependency "minitest-reporters" s.authors = ["Sam Stephenson"] s.email = ["sstephenson@gmail.com"] From 22c3c8a22a792db96c1b99c397f9a60ce477933a Mon Sep 17 00:00:00 2001 From: Jon Bracy Date: Wed, 17 Dec 2014 12:11:58 -0800 Subject: [PATCH 10/16] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c4ea210..437f31e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -EJS (Embedded JavaScript) template compiler for Ruby +EJS (Embedded JavaScript) template compiler for Ruby [![Circle CI](https://circleci.com/gh/malomalo/ruby-ejs.svg?style=svg)](https://circleci.com/gh/malomalo/ruby-ejs) ==================================================== EJS templates embed JavaScript code inside `<% ... %>` tags, much like From 43954da5470e4b1475398da37db63b537a342313 Mon Sep 17 00:00:00 2001 From: Jon Bracy Date: Wed, 11 Jul 2018 10:23:39 -0700 Subject: [PATCH 11/16] Moving to parsing the EJS templates with ruby --- Gemfile.lock | 19 +- ejs.gemspec | 6 +- lib/assets/ejs.js | 9 + lib/ejs.rb | 347 ++++++++++++++++++++++++------------ test/compilation_test.rb | 18 +- test/custom_pattern_test.rb | 52 ++++-- test/evaluation_test.rb | 85 ++------- test/subtemplate_test.rb | 4 +- 8 files changed, 310 insertions(+), 230 deletions(-) create mode 100644 lib/assets/ejs.js diff --git a/Gemfile.lock b/Gemfile.lock index f657166..b949c3d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,24 +1,22 @@ PATH remote: . specs: - ejs (1.1.1) + ejs (2.0.0) GEM remote: https://rubygems.org/ specs: - ansi (1.4.3) + ansi (1.5.0) builder (3.2.2) - execjs (0.4.0) - multi_json (~> 1.0) - minitest (5.5.0) - minitest-reporters (1.0.8) + execjs (2.7.0) + minitest (5.11.3) + minitest-reporters (1.2.0) ansi builder minitest (>= 5.0) ruby-progressbar - multi_json (1.10.1) - rake (10.4.2) - ruby-progressbar (1.7.0) + rake (12.3.1) + ruby-progressbar (1.9.0) PLATFORMS ruby @@ -30,3 +28,6 @@ DEPENDENCIES minitest minitest-reporters rake + +BUNDLED WITH + 1.16.0 diff --git a/ejs.gemspec b/ejs.gemspec index c896e00..2811ec5 100644 --- a/ejs.gemspec +++ b/ejs.gemspec @@ -1,16 +1,16 @@ Gem::Specification.new do |s| s.name = "ejs" - s.version = "1.1.1" + s.version = "2.0.0" s.summary = "EJS (Embedded JavaScript) template compiler" - s.description = "Compile and evaluate EJS (Embedded JavaScript) templates from Ruby." + s.description = "Compile EJS (Embedded JavaScript) templates in Ruby." s.files = Dir["README.md", "LICENSE", "lib/**/*.rb"] - s.add_development_dependency "execjs" s.add_development_dependency "rake" s.add_development_dependency "bundler" s.add_development_dependency "minitest" s.add_development_dependency "minitest-reporters" + s.add_development_dependency "execjs" s.authors = ["Sam Stephenson"] s.email = ["sstephenson@gmail.com"] diff --git a/lib/assets/ejs.js b/lib/assets/ejs.js new file mode 100644 index 0000000..f7714b8 --- /dev/null +++ b/lib/assets/ejs.js @@ -0,0 +1,9 @@ +export function escape(string) { + if (string !== undefined && string != null) { + return String(string).replace(/[&<>'"\\/]/g, function (c) { + return '&#' + c.codePointAt(0) + ';'; + }); + } else { + return ''; + } +} \ No newline at end of file diff --git a/lib/ejs.rb b/lib/ejs.rb index 90d6a95..749fa89 100644 --- a/lib/ejs.rb +++ b/lib/ejs.rb @@ -1,27 +1,28 @@ -# EJS (Embedded JavaScript) template compiler for Ruby -# This is a port of Underscore.js' `_.template` function: -# http://documentcloud.github.com/underscore/ - module EJS - JS_UNESCAPES = { - '\\' => '\\', - "'" => "'", - 'r' => "\r", - 'n' => "\n", - 't' => "\t", - 'u2028' => "\u2028", - 'u2029' => "\u2029" - } - JS_ESCAPES = JS_UNESCAPES.invert - JS_UNESCAPE_PATTERN = /\\(#{Regexp.union(JS_UNESCAPES.keys)})/ - JS_ESCAPE_PATTERN = Regexp.union(JS_ESCAPES.keys) + DEFAULTS = { + open_tag: '<%', + close_tag: '%>', + + open_tag_modifiers: { + escape: '=', + unescape: '-', + comment: '#', + literal: '%' + }, + + close_tag_modifiers: { + trim: '-', + literal: '%' + }, + + escape: nil + } + + ASSET_DIR = File.join(__dir__, 'assets') + class << self - attr_accessor :evaluation_pattern - attr_accessor :interpolation_pattern - attr_accessor :interpolation_with_subtemplate_pattern - attr_accessor :escape_pattern - + # Compiles an EJS template to a JavaScript function. The compiled # function takes an optional argument, an object specifying local # variables in the template. You can optionally pass the @@ -32,131 +33,243 @@ class << self # EJS.compile("Hello <%= name %>") # # => "function(obj){...}" # + def transform(source, options = {}) + options = default(options) + + output = if options[:escape] + "import {" + options[:escape].split('.').reverse.join(" as escape} from '") + "';\n" + else + "import {escape} from 'ejs';\n" + end + + output << "export default function (locals) {\n" + output << function_source(source, options) + output << "}" + + output + end + def compile(source, options = {}) - source = source.dup - - js_escape!(source) - replace_escape_tags!(source, options) - replace_interpolation_with_subtemplate_tags!(source, options) - replace_interpolation_tags!(source, options) - replace_evaluation_tags!(source, options) - - <<-EJS - function(locals) { - var __p = []; - var print = function() { __p.push.apply(__p,arguments); }; - - with(locals||{}) { - __p.push('#{source}'); - } - - return __p.join(''); - } - EJS + options = default(options) + + output = "function(locals, escape) {\n" + output << function_source(source, options) + output << "}" + output end + # Evaluates an EJS template with the given local variables and # compiler options. You will need the ExecJS # (https://github.com/sstephenson/execjs/) library and a # JavaScript runtime available. # - # EJS.evaluate("Hello <%= name %>", :name => "world") + # EJS.evaluate("Hello <%= name %>", name: "world") # # => "Hello world" # def evaluate(template, locals = {}, options = {}) require "execjs" - context = ExecJS.compile("var evaluate = #{compile(template, options)}") + context = ExecJS.compile(<<-JS) + #{escape_function} + + var template = #{compile(template, options)} + var evaluate = function(locals) { + return template(locals, escape); + } + JS context.call("evaluate", locals) end protected - def js_escape!(source) - source.gsub!(JS_ESCAPE_PATTERN) { |match| '\\' + JS_ESCAPES[match] } - source + + def default(options) + options = DEFAULTS.merge(options) + + [:open_tag_modifiers, :close_tag_modifiers].each do |k| + DEFAULTS[k].each do |sk, v| + next if options[k].has_key?(sk) + options[k] = v + end + end + + options end - def js_unescape!(source) - source.gsub!(JS_UNESCAPE_PATTERN) { |match| JS_UNESCAPES[match[1..-1]] } - source + def escape_module + escape_function.sub('function', 'export function') end - def replace_escape_tags!(source, options) - source.gsub!(options[:escape_pattern] || escape_pattern) do - "',(''+#{js_unescape!($1)})#{escape_function},'" - end + def escape_function(name='escape') + <<-JS + function #{name}(string) { + if (string !== undefined && string != null) { + return String(string).replace(/[&<>'"\\/]/g, function (c) { + return '&#' + c.codePointAt(0) + ';'; + }); + } else { + return ''; + } + } + JS + end + + def chars_balanced?(str, chars) + a = chars[0] + b = chars[1] + str = str.sub(/"(\\.|[^"])+"/, '') + str = str.sub(/'(\\.|[^'])+'/, '') + a_count = str.scan(/#{a}/).length + b_count = str.scan(/#{b}/).length + + a_count - b_count end + - def replace_interpolation_with_subtemplate_tags!(source, options) - regex = options[:interpolation_with_subtemplate_pattern] || interpolation_with_subtemplate_pattern - source.gsub!(regex) do |str| - match_data = regex.match(str) - lines = [] - matches = [match_data[:start], match_data[:middle], match_data[:end]] - - replace_escape_tags!(matches[1], options) - replace_interpolation_with_subtemplate_tags!(matches[1], options) - replace_interpolation_tags!(matches[1], options) - replace_evaluation_tags!(matches[1], options) - - lines << "', #{js_unescape!(matches[0])}" - lines << <<-EJS - var __p = []; - var print = function() { __p.push.apply(__p,arguments); }; - - __p.push('#{matches[1]}'); - - return __p.join(''); - EJS - lines << "#{js_unescape!(matches[2])},'" - - lines.join("\n") + def digest(source, options) + open_tag_count = 0 + close_tag_count = 0 + tag_length = nil + # var index, tagType, tagModifier, tagModifiers, matchingModifier, prefix; + index = nil + tag_type = nil + tag_modifiers = nil + tag_modifier = nil + prefix =nil + matching_modifier = nil + last_tag_modifier = nil + next_open_index = source.index(options[:open_tag]) + next_close_index = source.index(options[:close_tag]) + + while next_open_index || next_close_index + if (next_close_index && (!next_open_index || next_close_index < next_open_index)) + index = next_close_index + tag_type = :close + tag_length = options[:close_tag].length + tag_modifiers = options[:close_tag_modifiers] + close_tag_count += 1 + matching_modifier = tag_modifiers.find do |k, v| + source[index - v.length, v.length] == v + end + else + index = next_open_index + tag_type = :open + tag_length = options[:open_tag].length + tag_modifiers = options[:open_tag_modifiers] + open_tag_count += 1 + matching_modifier = tag_modifiers.find do |k, v| + source[index + tag_length, v.length] == v + end + end + + if matching_modifier + tag_length += matching_modifier[1].length + tag_modifier = matching_modifier[0] + else + tag_modifier = :default + end + + if tag_modifier == :literal + if tag_type == :open + source = source[0, tag_length - matching_modifier[1].length] + source[(index + tag_length)..-1] + # source = source.slice(0, index + tagLength - matchingModifier[1].length) + source.slice(index + tagLength); + open_tag_count -= 1 + else + close_tag_count -= 1 + if index == 0 + source = source[(index + matching_modifier[1].length)..-1] + else + source = source[0..index] + source[(index + matching_modifier[1].length)..-1] + end + end + + next_open_index = source.index(options.openTag, index + tag_length - matching_modifier[1].length); + next_close_index = source.index(options.closeTag, index + tag_length - matching_modifier[1].length); + next + end + + if index != 0 + if tag_type == :close + if matching_modifier + yield(source[0...(matching_modifier[1].length)], :js, last_tag_modifier) + else + yield(source[0...index], :js, last_tag_modifier) + end + else + yield(source[0...index], :text, last_tag_modifier) + end + + source = source[index..-1] + end + + if tag_type == :close && matching_modifier + source = source[(tag_length - matching_modifier[1].length)..-1] + source.lstrip! + else + source = source[tag_length..-1] + end + next_open_index = source.index(options[:open_tag]) + next_close_index = source.index(options[:close_tag]) + last_tag_modifier = tag_modifier end - end - def replace_evaluation_tags!(source, options) - source.gsub!(options[:evaluation_pattern] || evaluation_pattern) do - "'); #{js_unescape!($1)}; __p.push('" + if open_tag_count != close_tag_count + raise "Could not find closing tag for \"#{options[(tag_type.to_s + '_tag').to_sym]}\"." end + + yield(source, :text, tag_modifier) end - def replace_interpolation_tags!(source, options) - source.gsub!(options[:interpolation_pattern] || interpolation_pattern) do - "', #{js_unescape!($1)},'" + + def function_source(source, options) + stack = [] + output = " var __output = [], __append = __output.push.bind(__output);\n" + output << " with (locals || {}) {\n" + + digest(source, options) do |segment, type, modifier| + if type == :js + if segment.match(/\A\s*\}/m) + case stack.pop + when :escape + output << "\n return __output.join(\"\");\n" + output << segment << " ));\n" + when :unescape + output << "\n return __output.join(\"\");\n" + output << segment << " );\n" + else + output << " " << segment << "\n" + end + elsif segment.match(/\)\s*\{\s*\Z/m) + stack << modifier + case modifier + when :escape + output << " __append(escape(" << segment + output << "\n var __output = [], __append = __output.push.bind(__output);\n" + when :unescape + output << " __append(" << segment + output << "\n var __output = [], __append = __output.push.bind(__output);\n" + else + output << " " << segment << "\n" + end + else + case modifier + when :escape + output << " __append(escape(" << segment << "));\n" + when :unescape + output << " __append(" << segment << ");\n" + else + output << " " << segment << "\n" + end + end + elsif segment.length > 0 + output << " __append(`" + segment.gsub("\\"){"\\\\"}.gsub(/\n/, '\\n').gsub(/\r/, '\\r') + "`);\n" + end end - end - def escape_function - ".replace(/&/g, '&')" + - ".replace(//g, '>')" + - ".replace(/\"/g, '"')" + - ".replace(/'/g, ''')" + - ".replace(/\\//g,'/')" + output << " }\n" + output << " return __output.join(\"\");\n" + + output end + end - - self.evaluation_pattern = /<%([\s\S]+?)%>/ - self.interpolation_pattern = /<%=([\s\S]+?)%>/ - self.escape_pattern = /<%-([\s\S]+?)%>/ - self.interpolation_with_subtemplate_pattern = %r{ - <%=(?(?:(?!%>)[\s\S])+\{)\s*%> - (? - ( - (? - <%=?(?:(?!%>)[\s\S])+\{\s*%> - (?: - <%\s*\}(?:(?!\{\s*%>)[\s\S])+\{\s*%> - | - \g - | - .{1} - )*? - <%\s*\}[^\{]+?%> - ) - | - .{1} - )*? - ) - <%\s*(?\}[\s\S]+?)%> - }xm end diff --git a/test/compilation_test.rb b/test/compilation_test.rb index fa64746..76c83fe 100644 --- a/test/compilation_test.rb +++ b/test/compilation_test.rb @@ -7,14 +7,16 @@ class CompilationTest < Minitest::Test assert_match FUNCTION_PATTERN, result assert_no_match(/Hello \<%= name %\>/, result) - end - - test "compile with custom syntax" do - standard_result = EJS.compile("Hello <%= name %>") - braced_result = EJS.compile("Hello {{= name }}", BRACE_SYNTAX) - - assert_match FUNCTION_PATTERN, braced_result - assert_equal standard_result, braced_result + assert_equal(<<~JS.strip, result) + function(locals, escape) { + var __output = [], __append = __output.push.bind(__output); + with (locals || {}) { + __append(`Hello `); + __append(escape( name )); + } + return __output.join(""); + } + JS end end diff --git a/test/custom_pattern_test.rb b/test/custom_pattern_test.rb index 8d71475..edbd488 100644 --- a/test/custom_pattern_test.rb +++ b/test/custom_pattern_test.rb @@ -2,29 +2,45 @@ class CustomPatternTest < Minitest::Test - def setup - @original_evaluation_pattern = EJS.evaluation_pattern - @original_interpolation_pattern = EJS.interpolation_pattern - EJS.evaluation_pattern = BRACE_SYNTAX[:evaluation_pattern] - EJS.interpolation_pattern = BRACE_SYNTAX[:interpolation_pattern] - end + test "compile with custom defults" do + old_defaults = EJS::DEFAULTS + EJS::DEFAULTS = { + open_tag: '{{', + close_tag: '}}', + + open_tag_modifiers: { + escape: '=', + unescape: '-', + comment: '#', + literal: '%' + }, - def teardown - EJS.interpolation_pattern = @original_interpolation_pattern - EJS.evaluation_pattern = @original_evaluation_pattern - end + close_tag_modifiers: { + trim: '-', + literal: '%' + }, + + escape: nil + } - test "compile" do result = EJS.compile("Hello {{= name }}") - assert_match FUNCTION_PATTERN, result - assert_no_match(/Hello \{\{= name \}\}/, result) + assert_equal(<<~JS.strip, result) + function(locals, escape) { + var __output = [], __append = __output.push.bind(__output); + with (locals || {}) { + __append(`Hello `); + __append(escape( name )); + } + return __output.join(""); + } + JS + ensure + EJS::DEFAULTS = old_defaults end test "compile with custom syntax" do - standard_result = EJS.compile("Hello {{= name }}") - question_result = EJS.compile("Hello ", QUESTION_MARK_SYNTAX) - - assert_match FUNCTION_PATTERN, question_result + standard_result = EJS.compile("Hello <%= name %>") + question_result = EJS.compile("Hello ", open_tag: '') assert_equal standard_result, question_result end -end +end \ No newline at end of file diff --git a/test/evaluation_test.rb b/test/evaluation_test.rb index baf6eda..5defd12 100644 --- a/test/evaluation_test.rb +++ b/test/evaluation_test.rb @@ -4,16 +4,16 @@ class EvaluationTest < Minitest::Test test "quotes" do template = "<%= thing %> is gettin' on my noives!" - assert_equal "This is gettin' on my noives!", EJS.evaluate(template, :thing => "This") + assert_equal "This is gettin' on my noives!", EJS.evaluate(template, thing: "This") end test "backslashes" do template = "<%= thing %> is \\ridanculous" - assert_equal "This is \\ridanculous", EJS.evaluate(template, :thing => "This") + assert_equal "This is \\ridanculous", EJS.evaluate(template, thing: "This") end test "backslashes into interpolation" do - template = %q{<%= "Hello \"World\"" %>} + template = %q{<%- "Hello \"World\"" %>} assert_equal 'Hello "World"', EJS.evaluate(template) end @@ -26,7 +26,7 @@ class EvaluationTest < Minitest::Test template = "
    <% for (var i = 0; i < people.length; i++) { %>
  • <%= people[i] %>
  • <% } %>
" - result = EJS.evaluate(template, :people => ["Moe", "Larry", "Curly"]) + result = EJS.evaluate(template, people: %w[Moe Larry Curly]) assert_equal "
  • Moe
  • Larry
  • Curly
", result end @@ -52,79 +52,18 @@ class EvaluationTest < Minitest::Test assert_equal "This\n\t\tis: that.\n\tok.\nend.", EJS.evaluate(template, :x => "that") end - - test "braced iteration" do - template = "
    {{ for (var i = 0; i < people.length; i++) { }}
  • {{= people[i] }}
  • {{ } }}
" - result = EJS.evaluate(template, { :people => ["Moe", "Larry", "Curly"] }, BRACE_SYNTAX) - assert_equal "
  • Moe
  • Larry
  • Curly
", result - end - - test "braced quotes" do - template = "It's its, not it's" - assert_equal template, EJS.evaluate(template, {}, BRACE_SYNTAX) - end - - test "braced quotes in statement and body" do - template = "{{ if(foo == 'bar'){ }}Statement quotes and 'quotes'.{{ } }}" - assert_equal "Statement quotes and 'quotes'.", EJS.evaluate(template, { :foo => "bar" }, BRACE_SYNTAX) - end - - - test "question-marked iteration" do - template = "
" - result = EJS.evaluate(template, { :people => ["Moe", "Larry", "Curly"] }, QUESTION_MARK_SYNTAX) - assert_equal "
  • Moe
  • Larry
  • Curly
", result - end - - test "question-marked quotes" do - template = "It's its, not it's" - assert_equal template, EJS.evaluate(template, {}, QUESTION_MARK_SYNTAX) - end - - test "question-marked quote in statement and body" do - template = "Statement quotes and 'quotes'." - assert_equal "Statement quotes and 'quotes'.", EJS.evaluate(template, { :foo => "bar" }, QUESTION_MARK_SYNTAX) - end - test "escaping" do - template = "<%- foobar %>" - assert_equal "<b>Foo Bar</b>", EJS.evaluate(template, { :foobar => "Foo Bar" }) + template = "<%= foobar %>" + assert_equal "<b>Foo Bar</b>", EJS.evaluate(template, foobar: "Foo Bar") - template = "<%- foobar %>" - assert_equal "Foo & Bar", EJS.evaluate(template, { :foobar => "Foo & Bar" }) + template = "<%= foobar %>" + assert_equal "Foo & Bar", EJS.evaluate(template, { :foobar => "Foo & Bar" }) - template = "<%- foobar %>" - assert_equal ""Foo Bar"", EJS.evaluate(template, { :foobar => '"Foo Bar"' }) + template = "<%= foobar %>" + assert_equal ""Foo Bar"", EJS.evaluate(template, { :foobar => '"Foo Bar"' }) - template = "<%- foobar %>" - assert_equal "'Foo Bar'", EJS.evaluate(template, { :foobar => "'Foo Bar'" }) + template = "<%= foobar %>" + assert_equal "'Foo Bar'", EJS.evaluate(template, { :foobar => "'Foo Bar'" }) end - test "braced escaping" do - template = "{{- foobar }}" - assert_equal "<b>Foo Bar</b>", EJS.evaluate(template, { :foobar => "Foo Bar" }, BRACE_SYNTAX) - - template = "{{- foobar }}" - assert_equal "Foo & Bar", EJS.evaluate(template, { :foobar => "Foo & Bar" }, BRACE_SYNTAX) - - template = "{{- foobar }}" - assert_equal ""Foo Bar"", EJS.evaluate(template, { :foobar => '"Foo Bar"' }, BRACE_SYNTAX) - - template = "{{- foobar }}" - assert_equal "'Foo Bar'", EJS.evaluate(template, { :foobar => "'Foo Bar'" }, BRACE_SYNTAX) - end - - test "question-mark escaping" do - template = "" - assert_equal "<b>Foo Bar</b>", EJS.evaluate(template, { :foobar => "Foo Bar" }, QUESTION_MARK_SYNTAX) - - template = "" - assert_equal "Foo & Bar", EJS.evaluate(template, { :foobar => "Foo & Bar" }, QUESTION_MARK_SYNTAX) - - template = "" - assert_equal ""Foo Bar"", EJS.evaluate(template, { :foobar => '"Foo Bar"' }, QUESTION_MARK_SYNTAX) - - template = "" - assert_equal "'Foo Bar'", EJS.evaluate(template, { :foobar => "'Foo Bar'" }, QUESTION_MARK_SYNTAX) - end end \ No newline at end of file diff --git a/test/subtemplate_test.rb b/test/subtemplate_test.rb index dab644a..d0a27cd 100644 --- a/test/subtemplate_test.rb +++ b/test/subtemplate_test.rb @@ -6,11 +6,11 @@ class SubtemplateTest < Minitest::Test template = <<-DATA <% formTag = function(template) { return '
\\n'+template()+'\\n
'; } %> - <%= formTag(function () { %> + <%- formTag(function () { %> <% }) %> DATA - + assert_equal <<-DATA, EJS.evaluate(template) From e2f0a879fc5ae45a5e36a91cf84e2c30db014927 Mon Sep 17 00:00:00 2001 From: Jon Bracy Date: Wed, 11 Jul 2018 11:07:18 -0700 Subject: [PATCH 12/16] Rename gem --- .gitignore | 2 ++ Gemfile.lock | 4 ++-- lib/ejs.rb | 2 +- lib/ruby/ejs.rb | 1 + lib/{ => ruby/ejs}/assets/ejs.js | 0 ejs.gemspec => ruby-ejs.gemspec | 13 +++++++------ 6 files changed, 13 insertions(+), 9 deletions(-) create mode 100644 .gitignore create mode 100644 lib/ruby/ejs.rb rename lib/{ => ruby/ejs}/assets/ejs.js (100%) rename ejs.gemspec => ruby-ejs.gemspec (68%) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f1d7f5e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.gem +.DS_Store diff --git a/Gemfile.lock b/Gemfile.lock index b949c3d..efc4005 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - ejs (2.0.0) + ruby-ejs (1.0.0) GEM remote: https://rubygems.org/ @@ -23,11 +23,11 @@ PLATFORMS DEPENDENCIES bundler - ejs! execjs minitest minitest-reporters rake + ruby-ejs! BUNDLED WITH 1.16.0 diff --git a/lib/ejs.rb b/lib/ejs.rb index 749fa89..cbdb59e 100644 --- a/lib/ejs.rb +++ b/lib/ejs.rb @@ -19,7 +19,7 @@ module EJS escape: nil } - ASSET_DIR = File.join(__dir__, 'assets') + ASSET_DIR = File.join(__dir__, 'ruby', 'ejs', 'assets') class << self diff --git a/lib/ruby/ejs.rb b/lib/ruby/ejs.rb new file mode 100644 index 0000000..4ddb7e9 --- /dev/null +++ b/lib/ruby/ejs.rb @@ -0,0 +1 @@ +require File.expand_path(File.join(__dir__, '..', 'ejs')) \ No newline at end of file diff --git a/lib/assets/ejs.js b/lib/ruby/ejs/assets/ejs.js similarity index 100% rename from lib/assets/ejs.js rename to lib/ruby/ejs/assets/ejs.js diff --git a/ejs.gemspec b/ruby-ejs.gemspec similarity index 68% rename from ejs.gemspec rename to ruby-ejs.gemspec index 2811ec5..e511299 100644 --- a/ejs.gemspec +++ b/ruby-ejs.gemspec @@ -1,6 +1,7 @@ Gem::Specification.new do |s| - s.name = "ejs" - s.version = "2.0.0" + s.name = "ruby-ejs" + s.version = "1.0.0" + s.licenses = ['MIT'] s.summary = "EJS (Embedded JavaScript) template compiler" s.description = "Compile EJS (Embedded JavaScript) templates in Ruby." @@ -12,7 +13,7 @@ Gem::Specification.new do |s| s.add_development_dependency "minitest-reporters" s.add_development_dependency "execjs" - s.authors = ["Sam Stephenson"] - s.email = ["sstephenson@gmail.com"] - s.homepage = "https://github.com/sstephenson/ruby-ejs/" -end + s.authors = ["Jonathan Bracy"] + s.email = ["jonbracy@gmail.com"] + s.homepage = "https://github.com/malomalo/ruby-ejs" +end \ No newline at end of file From 6cbf420a415d0c42ff73956eaed8b07f380fc525 Mon Sep 17 00:00:00 2001 From: Jon Bracy Date: Wed, 11 Jul 2018 13:39:22 -0700 Subject: [PATCH 13/16] include js file in gem --- ruby-ejs.gemspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ruby-ejs.gemspec b/ruby-ejs.gemspec index e511299..03afa7d 100644 --- a/ruby-ejs.gemspec +++ b/ruby-ejs.gemspec @@ -1,11 +1,11 @@ Gem::Specification.new do |s| s.name = "ruby-ejs" - s.version = "1.0.0" + s.version = "1.0.1" s.licenses = ['MIT'] s.summary = "EJS (Embedded JavaScript) template compiler" s.description = "Compile EJS (Embedded JavaScript) templates in Ruby." - s.files = Dir["README.md", "LICENSE", "lib/**/*.rb"] + s.files = Dir["README.md", "LICENSE", "lib/**/*.{rb,js}"] s.add_development_dependency "rake" s.add_development_dependency "bundler" From f31fdc99549f92c095fe78f96a207a2c6494d2ea Mon Sep 17 00:00:00 2001 From: Jon Bracy Date: Tue, 4 Sep 2018 11:01:02 -0700 Subject: [PATCH 14/16] Add support for imports in EJS files --- Gemfile.lock | 2 +- lib/ejs.rb | 24 +++++++++++++++++------- ruby-ejs.gemspec | 2 +- test/import_test.rb | 43 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 9 deletions(-) create mode 100644 test/import_test.rb diff --git a/Gemfile.lock b/Gemfile.lock index efc4005..d68b861 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - ruby-ejs (1.0.0) + ruby-ejs (1.1.0) GEM remote: https://rubygems.org/ diff --git a/lib/ejs.rb b/lib/ejs.rb index cbdb59e..f84a13f 100644 --- a/lib/ejs.rb +++ b/lib/ejs.rb @@ -42,8 +42,10 @@ def transform(source, options = {}) "import {escape} from 'ejs';\n" end + fs = function_source(source, options) + output << fs[1] output << "export default function (locals) {\n" - output << function_source(source, options) + output << fs[0] output << "}" output @@ -53,7 +55,7 @@ def compile(source, options = {}) options = default(options) output = "function(locals, escape) {\n" - output << function_source(source, options) + output << function_source(source, options)[0] output << "}" output end @@ -222,8 +224,9 @@ def digest(source, options) def function_source(source, options) stack = [] + imports = "" output = " var __output = [], __append = __output.push.bind(__output);\n" - output << " with (locals || {}) {\n" + output << " with (locals || {}) {\n" unless options[:strict] digest(source, options) do |segment, type, modifier| if type == :js @@ -257,7 +260,13 @@ def function_source(source, options) when :unescape output << " __append(" << segment << ");\n" else - output << " " << segment << "\n" + if segment =~ /\A\s*import/ + imports << segment.strip + imports << ';' unless segment =~ /;\s*\Z/ + imports << "\n" + else + output << " " << segment << "\n" + end end end elsif segment.length > 0 @@ -265,10 +274,11 @@ def function_source(source, options) end end - output << " }\n" + output << " }\n" unless options[:strict] output << " return __output.join(\"\");\n" - - output + imports << "\n" + + [output, imports] end end diff --git a/ruby-ejs.gemspec b/ruby-ejs.gemspec index 03afa7d..db9b02c 100644 --- a/ruby-ejs.gemspec +++ b/ruby-ejs.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = "ruby-ejs" - s.version = "1.0.1" + s.version = "1.1.0" s.licenses = ['MIT'] s.summary = "EJS (Embedded JavaScript) template compiler" s.description = "Compile EJS (Embedded JavaScript) templates in Ruby." diff --git a/test/import_test.rb b/test/import_test.rb new file mode 100644 index 0000000..8216bbf --- /dev/null +++ b/test/import_test.rb @@ -0,0 +1,43 @@ +require 'test_helper' + +class ImportTest < Minitest::Test + + test "quotes" do + template = <<-DATA + <% import x from y %> + <% import a from z %> + + <% formTag = function(template) { return '
\\n'+template()+'\\n
'; } %> + + <%- formTag(function () { %> + + <% }) %> + DATA + + assert_equal <<~DATA.strip, EJS.transform(template) + import {escape} from 'ejs'; + import x from y; + import a from z; + + export default function (locals) { + var __output = [], __append = __output.push.bind(__output); + with (locals || {}) { + __append(` `); + __append(`\\n `); + __append(`\\n \\n `); + formTag = function(template) { return '
\\n'+template()+'\\n
'; } + __append(`\\n\\n `); + __append( formTag(function () { + var __output = [], __append = __output.push.bind(__output); + __append(`\\n \\n `); + + return __output.join(""); + }) ); + __append(`\\n`); + } + return __output.join(""); + } + DATA + end + +end \ No newline at end of file From 15c80e43ebf1e70d46c3991492d0cf2d7cf5192b Mon Sep 17 00:00:00 2001 From: Jon Bracy Date: Fri, 14 Sep 2018 10:50:39 -0700 Subject: [PATCH 15/16] Export a es5 syntax function --- lib/ejs.rb | 2 +- test/compilation_test.rb | 2 +- test/custom_pattern_test.rb | 2 +- test/import_test.rb | 12 ++++++------ 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/ejs.rb b/lib/ejs.rb index f84a13f..3a23ecd 100644 --- a/lib/ejs.rb +++ b/lib/ejs.rb @@ -270,7 +270,7 @@ def function_source(source, options) end end elsif segment.length > 0 - output << " __append(`" + segment.gsub("\\"){"\\\\"}.gsub(/\n/, '\\n').gsub(/\r/, '\\r') + "`);\n" + output << ' __append("' + segment.gsub("\\"){"\\\\"}.gsub(/\n/, '\\n').gsub(/\r/, '\\r').gsub('"', '\\"') + "\");\n" end end diff --git a/test/compilation_test.rb b/test/compilation_test.rb index 76c83fe..96ec7fb 100644 --- a/test/compilation_test.rb +++ b/test/compilation_test.rb @@ -11,7 +11,7 @@ class CompilationTest < Minitest::Test function(locals, escape) { var __output = [], __append = __output.push.bind(__output); with (locals || {}) { - __append(`Hello `); + __append("Hello "); __append(escape( name )); } return __output.join(""); diff --git a/test/custom_pattern_test.rb b/test/custom_pattern_test.rb index edbd488..40564bc 100644 --- a/test/custom_pattern_test.rb +++ b/test/custom_pattern_test.rb @@ -28,7 +28,7 @@ class CustomPatternTest < Minitest::Test function(locals, escape) { var __output = [], __append = __output.push.bind(__output); with (locals || {}) { - __append(`Hello `); + __append("Hello "); __append(escape( name )); } return __output.join(""); diff --git a/test/import_test.rb b/test/import_test.rb index 8216bbf..bbb675d 100644 --- a/test/import_test.rb +++ b/test/import_test.rb @@ -22,18 +22,18 @@ class ImportTest < Minitest::Test export default function (locals) { var __output = [], __append = __output.push.bind(__output); with (locals || {}) { - __append(` `); - __append(`\\n `); - __append(`\\n \\n `); + __append(" "); + __append("\\n "); + __append("\\n \\n "); formTag = function(template) { return '
\\n'+template()+'\\n
'; } - __append(`\\n\\n `); + __append("\\n\\n "); __append( formTag(function () { var __output = [], __append = __output.push.bind(__output); - __append(`\\n \\n `); + __append("\\n \\n "); return __output.join(""); }) ); - __append(`\\n`); + __append("\\n"); } return __output.join(""); } From b10a89a5119da82c7d28f5114d7bb4fa8cfdf27c Mon Sep 17 00:00:00 2001 From: Jon Bracy Date: Mon, 5 Nov 2018 11:08:00 -0800 Subject: [PATCH 16/16] Fix issue with subtemplates --- lib/ejs.rb | 4 +++- ruby-ejs.gemspec | 2 +- test/subtemplate_test.rb | 27 +++++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/lib/ejs.rb b/lib/ejs.rb index 3a23ecd..140ae90 100644 --- a/lib/ejs.rb +++ b/lib/ejs.rb @@ -230,7 +230,9 @@ def function_source(source, options) digest(source, options) do |segment, type, modifier| if type == :js - if segment.match(/\A\s*\}/m) + if segment.match(/\A\s*\}.*\{\s*\Z/m) + output << " " << segment << "\n" + elsif segment.match(/\A\s*\}/m) case stack.pop when :escape output << "\n return __output.join(\"\");\n" diff --git a/ruby-ejs.gemspec b/ruby-ejs.gemspec index db9b02c..a08b3e9 100644 --- a/ruby-ejs.gemspec +++ b/ruby-ejs.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = "ruby-ejs" - s.version = "1.1.0" + s.version = "1.2.0" s.licenses = ['MIT'] s.summary = "EJS (Embedded JavaScript) template compiler" s.description = "Compile EJS (Embedded JavaScript) templates in Ruby." diff --git a/test/subtemplate_test.rb b/test/subtemplate_test.rb index d0a27cd..ceae0b6 100644 --- a/test/subtemplate_test.rb +++ b/test/subtemplate_test.rb @@ -22,4 +22,31 @@ class SubtemplateTest < Minitest::Test DATA end + test "with an else" do + template = <<-DATA + <% formTag = function(template) { return '
\\n'+template()+'\\n
'; } %> + + <%- formTag(function (f) { %> + <% if (true) { %> + yes + <% } else { %> + no + <% } %> + <% }) %> + DATA + + assert_equal <<-DATA, EJS.evaluate(template) + + +
+ + + yes + + +
+ DATA + end + + end \ No newline at end of file