diff --git a/app/helpers/searches_helper.rb b/app/helpers/searches_helper.rb
index f3b22e69..72d9966d 100644
--- a/app/helpers/searches_helper.rb
+++ b/app/helpers/searches_helper.rb
@@ -22,7 +22,10 @@ def highlight_searched_content(leaf, content, query)
end
private
+ # Terms come from FTS5 highlight() spans, which can include document
+ # punctuation (e.g. a phrase match spanning "foo) bar"). Escape them so
+ # metacharacters are matched literally instead of raising a RegexpError.
def whole_word_matchers(terms)
- terms.map { |term| /\b#{term}\b/ }
+ terms.map { |term| /\b#{Regexp.escape(term)}\b/ }
end
end
diff --git a/test/controllers/leafables_controller_test.rb b/test/controllers/leafables_controller_test.rb
index d9e383c7..0d180e57 100644
--- a/test/controllers/leafables_controller_test.rb
+++ b/test/controllers/leafables_controller_test.rb
@@ -30,6 +30,20 @@ class LeafablesControllerTest < ActionDispatch::IntegrationTest
assert_select "mark", "great"
end
+ test "show highlights a phrase match whose span contains regex metacharacters" do
+ sign_out
+ books(:handbook).update!(published: true)
+
+ section = Section.new(body: "alpha) omega in the body")
+ books(:handbook).press(section, title: "Punctuated")
+ section.leaf.reindex
+
+ get leafable_slug_path(section.leaf), params: { search: '"alpha omega"' }
+
+ assert_response :success
+ assert_select "mark", text: /alpha\) omega/
+ end
+
test "show does not allow public access to an unpublished book" do
sign_out
diff --git a/test/helpers/searches_helper_test.rb b/test/helpers/searches_helper_test.rb
index 7b144406..a71c9571 100644
--- a/test/helpers/searches_helper_test.rb
+++ b/test/helpers/searches_helper_test.rb
@@ -1,6 +1,7 @@
require "test_helper"
class SearchesHelperTest < ActionView::TestCase
+ include PagesHelper
test "sanitize_search_result preserves mark tags" do
assert_equal "findme text", sanitize_search_result("findme text")
end
@@ -16,4 +17,14 @@ class SearchesHelperTest < ActionView::TestCase
test "sanitize_search_result strips attributes from mark tags" do
assert_equal "findme text", sanitize_search_result('findme text')
end
+
+ test "highlight_searched_content handles matched terms containing regex metacharacters" do
+ leaf = Struct.new(:terms) do
+ def matches_for_highlight(_query) = terms
+ end.new([ "foo) bar" ])
+
+ result = highlight_searched_content(leaf, "foo) bar in the body", "foo bar")
+
+ assert_includes result, "foo) bar"
+ end
end