Skip to content
Merged
8 changes: 5 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,15 @@ jobs:
strategy:
matrix:
os:
- macos-14
- macos-latest
- ubuntu-22.04
- ubuntu-latest
ruby:
- ruby-2.6
- ruby-2.7
- ruby-3.0
- ruby-3.1
- ruby-3.2
- ruby-3.3
- ruby-3.4
# - jruby
# - truffleruby
runs-on: ${{ matrix.os }}
Expand Down
7 changes: 5 additions & 2 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
require:
plugins:
- rubocop-performance
- rubocop-rake
- rubocop-rspec

AllCops:
TargetRubyVersion: 2.6
TargetRubyVersion: 3.0
Exclude:
- bin/**/*
- vendor/**/*
Expand Down Expand Up @@ -148,6 +148,9 @@ RSpec/ExampleLength:
- hash
- heredoc

RSpec/IncludeExamples:
Enabled: false

RSpec/MultipleMemoizedHelpers:
Max: 20

Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.3.4] - 2025-08-12

### Changed
- `engine`: fixed issue with serialization of functions ([#26](https://github.com/gi/handlebars-ruby/pull/26))
- `engine`: include optional logging ([#24](https://github.com/gi/handlebars-ruby/pull/24))
- `ci`: test against macOS 14, Ubuntu 22.04, and ruby 3.1-3.4 ([#27](https://github.com/gi/handlebars-ruby/pull/27))

## [0.3.3] - 2022-02-17

### Changed
Expand Down
2 changes: 1 addition & 1 deletion handlebars-engine.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Gem::Specification.new do |spec|

spec.homepage = "https://github.com/gi/handlebars-ruby"
spec.license = "MIT"
spec.required_ruby_version = ">= 2.6.0"
spec.required_ruby_version = ">= 3.0"

spec.metadata["changelog_uri"] = "#{spec.homepage}/CHANGELOG.md"
spec.metadata["github_repo"] = spec.homepage
Expand Down
19 changes: 16 additions & 3 deletions lib/handlebars/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ class Engine
# environment.
# @param path [String, nil] the path to the version of Handlebars to load.
# If `nil`, the contents of `Handlebars::Source.bundled_path` is loaded.
def initialize(lazy: false, path: nil)
def initialize(lazy: false, logger: nil, path: nil)
@logger = logger
@path = path
init! unless lazy
end
Expand Down Expand Up @@ -84,9 +85,9 @@ def register_helper(name = nil, function = nil, **helpers, &block)
case f
when Proc
attach(n, &f)
evaluate("registerHelper('#{n}', #{n})")
evaluate("registerRbHelper('#{n}', #{n})")
when String, Symbol
evaluate("Handlebars.registerHelper('#{n}', #{f})")
evaluate("registerJsHelper('#{n}', #{f})")
end
end
end
Expand Down Expand Up @@ -175,13 +176,16 @@ def version

def attach(name, &block)
init!
@logger&.debug { "[handlebars] attaching #{name}" }
@context.attach(name.to_s, block)
end

def call(name, args, assign: false, eval: false)
init!
name = name.to_s

@logger&.debug { "[handlebars] calling #{name} with args #{args}" }

if assign || eval
call_via_eval(name, args, assign: assign)
else
Expand All @@ -207,6 +211,7 @@ def call_via_eval(name, args, assign: false)
end

def evaluate(code)
@logger&.debug { "[handlebars] evaluating #{code}" }
@context.eval(code)
end

Expand All @@ -222,10 +227,18 @@ def helper_missing_name(type)
def init!
return if @init

@logger&.debug { "[handlebars] initializing" }

@context = MiniRacer::Context.new
@context.attach(
"console.log",
->(*args) { @logger&.debug { "[handlebars] #{args.join(" ")}" } },
)
@context.load(@path || ::Handlebars::Source.bundled_path)
@context.load(File.absolute_path("engine/init.js", __dir__))

@logger&.debug { "[handlebars] initialized" }

@init = true
end

Expand Down
4 changes: 3 additions & 1 deletion lib/handlebars/engine/function.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ module Handlebars
class Engine
# A proxy for a JavaScript function defined in the context.
class Function
def initialize(context, name)
def initialize(context, name, logger: nil)
@context = context
@logger = logger
@name = name
end

def call(*args)
@logger&.debug { "[handlebars] calling #{@name} with args #{args}" }
@context.call(@name, *args)
end
end
Expand Down
17 changes: 13 additions & 4 deletions lib/handlebars/engine/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,23 @@ var template = (spec) => {
var registerPartial = Handlebars.registerPartial.bind(Handlebars);
var unregisterPartial = Handlebars.unregisterPartial.bind(Handlebars);

var registerHelper = (...args) => {
const fn = args[args.length - 1];
var registerJsHelper = Handlebars.registerHelper.bind(Handlebars);

var registerRbHelper = (name, fn) => {
function wrapper(...args) {
// Ruby cannot access the `this` context, so pass it as the first argument.
args.unshift(this);
const { ...options } = args[args.length-1];
Object.entries(options).forEach(([key, value]) => {
if (typeof value === "function") {
// functions are cannot be passed back to Ruby
options[key] = "function";
}
});
args[args.length-1] = options
return fn(...args);
}
args[args.length - 1] = wrapper;
return Handlebars.registerHelper(...args);
return registerJsHelper(name, wrapper);
};

var unregisterHelper = Handlebars.unregisterHelper.bind(Handlebars);
Expand Down
2 changes: 1 addition & 1 deletion lib/handlebars/engine/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

module Handlebars
class Engine
VERSION = "0.3.3"
VERSION = "0.3.4"
end
end
36 changes: 33 additions & 3 deletions spec/handlebars/engine_spec.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
# frozen_string_literal: true

require "logger"
require "tempfile"

RSpec.describe Handlebars::Engine do
let(:engine) { described_class.new(**engine_options) }
let(:engine_context) { engine.instance_variable_get(:@context) }
let(:engine_options) { {} }
let(:log) { Tempfile.new }
let(:logger) { Logger.new(log, level: Logger::FATAL) }
let(:render) { renderer.call(render_context, render_options) }
let(:render_context) { { name: "Zach", age: 30 } }
let(:render_options) { {} }
Expand Down Expand Up @@ -40,7 +43,26 @@
end

it "does not create the context" do
expect(engine_context).to be nil
expect(engine_context).to be_nil
end
end

context "when `logger` is defined" do
before do
engine_options[:logger] = logger
logger.debug!
end

it "logs initialization" do
engine
log.rewind
expect(log.read).to include("[handlebars] initializing")
end

it "logs javascript" do
engine.send(:evaluate, "console.log('js', 'log')")
log.rewind
expect(log.read).to include("[handlebars] js log")
end
end

Expand Down Expand Up @@ -255,7 +277,7 @@
describe "the options" do
it "includes the main block function" do
opts = include(
"fn" => kind_of(MiniRacer::JavaScriptFunction),
"fn" => "function", # kind_of(MiniRacer::JavaScriptFunction),
)
args = [anything, any_args, opts]
render
Expand All @@ -264,7 +286,7 @@

it "includes the else block function" do
opts = include(
"inverse" => kind_of(MiniRacer::JavaScriptFunction),
"inverse" => "function", # kind_of(MiniRacer::JavaScriptFunction),
)
args = [anything, any_args, opts]
render
Expand All @@ -279,6 +301,14 @@
<<~JS
function (...args) {
args.unshift(this);
const { ...options } = args[args.length-1];
Object.entries(options).forEach(([key, value]) => {
if (typeof value === "function") {
// functions are cannot be passed back to Ruby
options[key] = "function";
}
});
args[args.length-1] = options
return tester(...args);
}
JS
Expand Down
Loading