├── test ├── _partial.html.string ├── main.html.string ├── test_helper.rb └── string_template_test.rb ├── lib ├── string_template.rb └── string_template │ ├── handler.rb │ └── railtie.rb ├── .gitignore ├── bin ├── setup └── console ├── Rakefile ├── Gemfile ├── benchmark.rb ├── MIT-LICENSE ├── hello.string ├── string_template.gemspec ├── hello.erb ├── .github └── workflows │ └── main.yml └── README.md /test/_partial.html.string: -------------------------------------------------------------------------------- 1 | #{ 'world!' } 2 | -------------------------------------------------------------------------------- /test/main.html.string: -------------------------------------------------------------------------------- 1 | hello, #{ render('partial') } 2 | -------------------------------------------------------------------------------- /lib/string_template.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'string_template/handler' 4 | require_relative 'string_template/railtie' 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | Gemfile.lock 10 | gemfiles/*.lock 11 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.libs << "lib" 7 | t.test_files = FileList["test/**/*_test.rb"] 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /lib/string_template/handler.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module StringTemplate 4 | class Handler 5 | def self.call(template, source = nil) 6 | "%Q\0#{source || template.source}\0" 7 | end 8 | 9 | def self.handles_encoding? 10 | true 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/string_template/railtie.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module StringTemplate 4 | class Railtie < ::Rails::Railtie 5 | initializer 'string_template' do 6 | ActiveSupport.on_load :action_view do 7 | ActionView::Template.register_template_handler :string, StringTemplate::Handler 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__) 4 | 5 | # require logger before requiring rails, or Rails 6 fails to boot 6 | require 'logger' 7 | require 'rails' 8 | require 'action_view' 9 | require "string_template" 10 | StringTemplate::Railtie.run_initializers 11 | require 'action_view/base' 12 | 13 | require "minitest/autorun" 14 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "string_template" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify your gem's dependencies in string_template.gemspec 6 | gemspec 7 | 8 | if ENV['RAILS_VERSION'] == 'edge' 9 | gem 'rails', git: 'https://github.com/rails/rails.git' 10 | elsif ENV['RAILS_VERSION'] 11 | gem 'rails', "~> #{ENV['RAILS_VERSION']}.0" 12 | end 13 | 14 | gem 'logger' if RUBY_VERSION >= '3.5' 15 | gem 'bigdecimal' if RUBY_VERSION >= '3.4' 16 | gem 'mutex_m' if RUBY_VERSION >= '3.4' 17 | -------------------------------------------------------------------------------- /benchmark.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'benchmark_driver' 4 | 5 | Benchmark.driver do |x| 6 | x.prelude %{ 7 | require 'rails' 8 | require 'action_view' 9 | require 'string_template' 10 | StringTemplate::Railtie.run_initializers 11 | require 'action_view/base' 12 | 13 | (view = Class.new(ActionView::Base).new(ActionView::LookupContext.new(''))).instance_variable_set(:@world, 'world!') 14 | } 15 | x.report 'erb', %{ view.render(template: 'hello', handlers: 'erb') } 16 | x.report 'string', %{ view.render(template: 'hello', handlers: 'string') } 17 | end 18 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Akira Matsuda 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /hello.string: -------------------------------------------------------------------------------- 1 | hello, #{ @world } 2 | hello, #{ @world } 3 | hello, #{ @world } 4 | hello, #{ @world } 5 | hello, #{ @world } 6 | hello, #{ @world } 7 | hello, #{ @world } 8 | hello, #{ @world } 9 | hello, #{ @world } 10 | hello, #{ @world } 11 | hello, #{ @world } 12 | hello, #{ @world } 13 | hello, #{ @world } 14 | hello, #{ @world } 15 | hello, #{ @world } 16 | hello, #{ @world } 17 | hello, #{ @world } 18 | hello, #{ @world } 19 | hello, #{ @world } 20 | hello, #{ @world } 21 | hello, #{ @world } 22 | hello, #{ @world } 23 | hello, #{ @world } 24 | hello, #{ @world } 25 | hello, #{ @world } 26 | hello, #{ @world } 27 | hello, #{ @world } 28 | hello, #{ @world } 29 | hello, #{ @world } 30 | hello, #{ @world } 31 | hello, #{ @world } 32 | hello, #{ @world } 33 | hello, #{ @world } 34 | hello, #{ @world } 35 | hello, #{ @world } 36 | hello, #{ @world } 37 | hello, #{ @world } 38 | hello, #{ @world } 39 | hello, #{ @world } 40 | hello, #{ @world } 41 | hello, #{ @world } 42 | hello, #{ @world } 43 | hello, #{ @world } 44 | hello, #{ @world } 45 | hello, #{ @world } 46 | hello, #{ @world } 47 | hello, #{ @world } 48 | hello, #{ @world } 49 | hello, #{ @world } 50 | hello, #{ @world } 51 | -------------------------------------------------------------------------------- /string_template.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path("../lib", __FILE__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | 4 | Gem::Specification.new do |spec| 5 | spec.name = "string_template" 6 | spec.version = '0.2.1' 7 | spec.authors = ["Akira Matsuda"] 8 | spec.email = ["ronnie@dio.jp"] 9 | 10 | spec.summary = "A template engine for Rails, focusing on speed, using Ruby's String interpolation syntax" 11 | spec.description = %]string_template is a Rails plugin that adds an Action View handler for .string template that accepts Ruby's String literal that uses #{} notation for interpolating dynamic variables] 12 | spec.homepage = 'https://github.com/amatsuda/string_template' 13 | spec.license = "MIT" 14 | 15 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 16 | f.match(%r{^(test|spec|features)/}) 17 | end 18 | spec.bindir = "exe" 19 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 20 | spec.require_paths = ["lib"] 21 | 22 | spec.add_dependency 'rails' 23 | 24 | spec.add_development_dependency 'bundler' 25 | spec.add_development_dependency 'rake' 26 | spec.add_development_dependency 'minitest' 27 | spec.add_development_dependency 'benchmark_driver', '>= 0.9.0' 28 | end 29 | -------------------------------------------------------------------------------- /hello.erb: -------------------------------------------------------------------------------- 1 | hello, <%== @world %> 2 | hello, <%== @world %> 3 | hello, <%== @world %> 4 | hello, <%== @world %> 5 | hello, <%== @world %> 6 | hello, <%== @world %> 7 | hello, <%== @world %> 8 | hello, <%== @world %> 9 | hello, <%== @world %> 10 | hello, <%== @world %> 11 | hello, <%== @world %> 12 | hello, <%== @world %> 13 | hello, <%== @world %> 14 | hello, <%== @world %> 15 | hello, <%== @world %> 16 | hello, <%== @world %> 17 | hello, <%== @world %> 18 | hello, <%== @world %> 19 | hello, <%== @world %> 20 | hello, <%== @world %> 21 | hello, <%== @world %> 22 | hello, <%== @world %> 23 | hello, <%== @world %> 24 | hello, <%== @world %> 25 | hello, <%== @world %> 26 | hello, <%== @world %> 27 | hello, <%== @world %> 28 | hello, <%== @world %> 29 | hello, <%== @world %> 30 | hello, <%== @world %> 31 | hello, <%== @world %> 32 | hello, <%== @world %> 33 | hello, <%== @world %> 34 | hello, <%== @world %> 35 | hello, <%== @world %> 36 | hello, <%== @world %> 37 | hello, <%== @world %> 38 | hello, <%== @world %> 39 | hello, <%== @world %> 40 | hello, <%== @world %> 41 | hello, <%== @world %> 42 | hello, <%== @world %> 43 | hello, <%== @world %> 44 | hello, <%== @world %> 45 | hello, <%== @world %> 46 | hello, <%== @world %> 47 | hello, <%== @world %> 48 | hello, <%== @world %> 49 | hello, <%== @world %> 50 | hello, <%== @world %> 51 | -------------------------------------------------------------------------------- /test/string_template_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class StringTemplateTest < Minitest::Test 6 | def setup 7 | @view = if ActionView::VERSION::MAJOR >= 6 8 | Class.new(ActionView::Base.with_empty_template_cache).with_view_paths([__dir__]) 9 | else 10 | Class.new(ActionView::Base).new(ActionView::LookupContext.new(__dir__)) 11 | end 12 | super 13 | end 14 | 15 | def assert_render(expected, template, options = {}) 16 | result = @view.render(options.merge(inline: template, type: 'string')) 17 | assert_equal expected, result 18 | end 19 | 20 | def test_no_interpolation 21 | assert_render 'hello', 'hello' 22 | end 23 | 24 | def test_basic_interpolation 25 | assert_render 'hello', '#{"hello"}' 26 | end 27 | 28 | def test_ivar 29 | @view.instance_variable_set :@foo, 'hello' 30 | assert_render 'hello', '#{@foo}' 31 | end 32 | 33 | def test_if 34 | assert_render 'hello', '#{if true;"hello";end}' 35 | assert_render 'hello', '#{"hello" if true}' 36 | end 37 | 38 | def test_each_as_map_and_join 39 | assert_render "hello\nhello\nhello", 40 | '#{[*1..3].map do 41 | "hello" 42 | end.join("\n")}' 43 | end 44 | 45 | def test_each_with_object 46 | assert_render "hello\nhello\nhello\n", 47 | '#{[*1..3].each_with_object("".dup) do |i, s| 48 | s << "hello\n" 49 | end}' 50 | end 51 | 52 | def test_locals 53 | assert_render 'hello', '#{foo}', locals: {foo: 'hello'} 54 | end 55 | 56 | def test_render_partial 57 | result = @view.render(template: 'main', handlers: :string) 58 | assert_equal 'hello, world!', result.strip 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | strategy: 8 | matrix: 9 | ruby_version: [ruby-head, '3.4', '3.3', '3.2', '3.1'] 10 | rails_version: ['edge', '8.0', '7.2', '7.1', '7.0'] 11 | 12 | include: 13 | - ruby_version: '3.0' 14 | rails_version: '7.1' 15 | 16 | - ruby_version: '3.0' 17 | rails_version: '7.0' 18 | - ruby_version: '2.7' 19 | rails_version: '7.0' 20 | 21 | - ruby_version: '3.2' 22 | rails_version: '6.1' 23 | - ruby_version: '3.1' 24 | rails_version: '6.1' 25 | - ruby_version: '3.0' 26 | rails_version: '6.1' 27 | - ruby_version: '2.7' 28 | rails_version: '6.1' 29 | - ruby_version: '2.6' 30 | rails_version: '6.1' 31 | 32 | - ruby_version: '3.0' 33 | rails_version: '6.0' 34 | - ruby_version: '2.7' 35 | rails_version: '6.0' 36 | - ruby_version: '2.6' 37 | rails_version: '6.0' 38 | 39 | - ruby_version: '2.7' 40 | rails_version: '5.2' 41 | - ruby_version: '2.6' 42 | rails_version: '5.2' 43 | 44 | - ruby_version: '2.5' 45 | rails_version: '5.1' 46 | 47 | - ruby_version: '2.5' 48 | rails_version: '5.0' 49 | 50 | - ruby_version: '2.5' 51 | rails_version: '4.2' 52 | 53 | exclude: 54 | - ruby_version: '3.1' 55 | rails_version: edge 56 | 57 | - ruby_version: '3.1' 58 | rails_version: '8.0' 59 | 60 | runs-on: ubuntu-24.04 61 | 62 | env: 63 | RAILS_VERSION: ${{ matrix.rails_version }} 64 | 65 | steps: 66 | - uses: actions/checkout@v3 67 | 68 | - uses: ruby/setup-ruby@v1 69 | with: 70 | ruby-version: ${{ matrix.ruby_version }} 71 | bundler-cache: true 72 | rubygems: ${{ matrix.ruby_version < '2.6' && 'default' || 'latest' }} 73 | bundler: ${{ matrix.rails_version == '4.2' && '1' || 'latest' }} 74 | continue-on-error: ${{ (matrix.ruby_version == 'ruby-head') || (matrix.allow_failures == 'true') }} 75 | 76 | - run: bundle exec rake 77 | continue-on-error: ${{ (matrix.ruby_version == 'ruby-head') || contains(matrix.rails_version, 'edge') || (matrix.allow_failures == 'true') }} 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StringTemplate 2 | 3 | The fastest template engine for Rails. 4 | 5 | 6 | ## Concept 7 | 8 | Ruby's String literal has such a powerful interpolation mechanism. 9 | It's almost a template engine, it's the fastest way to compose a String, and the syntax is already very well known by every Ruby programmer. 10 | Why don't we use this for the view files in our apps? 11 | 12 | 13 | ## Installation 14 | 15 | Add this line to your Rails application's Gemfile: 16 | 17 | ```ruby 18 | gem 'string_template' 19 | ``` 20 | 21 | And then bundle. 22 | 23 | 24 | ## Syntax 25 | 26 | StringTemplate's syntax is based on Ruby's String interpolation. 27 | Plus, you can use Action View features. 28 | 29 | ### Example 30 | Here's an example of a scaffold generated ERB template, and its string\_template version. 31 | 32 | ERB: 33 | ``` 34 |
<%= notice %>
35 | 36 |37 | Title: 38 | <%= @post.title %> 39 |
40 | 41 |42 | Body: 43 | <%= @post.body %> 44 |
45 | 46 | <%= link_to 'Edit', "/posts/#{@post.id}/edit" %> | 47 | <%= link_to 'Back', '/posts' %> 48 | ``` 49 | 50 | string\_template: 51 | ``` 52 |#{h notice }
53 | 54 |55 | Title: 56 | #{h @post.title } 57 |
58 | 59 |60 | Body: 61 | #{h @post.body } 62 |
63 | 64 | #{ link_to 'Edit', "/posts/#{@post.id}/edit" } | 65 | #{ link_to 'Back', '/posts' } 66 | ``` 67 | 68 | ### More Examples 69 | Please take a look at [the tests](https://github.com/amatsuda/string_template/blob/master/test/string_template_test.rb) for actual examples. 70 | 71 | 72 | ## File Names 73 | By default, string\_template renders view files with `.string` extension, e.g. `app/views/posts/show.html.string` 74 | 75 | 76 | ## Security 77 | string\_template does not automatically `html_escape`. Don't forget to explicitly call `h()` when interpolating possibly HTML unsafe strings, like we used to do in pre Rails 3 era. 78 | 79 | 80 | ## So, Should We Rewrite Everything with This? 81 | string\_template may not be the best choice as a general purpose template engine. 82 | It may sometimes be hard to express your template in a simple and maintainable code, especially when the template includes some business logic. 83 | You need to care about security. 84 | So this template engine is recommended to use only for performance hotspots. 85 | For other templates, you might better use your favorite template engine such as haml, or haml, or haml. 86 | 87 | 88 | ## Benchmark 89 | Following is the benchmark result showing how string\_template is faster than ERB (Erubi, to be technically accurate), executed on Ruby trunk (2.6). 90 | This repo includes [this actual benchmarking script](https://github.com/amatsuda/string_template/blob/master/benchmark.rb) so that you can try it on your machine. 91 | 92 | ``` 93 | % ruby benchmark.rb 94 | Warming up -------------------------------------- 95 | erb 993.525 i/100ms 96 | string 1.911k i/100ms 97 | Calculating ------------------------------------- 98 | erb 11.012k i/s - 49.676k in 4.511268s 99 | string 22.029k i/s - 95.529k in 4.336571s 100 | 101 | Comparison: 102 | string: 22028.7 i/s 103 | erb: 11011.5 i/s - 2.00x slower 104 | ``` 105 | 106 | 107 | ## Contributing 108 | 109 | Bug reports and pull requests are welcome on GitHub at https://github.com/amatsuda/string_template. 110 | 111 | ## License 112 | 113 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 114 | --------------------------------------------------------------------------------