diff --git a/Gemfile b/Gemfile index 466d12bf..09b5c1c7 100644 --- a/Gemfile +++ b/Gemfile @@ -2,8 +2,12 @@ source "https://rubygems.org" ruby '~> 3.2.2' +gem "actionpack", "~> 8.0.0" +gem "activesupport", "~> 8.0.0" +gem "actionview", "~> 8.0.0" gem "jekyll", "~> 4" gem "kramdown", ">= 2.3.0" +gem "view_component", "~> 3.20.0" group :jekyll_plugins do gem "jekyll-redirect-from" @@ -12,7 +16,6 @@ group :jekyll_plugins do end group :test do - gem 'activesupport', '~> 7.0.7' gem 'html-proofer', '~> 4.4.3' gem 'nokogiri', '~> 1.16.5' gem 'rspec', '~> 3.9.0' diff --git a/Gemfile.lock b/Gemfile.lock index df6dec4c..680a049d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,19 +1,51 @@ GEM remote: https://rubygems.org/ specs: - activesupport (7.0.7.1) - concurrent-ruby (~> 1.0, >= 1.0.2) + actionpack (8.0.0) + actionview (= 8.0.0) + activesupport (= 8.0.0) + nokogiri (>= 1.8.5) + rack (>= 2.2.4) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + useragent (~> 0.16) + actionview (8.0.0) + activesupport (= 8.0.0) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activesupport (8.0.0) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) + logger (>= 1.4.2) minitest (>= 5.1) - tzinfo (~> 2.0) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) addressable (2.8.4) public_suffix (>= 2.0.2, < 6.0) + base64 (0.2.0) + benchmark (0.4.0) + bigdecimal (3.1.8) + builder (3.3.0) colorator (1.1.0) - concurrent-ruby (1.2.2) + concurrent-ruby (1.3.4) + connection_pool (2.4.1) + crass (1.0.6) diff-lcs (1.5.0) + drb (2.2.1) em-websocket (0.5.3) eventmachine (>= 0.12.9) http_parser.rb (~> 0) + erubi (1.13.0) ethon (0.16.0) ffi (>= 1.15.0) eventmachine (1.2.7) @@ -30,7 +62,7 @@ GEM yell (~> 2.0) zeitwerk (~> 2.5) http_parser.rb (0.8.0) - i18n (1.14.1) + i18n (1.14.6) concurrent-ruby (~> 1.0) jekyll (4.3.2) addressable (~> 2.4) @@ -67,9 +99,14 @@ GEM listen (3.8.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) + logger (1.6.2) + loofah (2.23.1) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) mercenary (0.4.0) + method_source (1.1.0) mini_portile2 (2.8.6) - minitest (5.19.0) + minitest (5.25.4) nokogiri (1.16.5) mini_portile2 (~> 2.8.2) racc (~> 1.4) @@ -79,6 +116,18 @@ GEM posix-spawn (0.3.15) public_suffix (5.0.1) racc (1.7.3) + rack (3.1.8) + rack-session (2.0.0) + rack (>= 3.0.0) + rack-test (2.1.0) + rack (>= 1.3) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) rainbow (3.1.1) rake (13.0.6) rb-fsevent (0.11.2) @@ -103,6 +152,7 @@ GEM sass-embedded (1.63.2) google-protobuf (~> 3.23) rake (>= 10.0.0) + securerandom (0.4.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) typhoeus (1.4.0) @@ -110,6 +160,12 @@ GEM tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.4.2) + uri (1.0.2) + useragent (0.16.11) + view_component (3.20.0) + activesupport (>= 5.2.0, < 8.1) + concurrent-ruby (~> 1.0) + method_source (~> 1.0) webrick (1.8.2) yell (2.2.2) zeitwerk (2.6.7) @@ -118,7 +174,9 @@ PLATFORMS ruby DEPENDENCIES - activesupport (~> 7.0.7) + actionpack (~> 8.0.0) + actionview (~> 8.0.0) + activesupport (~> 8.0.0) html-proofer (~> 4.4.3) jekyll (~> 4) jekyll-environment-variables @@ -127,6 +185,7 @@ DEPENDENCIES kramdown (>= 2.3.0) nokogiri (~> 1.16.5) rspec (~> 3.9.0) + view_component (~> 3.20.0) RUBY VERSION ruby 3.2.2p53 diff --git a/_components/breadcrumb_component.html.erb b/_components/breadcrumb_component.html.erb new file mode 100644 index 00000000..4f562f76 --- /dev/null +++ b/_components/breadcrumb_component.html.erb @@ -0,0 +1,5 @@ + diff --git a/_components/breadcrumb_component.rb b/_components/breadcrumb_component.rb new file mode 100644 index 00000000..dc1162bf --- /dev/null +++ b/_components/breadcrumb_component.rb @@ -0,0 +1 @@ +class BreadcrumbComponent < ViewComponent::Base; end diff --git a/_components/breadcrumb_item_component.html.erb b/_components/breadcrumb_item_component.html.erb new file mode 100644 index 00000000..f8817ca4 --- /dev/null +++ b/_components/breadcrumb_item_component.html.erb @@ -0,0 +1,3 @@ +
  • + <%= content %> +
  • diff --git a/_components/breadcrumb_item_component.rb b/_components/breadcrumb_item_component.rb new file mode 100644 index 00000000..107b0283 --- /dev/null +++ b/_components/breadcrumb_item_component.rb @@ -0,0 +1,9 @@ +class BreadcrumbItemComponent < ViewComponent::Base + attr_reader :href, :current + alias_method :current?, :current + + def initialize(href:, current: false) + @href = href + @current = current + end +end diff --git a/_layouts/article.html b/_layouts/article.html index e653437a..89996e7e 100644 --- a/_layouts/article.html +++ b/_layouts/article.html @@ -20,6 +20,16 @@
    + {% if page.category %} + {% assign home_url = "/" | prepend: site.baseurl %} + {% assign category_url = page.category | slugify | prepend: "/categories/" | prepend: site.baseurl | append: ".html" %} + {% component breadcrumb %} + {% component breadcrumb_item href=home_url %}Home{% endcomponent %} + {% component breadcrumb_item href=category_url %}{{ page.category }}{% endcomponent %} + {% component breadcrumb_item href=page.url current %}{{ page.title }}{% endcomponent %} + {% endcomponent %} + {% endif %} +

    {{ page.title }} {% if page.deprecated %} diff --git a/_layouts/category.html b/_layouts/category.html index cc0fb02f..b807a07e 100644 --- a/_layouts/category.html +++ b/_layouts/category.html @@ -35,6 +35,12 @@
    + {% assign home_url = "/" | prepend: site.baseurl %} + {% component breadcrumb %} + {% component breadcrumb_item href=home_url %}Home{% endcomponent %} + {% component breadcrumb_item href=page.url current %}{{ page.title }}{% endcomponent %} + {% endcomponent %} +

    {% if page.icon %} {% include uswds-icon.html icon=page.icon %} diff --git a/_plugins/view_component.rb b/_plugins/view_component.rb new file mode 100644 index 00000000..15ca547f --- /dev/null +++ b/_plugins/view_component.rb @@ -0,0 +1,82 @@ +require 'action_controller' +require 'action_view' +require 'active_support' +require 'view_component' + +unless defined?(Rails) + module Rails + class Application + def routes + @routes ||= Struct.new(:url_helpers).new(Module.new) + end + end + + def self.version + ActionView.version.to_s + end + + module VERSION + MAJOR, MINOR = Rails.version.split('.').map(&:to_i) + end + + def self.env + @env ||= Struct.new(:development?, :test?, :production?).new(false, false, true) + end + + def self.application + @application ||= Application.new + end + end +end + +Dir['_components/**/*.rb'].each { |f| require File.join('.', f) } + +class ComponentTag < Liquid::Block + PARAM_SYNTAX = /(\w+)(?:=(?:"([^"]+?)"|(\S+)))?/.freeze + + def initialize(tag_name, variables, context) + super + + @component_name, @params = variables.split(' ', 2) + end + + # Jekyll tags don't have built-in support for variable references, but Jekyll's built-in `include` + # tag has a syntax we can use as a starting point. + # + # @see https://github.com/jekyll/jekyll/blob/master/lib/jekyll/tags/include.rb + # + # Example: + # + # {% component link url=page.url text="Home" current %} + # + # In this example, `url` would be assigned from the page context variable nested value, `text` + # would be assigned to a string literal, and `current` would be `true`. + def parse_params(context) + params = {} + @params.scan(PARAM_SYNTAX) do |key, string, variable| + if string + params[key] = string + elsif variable + parts = variable.split('.') + value = context + while (part = parts.shift) + value = value[part] + end + params[key] = value + else + params[key] = true + end + end + params + end + + def render(context) + content = super.html_safe + + component_class = "#{@component_name.camelize}Component".constantize + component = component_class.new(**parse_params(context).symbolize_keys).with_content(content) + ActionController::Base.new.render_to_string(component) + end +end + +Liquid::Template.register_tag('component', ComponentTag)