Repository: bridgetownrb/bridgetown (Ruby)

Issue:
feat: Support Nokolexbor for HTML Inspectors
Improved parsing and CSS selectors performance over Nokogiri: https://github.com/serpapi/nokolexbor

(and I'm now using it on a Bridgetown adjacent project to be announced soon…)


Discussion:
we're still waiting with bated breath for the Bridgetown-adjacent project 😉



Patch:
diff --git a/Gemfile b/Gemfile
index 530280a15..d0a834412 100644
--- a/Gemfile
+++ b/Gemfile
@@ -17,6 +17,7 @@ group :test do
   gem "minitest-profile"
   gem "minitest-reporters"
   gem "nokogiri", "~> 1.7"
+  gem "nokolexbor"
   gem "rack-test"
   gem "rspec"
   gem "rspec-mocks"
diff --git a/bridgetown-builder/lib/bridgetown-builder/dsl/inspectors.rb b/bridgetown-builder/lib/bridgetown-builder/dsl/inspectors.rb
index 198dc4b06..01e26bb1c 100644
--- a/bridgetown-builder/lib/bridgetown-builder/dsl/inspectors.rb
+++ b/bridgetown-builder/lib/bridgetown-builder/dsl/inspectors.rb
@@ -28,7 +28,11 @@ def self.can_run?(resource, inspectors)
           #
           # @return [String] transformed HTML
           def self.call(resource, inspectors)
-            doc = Nokogiri.HTML5(resource.output)
+            doc = if resource.site.config.html_inspector_parser == "nokolexbor"
+                    Nokolexbor::HTML(resource.output)
+                  else
+                    Nokogiri.HTML5(resource.output)
+                  end
 
             inspectors.each do |block|
               block.call(doc, resource)
@@ -73,9 +77,16 @@ def setup_nokogiri
               Bridgetown::Utils::RequireGems.require_with_graceful_fail "nokogiri"
             end
 
-            return if Nokogiri::XML::Node <= QuerySelection
+            Nokogiri::XML::Node.include QuerySelection unless Nokogiri::XML::Node <= QuerySelection
+          end
+
+          # Require the Nokolexbor gem if necessary and add the `QuerySelection` mixin
+          def setup_nokolexbor
+            unless defined?(Nokolexbor)
+              Bridgetown::Utils::RequireGems.require_with_graceful_fail "nokolexbor"
+            end
 
-            Nokogiri::XML::Node.include QuerySelection
+            Nokolexbor::Node.include QuerySelection unless Nokolexbor::Node <= QuerySelection
           end
 
           # Shorthand for `HTML.call`
@@ -91,12 +102,17 @@ def process_xml(...)
 
         # Set up an inspector to review or manipulate HTML resources
         # @yield the block to be called after the resource has been rendered
-        # @yieldparam [Nokogiri::HTML5::Document] the Nokogiri document
+        # @yieldparam [Nokogiri::HTML5::Document, Nokolexbor::Document]
+        #   the Nokogiri or Nokolexbor document
         def inspect_html(&block)
           unless @_html_inspectors
             @_html_inspectors = []
 
-            Inspectors.setup_nokogiri
+            if site.config.html_inspector_parser == "nokolexbor"
+              Inspectors.setup_nokolexbor
+            else
+              Inspectors.setup_nokogiri
+            end
 
             hook :resources, :post_render do |resource|
               next unless HTML.can_run?(resource, @_html_inspectors)
diff --git a/bridgetown-core/lib/site_template/Gemfile.erb b/bridgetown-core/lib/site_template/Gemfile.erb
index 6868fa73c..4aea0b31c 100644
--- a/bridgetown-core/lib/site_template/Gemfile.erb
+++ b/bridgetown-core/lib/site_template/Gemfile.erb
@@ -24,10 +24,13 @@ gem "bridgetown", "~> <%= Bridgetown::VERSION %>"
 # Uncomment to add file-based dynamic routing to your project:
 # gem "bridgetown-routes", "~> <%= Bridgetown::VERSION %>"
 
+# Puma is the Rack-compatible web server used by Bridgetown
+# (you can optionally limit this to the "development" group)
+gem "puma", "< 7"
+
 # Uncomment to use the Inspectors API to manipulate the output
 # of your HTML or XML resources:
 # gem "nokogiri", "~> 1.13"
 
-# Puma is a Rack-compatible server used by Bridgetown
-# (you can optionally limit this to the "development" group)
-gem "puma", "~> 5.6"
+# Or for faster parsing of HTML-only resources via Inspectors, use Nokolexbor:
+# gem "nokolexbor", "~> 0.4"
diff --git a/bridgetown-website/Gemfile b/bridgetown-website/Gemfile
index a760a3577..31b42acce 100644
--- a/bridgetown-website/Gemfile
+++ b/bridgetown-website/Gemfile
@@ -8,7 +8,7 @@ gem "bridgetown-builder", path: "../bridgetown-builder"
 gem "bridgetown-core", path: "../bridgetown-core"
 gem "bridgetown-paginate", path: "../bridgetown-paginate"
 
-gem "puma", "~> 5.5"
+gem "puma", "< 7"
 
 gem "bridgetown-feed", "~> 2.1", group: :bridgetown_plugins
 gem "bridgetown-quick-search", "~> 1.1", group: :bridgetown_plugins
@@ -16,3 +16,4 @@ gem "bridgetown-seo-tag", "~> 5.0", group: :bridgetown_plugins
 gem "bridgetown-svg-inliner", "~> 1.0", group: :bridgetown_plugins
 
 gem "gems", "~> 1.2"
+gem "nokolexbor", "~> 0.4"
diff --git a/bridgetown-website/bridgetown.config.yml b/bridgetown-website/bridgetown.config.yml
index 6b6c1015b..6edc21cc9 100644
--- a/bridgetown-website/bridgetown.config.yml
+++ b/bridgetown-website/bridgetown.config.yml
@@ -13,6 +13,8 @@ collections:
 pagination:
   enabled: true
 
+html_inspector_parser: nokolexbor
+
 # Environment-specific settings
 development:
   unpublished: true
diff --git a/bridgetown-website/frontend/javascript/index.js.rb b/bridgetown-website/frontend/javascript/index.js.rb
index 272dcd9ce..a4b46f5c6 100644
--- a/bridgetown-website/frontend/javascript/index.js.rb
+++ b/bridgetown-website/frontend/javascript/index.js.rb
@@ -54,7 +54,7 @@
   resolver: -> name do
     match = name.match(/^(.*?)\/(.*?)(-(fill))?$/)
     match[1] = match[1].char_at(0).upcase() + match[1].slice(1)
-    "https://cdn.jsdelivr.net/npm/remixicon@2.5.0/icons/#{match[1]}/#{match[2]}#{match[3] || '-line'}.svg";
+    "https://cdn.jsdelivr.net/npm/remixicon@3.2.0/icons/#{match[1]}/#{match[2]}#{match[3] || '-line'}.svg";
   end,
   mutator: -> svg { svg.set_attribute('fill', 'currentColor') }
 )
diff --git a/bridgetown-website/src/_docs/plugins/inspectors.md b/bridgetown-website/src/_docs/plugins/inspectors.md
index 2d8bb0ad1..9e78d5632 100644
--- a/bridgetown-website/src/_docs/plugins/inspectors.md
+++ b/bridgetown-website/src/_docs/plugins/inspectors.md
@@ -5,11 +5,11 @@ top_section: Configuration
 category: plugins
 ---
 
-The Inspectors API provides a useful way to review or manipulate the output of your HTML or XML resources. The API utilizes [Nokogiri](https://nokogiri.org), a Ruby gem which lets you work with a DOM-like API directly on the nodes of a document tree.
+The Inspectors API provides a useful way to review or manipulate the output of your HTML or XML resources. It allows for a safer approach of modifying HTML/XML content than alternatives such as string manipulation or regular expressions which can be prone to error or fail on unexpected input.
 
-This is a safer approach of modifying HTML/XML content than alternatives such as string manipulation or regular expressions which can be prone to error or fail on unexpected input.
+The API utilizes [Nokogiri](https://nokogiri.org) or [Nokolexbor](https://github.com/serpapi/nokolexbor), Ruby gems which lets you work with a DOM-like API directly on the nodes of a document tree. Nokogiri supports both HTML & XML files, whereas Nokolexbor only supports HTML (but is noticeably faster with most CSS-style querying).
 
-Bridgetown doesn't directly install the Nokogiri gem, so be sure to run `bundle add nokogiri` if it isn't already part of your bundle.
+Bridgetown doesn't directly install the Nokogiri or Nokolexbor gems, so if that isn't already part of your bundle, be sure to update your `Gemfile` to uncomment your gem(s) of choice and then run `bundle install`. If using Nokolexbor, be sure to add `html_inspector_parser: nokolexbor` to your [Bridgetown configuration](/docs/configuration).
 
 {%@ Note type: :warning do %}
   Inspectors will only apply to files Bridgetown considers [Resources](/docs/resources). Thus any HTML or XML file in your project lacking front matter won't get processed through your Inspectors. Make sure you add two lines of triple dashes `---` to the top of any file to indicate it's a Resource.
@@ -19,7 +19,7 @@ Bridgetown doesn't directly install the Nokogiri gem, so be sure to run `bundle
 
 Let's add an oft-requested feature to our site: automatic `target="_blank"` attributes on all outgoing links. It's easy with an HTML Inspector.
 
-We'll create a new builder plugin and use the `inspect_html` method to access the Nokogiri document and update all the relevant links.
+We'll create a new builder plugin and use the `inspect_html` method to access the document and update all the relevant links.
 
 ```ruby
 class Builders::Inspectors < SiteBuilder
@@ -38,7 +38,7 @@ end
 ```
 
 {%@ Note do %}
-Note that `query_selector_all` is an alias for Nokogiri's `css` method. We also provide `query_selector` as an alias for `at_css`.
+Note that `query_selector_all` is an alias for Nokogiri/Nokolexbor's `css` method. We also provide `query_selector` as an alias for `at_css`.
 {% end %}
 
 In the example above, we loop through all `a` tags, skip the tag if it already has a target or is not a true external link, otherwise we set the target attribute to `_blank`.
@@ -86,10 +86,12 @@ end
 
 ## Performance Considerations
 
-All resources which result in HTML or XML output (rather than JSON or some other format) will be processed through any defined Inspectors. For greater performance and fidelity, the Nokogiri document for a single resource will be the same across all Inspectors (rather than instantiating a new Nokogiri document for each Inspector).
+All resources which result in HTML or XML output (rather than JSON or some other format) will be processed through any defined Inspectors. For greater performance and fidelity, the Nokogiri/Nokolexbor document for a single resource will be the same across all Inspectors (rather than instantiating a new document for each Inspector).
 
 {%@ Note type: :warning do %}
-Nokogiri [relies on a C extension](https://nokogiri.org/#guiding-principles_1) which in turn uses `libxml2`. You should see fast performance unless the number of resources in your project is quite extensive.
+Nokogiri [relies on a C extension](https://nokogiri.org/#guiding-principles_1) which in turn uses `libxml2`. You should see pretty good performance unless the number of resources in your project is quite extensive.
+
+Nokolexbor [utilizes a C-based library called Lexbor](https://github.com/lexbor/lexbor/) which in some benchmarks shows a noticeable improvement over Nokogiri. Nokolexbor may also in some cases provide greater browser-like fidelity of HTML5 document trees as compared to Nokogiri.
 {% end %}
 
-If you find yourself needing to bypass Inspectors for certain, large resources to avoid the overhead of using Nokogiri, you can set the [front matter](/docs/front-matter) variable `bypass_inspectors: true` to instruct Nokogiri not to parse that resource. To apply this to a whole array of resources, make it a default with [front matter defaults](/docs/content/front-matter-defaults).
+If you find yourself needing to bypass Inspectors for certain, large resources to avoid the overhead of using Nokogiri/Nokolexbor, you can set the [front matter](/docs/front-matter) variable `bypass_inspectors: true` to instruct Bridgetown not to parse that resource. To apply this to a whole array of resources, make it a default with [front matter defaults](/docs/content/front-matter-defaults).
diff --git a/bridgetown-website/src/_pages/index.md b/bridgetown-website/src/_pages/index.md
index dd4a7ada6..617b9df6e 100644
--- a/bridgetown-website/src/_pages/index.md
+++ b/bridgetown-website/src/_pages/index.md
@@ -6,7 +6,7 @@ page_class: homepage
 
 <main-content>
   <section-wrapper size="large" class="fade-in-animation" invert style="margin-top:-0.5rem">
-  <svg-wrapper class="main-wave">{{ svg "/images/waves/wave_1.svg" }}</svg-wrapper>
+  <svg-wrapper class="main-wave" style="box-shadow: inset 0px -1px 0px #131e1b">{{ svg "/images/waves/wave_1.svg" }}</svg-wrapper>
   <section style="padding-top:2rem; padding-bottom:2rem" markdown="1">
 
 
@@ -46,7 +46,7 @@ Looking for [0.2x documentation](https://bridgetown-v0.onrender.com/)?
   <section markdown="1">
 
 <p class="heading-icon">
-  <sl-icon library="remixicon" name="logos/twitter-fill"></sl-icon>
+  <sl-icon library="remixicon" name="business/megaphone-fill"></sl-icon>
 </p>
 
 ## Listen to what they're saying about Bridgetown:
diff --git a/bridgetown-website/src/_pages/plugins.serb b/bridgetown-website/src/_pages/plugins.serb
index d3263ec9a..5685180b2 100644
--- a/bridgetown-website/src/_pages/plugins.serb
+++ b/bridgetown-website/src/_pages/plugins.serb
@@ -12,7 +12,7 @@
       ) do |faraday|
         if ENV["BRIDGETOWN_GITHUB_TOKEN"]
           username, token = ENV["BRIDGETOWN_GITHUB_TOKEN"].split(":")
-          faraday.request(:basic_auth, username, token)
+          faraday.request :authorization, :basic, username, token
         end
       end
       items = JSON.parse(conn.get.body)["items"].sort_by {|item| item["name"]}


Test patch:
diff --git a/bridgetown-builder/test/test_inspectors.rb b/bridgetown-builder/test/test_inspectors.rb
index 61c402103..07d106da5 100644
--- a/bridgetown-builder/test/test_inspectors.rb
+++ b/bridgetown-builder/test/test_inspectors.rb
@@ -121,4 +121,46 @@ def functions # stub to get hooks working
       assert_includes resource.output, "<title>ATOM-POWERED ROBOTS RUN AMOK</title>"
     end
   end
+
+  context "a resource to transform using Nokolexbor" do
+    setup do
+      @site = Site.new(site_configuration({ "html_inspector_parser" => "nokolexbor" }))
+      @_test_functions = []
+
+      inspect_html do |document|
+        document.query_selector_all("h1").each do |heading|
+          heading.content = heading.content.sub("World", "Universe")
+          heading.add_class "universal"
+        end
+      end
+
+      inspect_xml "atom" do |document, resource|
+        title = document.query_selector("entry > title")
+        title.content = title.content.upcase
+
+        assert_equal ".atom", resource.extname
+      end
+    end
+
+    teardown do
+      @_html_inspectors = nil
+      @_xml_inspectors = nil
+    end
+
+    should "allow manipulation via Nokolexbor" do
+      add_resource :posts, "html-inspectors.md" do
+        title "I'm a Markdown post!"
+        content <<~MARKDOWN
+          # Hello World!
+        MARKDOWN
+      end
+
+      resource = @site.collections.posts.resources.first
+      assert_equal 1, @site.collections.posts.resources.length
+      assert_equal "# Hello World!", resource.content.strip
+      resource.transform!
+      assert_equal %(<html><head></head><body><h1 id="hello-world" class="universal">Hello Universe!</h1>\n</body></html>),
+                   resource.output.strip
+    end
+  end
 end
diff --git a/bridgetown-core/test/helper.rb b/bridgetown-core/test/helper.rb
index 7834a8b40..ff544c804 100644
--- a/bridgetown-core/test/helper.rb
+++ b/bridgetown-core/test/helper.rb
@@ -17,6 +17,7 @@
 end
 
 require "nokogiri"
+require "nokolexbor"
 require "rubygems"
 require "ostruct"
 require "minitest/autorun"