From 4cd9223a9a7e7ec29d1597521dbe3399b06e4d18 Mon Sep 17 00:00:00 2001 From: Michael Herold Date: Sun, 26 Feb 2023 15:03:17 -0600 Subject: [PATCH] Make OutputBuffer more performant for 3.2+ YJIT This change ports Jean Boussier's Rails change over to Bridgetown with a few tweaks. 1. Using `instance_of?` against the constant is faster than the alternative `self.class == other.class` construct 2. Linear, comparable logic for `<<` 3. An expanded set of delegated methods to include `#empty?`, `#encode`, `#lines`, `#reverse`, `#strip`, and `#valid_encoding?` because these are all called in the test suite. In the current test suite, this is a count of how many times each method is called: 2794 empty? 54 encode 54 encoding 381 lines 127 reverse 127 strip 54 valid_encoding? It's unclear to me which, if any, of these should be public API so I kept all of the delegated methods as well as the ones Jean picked for Rails. --- .../converters/erb_templates.rb | 57 +++++++++++++++++-- .../test/bridgetown/test_output_buffer.rb | 41 +++++++++++++ 2 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 bridgetown-core/test/bridgetown/test_output_buffer.rb diff --git a/bridgetown-core/lib/bridgetown-core/converters/erb_templates.rb b/bridgetown-core/lib/bridgetown-core/converters/erb_templates.rb index 552fc9efe..40248ba8f 100644 --- a/bridgetown-core/lib/bridgetown-core/converters/erb_templates.rb +++ b/bridgetown-core/lib/bridgetown-core/converters/erb_templates.rb @@ -3,26 +3,71 @@ require "tilt/erubi" module Bridgetown - class OutputBuffer < ActiveSupport::SafeBuffer - def initialize(*) - super - encode! + class OutputBuffer + def initialize(buffer = "") + @buffer = String.new(buffer) + @buffer.encode! end + def initialize_copy(other) + @buffer = other.to_str + end + + delegate( + :blank?, + :empty?, + :encode, + :encode!, + :encoding, + :force_encoding, + :length, + :lines, + :reverse, + :strip, + :valid_encoding?, + to: :@buffer + ) + def <<(value) return self if value.nil? - super(value.to_s) + value = value.to_s + value = CGI.escapeHTML(value) unless value.html_safe? + + @buffer << value + + self end alias_method :append=, :<< + def ==(other) + other.instance_of?(OutputBuffer) && + @buffer == other.to_str + end + + def html_safe? + true + end + + def safe_concat(value) + @buffer << value + self + end + alias_method :safe_append=, :safe_concat + def safe_expr_append=(val) return self if val.nil? # rubocop:disable Lint/ReturnInVoidContext safe_concat val.to_s end - alias_method :safe_append=, :safe_concat + def to_s + @buffer.html_safe + end + + def to_str + @buffer.dup + end end class ERBEngine < Erubi::Engine diff --git a/bridgetown-core/test/bridgetown/test_output_buffer.rb b/bridgetown-core/test/bridgetown/test_output_buffer.rb new file mode 100644 index 000000000..dbc3bde4b --- /dev/null +++ b/bridgetown-core/test/bridgetown/test_output_buffer.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require "helper" + +module Bridgetown + class OutputBufferTest < BridgetownUnitTest + def setup + super + @buffer = Bridgetown::OutputBuffer.new + end + + should "be able to be duped" do + @buffer << "Hello" + copy = @buffer.dup + copy << " world!" + + assert_equal "Hello", @buffer.to_s + assert_equal "Hello world!", copy.to_s + end + + context "#<<" do + should "maintain HTML safety" do + @buffer << "

Nothing bad to see here.

" + + assert_predicate @buffer, :html_safe? + assert_predicate @buffer.to_s, :html_safe? + assert_equal "<p>Nothing bad to see here.</p>", @buffer.to_s + end + end + + context "#safe_append=" do + should "bypass HTML safety" do + @buffer.safe_append = "

Nothing bad to see here.

" + + assert_predicate @buffer, :html_safe? + assert_predicate @buffer.to_s, :html_safe? + assert_equal "

Nothing bad to see here.

", @buffer.to_s + end + end + end +end