Skip to content

Commit

Permalink
Improve pipeline to support bound local variables, locals in exec
Browse files Browse the repository at this point in the history
  • Loading branch information
jaredcwhite committed Nov 19, 2020
1 parent 7702e9b commit 9fa3a06
Show file tree
Hide file tree
Showing 11 changed files with 37 additions and 19 deletions.
2 changes: 0 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,3 @@ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

# Specify your gem's dependencies in serbea.gemspec
gemspec

gem "erubi", github: "jaredcwhite/erubi", branch: "config-literal-prefix"
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Serbea combines the best ideas from "brace-style" template languages such as Liq
* Supports every convention of ERB and builds upon it with new features (which is why it's "awesomer!").
* Builtin frontmatter support so you can access the variables written into the top YAML within your templates. In any Rails view, including layouts, you'll have access to the `@frontmatter` ivar which is a merged `HashWithDotAccess::Hash` with data from any part of the view tree (partials, pages, layout).
* The filters accessible within Serbea templates are either helpers (where the variable gets passed as the first argument) or instance methods of the variable itself, so you can build extremely expressive pipelines that take advantage of the code you already know and love. (For example, in Rails you could write `{{ "My Link" | link_to: route_path }}`).
* The `Serbea::Pipeline.exec` method lets you pass a pipeline template in, along with an optional input value or included helpers module, and you'll get the output as a object of any type (not converted to a string like in traditional templates). For example: `Serbea::Pipeline.exec("[1,2,3] |> map: ->(i) { i * 10 }")` will return `[10, 20, 30]`.
* The `Serbea::Pipeline.exec` method lets you pass a pipeline template in, along with an optional input value or included helpers module, and you'll get the output as a object of any type (not converted to a string like in traditional templates). For example: `Serbea::Pipeline.exec("arr |> map: ->(i) { i * 10 }", arr: [1,2,3])` will return `[10, 20, 30]`.

## What It Looks Like

Expand Down
2 changes: 1 addition & 1 deletion lib/serbea.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class SerbeaTemplate < ErubiTemplate
def prepare
@options.merge!(
outvar: "@_erbout",
bufval: "Serbea::Buffer.new",
bufval: "Serbea::OutputBuffer.new",
literal_prefix: "{%",
literal_postfix: "%}",
engine_class: Serbea::TemplateEngine
Expand Down
9 changes: 5 additions & 4 deletions lib/serbea/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ def self.included(mod)

def capture(obj = nil, &block)
previous_buffer_state = @_erbout
@_erbout = Serbea::Buffer.new
@_erbout = Serbea::OutputBuffer.new

# For compatibility with ActionView, not used by Bridgetown normally
previous_ob_state = @output_buffer
@output_buffer = Serbea::Buffer.new
@output_buffer = Serbea::OutputBuffer.new


result = instance_exec(obj, &block)
if @output_buffer != ""
Expand All @@ -32,11 +33,11 @@ def pipeline(context, value)
def helper(name, &helper_block)
self.class.define_method(name) do |*args, &block|
previous_buffer_state = @_erbout
@_erbout = Serbea::Buffer.new
@_erbout = Serbea::OutputBuffer.new

# For compatibility with ActionView, not used by Bridgetown normally
previous_ob_state = @output_buffer
@output_buffer = Serbea::Buffer.new
@output_buffer = Serbea::OutputBuffer.new

result = helper_block.call(*args, &block)
if @output_buffer != ""
Expand Down
26 changes: 20 additions & 6 deletions lib/serbea/pipeline.rb
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
require "active_support/core_ext/string/output_safety"
require "active_support/core_ext/object/blank"

module Serbea
class Pipeline
def self.exec(template, input: (no_input_passed = true; nil), include_helpers: nil)
def self.exec(template, locals = {}, include_helpers: nil, **kwargs)
anon = Class.new do
include Serbea::Helpers

attr_accessor :input, :output
attr_accessor :output
end

if include_helpers
anon.include include_helpers
end

pipeline_obj = anon.new
pipeline_obj.input = input unless no_input_passed

full_template = "{{ #{template} | assign_to: :output }}"

tmpl = Tilt::SerbeaTemplate.new { full_template }
tmpl.render(pipeline_obj)
tmpl.render(pipeline_obj, locals.presence || kwargs)

pipeline_obj.output
end
Expand Down Expand Up @@ -54,8 +54,9 @@ def self.value_methods_denylist
@value_methods_denylist ||= Set.new
end

def initialize(context, value)
@context = context
def initialize(binding, value)
@binding = binding
@context = binding.receiver
@value = value
end

Expand Down Expand Up @@ -83,6 +84,19 @@ def filter(name, *args, **kwargs)
else
@value = @context.send(name, @value, *args)
end
elsif @binding.local_variables.include?(name)
var = @binding.local_variable_get(name)
if var.respond_to?(:call)
unless kwargs.empty?
@value = var.call(@value, *args, **kwargs)
else
@value = var.call(@value, *args)
end
else
"Serbea warning: Filter #{name} does not respond to call".tap do |warning|
self.class.raise_on_missing_filters ? raise(warning) : STDERR.puts(warning)
end
end
else
"Serbea warning: Filter not found: #{name}".tap do |warning|
self.class.raise_on_missing_filters ? raise(warning) : STDERR.puts(warning)
Expand Down
4 changes: 2 additions & 2 deletions lib/serbea/template_engine.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
require "strscan"

module Serbea
class Buffer < String
class OutputBuffer < String
def concat_to_s(input)
concat input.to_s
end
Expand Down Expand Up @@ -125,7 +125,7 @@ def process_serbea_input(template, properties)
end
end

segments[0] = "pipeline(self, (#{segments[0].strip}))"
segments[0] = "pipeline(binding, (#{segments[0].strip}))"
segments[1..-1].each_with_index do |segment, index|
filter, args = segment.strip.match(/([^ :]*)(.*)/m).captures
segments[index + 1] = ".filter(:" + filter
Expand Down
2 changes: 1 addition & 1 deletion lib/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module Serbea
VERSION = "0.8.1"
VERSION = "0.9.0"
end
2 changes: 1 addition & 1 deletion serbea.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Gem::Specification.new do |spec|
spec.require_paths = ["lib"]

spec.add_runtime_dependency("activesupport", "~> 6.0")
spec.add_runtime_dependency("erubi", "~> 1.9")
spec.add_runtime_dependency("erubi", ">= 1.10")
spec.add_runtime_dependency("hash_with_dot_access", "~> 1.1")
spec.add_runtime_dependency("tilt", "~> 2.0")

Expand Down
3 changes: 3 additions & 0 deletions test/template.serb
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,6 @@ Prepend permanent?
{{ [1, 3, 6] \| [2, 3, 9] | join: "-" | append: " Foo \| bar!" | gsub: /ba\|r/, "x" }}

{{ LambdaTest.new | scope: "lambda test", ->(x) { x * 20 }, ->{} | center: 50 }}

{% use_local_helper = ->(input) { input.downcase } %}
{{ "YO" | use_local_helper }}
2 changes: 1 addition & 1 deletion test/test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def page

def form(classname:)
previous_buffer_state = @_erbout
@_erbout = Serbea::Buffer.new
@_erbout = Serbea::OutputBuffer.new
fields = Fields.new
str = "<form class=\"#{classname}\">"
str << yield(fields)
Expand Down
2 changes: 2 additions & 0 deletions test/test_output.txt
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,5 @@ ha1heha2heha3he
1-3-6-2-9 Foo | xx!

Name: lambda test, output: 200

yo

0 comments on commit 9fa3a06

Please sign in to comment.