diff --git a/Gemfile b/Gemfile index 5b972c55..78224b06 100644 --- a/Gemfile +++ b/Gemfile @@ -4,7 +4,7 @@ source 'https://rubygems.org' git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } -gem 'html2rss', '~> 0.19' +gem 'html2rss', '~> 0.20' # gem 'html2rss', github: 'html2rss/html2rss', branch: 'master' gem 'html2rss-configs', github: 'html2rss/html2rss-configs' diff --git a/Gemfile.lock b/Gemfile.lock index 9a1eb204..1a2acf88 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -136,7 +136,7 @@ GEM fiber-storage fiber-storage (1.0.1) hashdiff (1.2.1) - html2rss (0.19.1) + html2rss (0.20.0) addressable (~> 2.7) brotli dry-validation @@ -371,7 +371,7 @@ PLATFORMS DEPENDENCIES climate_control concurrent-ruby - html2rss (~> 0.19) + html2rss (~> 0.20) html2rss-configs! irb parallel @@ -439,7 +439,7 @@ CHECKSUMS fiber-local (1.1.0) sha256=c885f94f210fb9b05737de65d511136ea602e00c5105953748aa0f8793489f06 fiber-storage (1.0.1) sha256=f48e5b6d8b0be96dac486332b55cee82240057065dc761c1ea692b2e719240e1 hashdiff (1.2.1) sha256=9c079dbc513dfc8833ab59c0c2d8f230fa28499cc5efb4b8dd276cf931457cd1 - html2rss (0.19.1) sha256=0af89c99421eb45ad386885cd4e9d946fbb1a887e757922c6f6bc5d825a32ac2 + html2rss (0.20.0) sha256=88454f78ce125234f2934b969f18c4d2e3de1884d773a355c62e0b99ad2b7588 html2rss-configs (0.2.0) i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5 io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc diff --git a/app/web/boot/setup.rb b/app/web/boot/setup.rb index cf807ecf..f06718ac 100644 --- a/app/web/boot/setup.rb +++ b/app/web/boot/setup.rb @@ -23,11 +23,21 @@ def call! configure_sentry! configure_request_service! configure_runtime_logging! + configure_gem_defaults! log_startup! end private + # @return [void] + def configure_gem_defaults! + global_config = LocalConfig.global + Html2rss.configure do |config| + config.headers = global_config[:headers] if global_config[:headers] + config.stylesheets = global_config[:stylesheets] if global_config[:stylesheets] + end + end + # @return [void] def validate_environment! EnvironmentValidator.validate_environment! diff --git a/app/web/config/local_config.rb b/app/web/config/local_config.rb index 038f83a2..7518eda0 100644 --- a/app/web/config/local_config.rb +++ b/app/web/config/local_config.rb @@ -42,7 +42,7 @@ def find(name) config_hash = local_feed_config(normalized_name) || embedded_feed_config(normalized_name) raise NotFound, "Did not find local feed config at '#{normalized_name}'" unless config_hash - apply_global_defaults(config_hash) + config_hash end ## @@ -120,19 +120,6 @@ def embedded_feed_config(normalized_name) nil end - # Applies global defaults only when feed-level keys are absent. - # - # @param config [Hash{Symbol=>Object}] - # @return [Hash{Symbol=>Object}] - def apply_global_defaults(config) - global_config = global - - config[:stylesheets] ||= deep_dup(global_config[:stylesheets]) if global_config[:stylesheets] - config[:headers] ||= deep_dup(global_config[:headers]) if global_config[:headers] - - config - end - # @param name [String, Symbol, #to_s] # @return [String] path without feed extension for feed lookup. def normalize_name(name) diff --git a/app/web/rendering/xml_builder.rb b/app/web/rendering/xml_builder.rb index 057e997b..d8986171 100644 --- a/app/web/rendering/xml_builder.rb +++ b/app/web/rendering/xml_builder.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true -require 'nokogiri' +require 'rss' require 'time' + module Html2rss module Web ## @@ -18,17 +19,11 @@ class << self # @param timestamp [Time, nil] # @return [String] serialized RSS XML document. def build_rss_feed(title:, description:, link: nil, items: [], timestamp: nil) - current_time = timestamp || Time.now - formatted_now = format_pub_date(current_time) - - Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml| - xml.rss(version: '2.0') do - xml.channel do - build_channel(xml, title:, description:, link:, now: formatted_now) - build_items(xml, items, default_pub_date: formatted_now) - end - end - end.doc.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML) + RSS::Maker.make('2.0') do |maker| + apply_stylesheets(maker) + build_channel(maker.channel, title:, description:, link:, timestamp:) + build_items(maker, items, default_timestamp: timestamp) + end.to_s end # @param message [String] @@ -61,6 +56,16 @@ def build_empty_feed_warning(url:, strategy:, site_title: nil) private + # @param maker [RSS::Maker::RSS20] + # @return [void] + def apply_stylesheets(maker) + # Use the gem's internal stylesheet support. + stylesheets = Html2rss.configuration.stylesheets.map do |s| + Html2rss::RssBuilder::Stylesheet.new(**s) + end + Html2rss::RssBuilder::Stylesheet.add(maker, stylesheets) + end + # @param title [String] # @param description [String] # @param item [Hash{Symbol=>Object}] @@ -81,57 +86,43 @@ def build_single_item_feed(title:, description:, item:, link: nil) # @param timestamp [Time] # @return [Hash{Symbol=>Object}] normalized item with required RSS fields. def feed_item(item, timestamp:) - feed_item = { + { title: item[:title], description: item[:description], + link: item[:link], pubDate: timestamp } - feed_item[:link] = item[:link] if item[:link] - feed_item end - # @param xml [Nokogiri::XML::Builder] + # @param channel [RSS::Maker::RSS20::Channel] # @param title [String] # @param description [String] # @param link [String, nil] - # @param now [String] + # @param timestamp [Time, nil] # @return [void] - def build_channel(xml, title:, description:, link:, now:) - xml.title(title.to_s) - xml.description(description.to_s) - xml.link(link.to_s) if link - xml.lastBuildDate(now) - xml.pubDate(now) + def build_channel(channel, title:, description:, link:, timestamp:) + now = timestamp || Time.now + channel.title = title.to_s + channel.description = description.to_s + channel.link = link.to_s + channel.lastBuildDate = now + channel.pubDate = now end - # @param xml [Nokogiri::XML::Builder] + # @param maker [RSS::Maker::RSS20] # @param items [ArrayObject}>] - # @param default_pub_date [String] + # @param default_timestamp [Time, nil] # @return [void] - def build_items(xml, items, default_pub_date:) + def build_items(maker, items, default_timestamp:) items.each do |item| - xml.item do - append_text_node(xml, :title, item[:title]) - append_text_node(xml, :description, item[:description]) - append_text_node(xml, :link, item[:link]) - xml.pubDate(format_pub_date(item[:pubDate] || default_pub_date)) + maker.items.new_item do |i| + i.title = item[:title].to_s + i.description = item[:description].to_s + i.link = item[:link].to_s + i.pubDate = item[:pubDate] || default_timestamp || Time.now end end end - - # @param xml [Nokogiri::XML::Builder] - # @param node_name [Symbol] - # @param value [Object] - # @return [void] - def append_text_node(xml, node_name, value) - xml.public_send(node_name, value.to_s) if value - end - - # @param pub_date [Time, String] - # @return [String] RFC2822 date string for RSS output. - def format_pub_date(pub_date) - pub_date.is_a?(Time) ? pub_date.rfc2822 : pub_date.to_s - end end end end diff --git a/spec/html2rss/web/xml_builder_spec.rb b/spec/html2rss/web/xml_builder_spec.rb index a06d9047..ffd4f921 100644 --- a/spec/html2rss/web/xml_builder_spec.rb +++ b/spec/html2rss/web/xml_builder_spec.rb @@ -29,5 +29,9 @@ it 'does not mention hidden strategy controls in item text' do expect(xml_doc.at_xpath('//item/description').text).not_to include('browserless strategy') end + + it 'includes the default stylesheet processing instruction' do + expect(xml_doc.to_s).to include('') + end end end