diff --git a/app.rb b/app.rb index 3e291364..9471c646 100644 --- a/app.rb +++ b/app.rb @@ -100,6 +100,9 @@ def self.development? = ENV['RACK_ENV'] == 'development' config[:strategy] ||= Html2rss::RequestService.default_strategy_name end + # Merge global stylesheets into the config + config = LocalConfig.merge_global_stylesheets(config) + feed = Html2rss.feed(config) HttpCache.expires(response, feed.channel.ttl.to_i * 60, cache_control: 'public') diff --git a/app/local_config.rb b/app/local_config.rb index e1b8f3e4..74347d62 100644 --- a/app/local_config.rb +++ b/app/local_config.rb @@ -19,7 +19,22 @@ class NotFound < RuntimeError; end # @param name [String, Symbol, #to_sym] # @return [Hash] def find(name) - feeds.fetch(name.to_sym) { raise NotFound, "Did not find local feed config at '#{name}'" } + feed_config = feeds.fetch(name.to_sym) { raise NotFound, "Did not find local feed config at '#{name}'" } + merge_global_stylesheets(feed_config) + end + + ## + # Merges global stylesheets into a feed configuration if the feed doesn't already have stylesheets. + # + # @param config [Hash] The feed configuration to merge stylesheets into + # @return [Hash] The configuration with merged stylesheets (duplicated if needed) + def merge_global_stylesheets(config) + global_config = global + return config unless global_config[:stylesheets] && !config.key?(:stylesheets) + + config = config.dup + config[:stylesheets] = global_config[:stylesheets] + config end ## diff --git a/spec/html2rss/web/app/local_config_spec.rb b/spec/html2rss/web/app/local_config_spec.rb index ab9e42c8..511bd290 100644 --- a/spec/html2rss/web/app/local_config_spec.rb +++ b/spec/html2rss/web/app/local_config_spec.rb @@ -31,4 +31,140 @@ describe '.global' do it { expect(described_class.global).to be_a Hash } end + + describe '.merge_global_stylesheets' do + let(:config_with_stylesheets) do + { + stylesheets: [{ href: '/custom.xsl', type: 'text/xsl' }], + feeds: { + example: { + channel: { url: 'https://example.com' }, + selectors: { items: { selector: 'div' } } + } + } + } + end + + let(:config_without_stylesheets) do + { + stylesheets: [{ href: '/rss.xsl', type: 'text/xsl' }], + feeds: { + example: { + channel: { url: 'https://example.com' }, + selectors: { items: { selector: 'div' } } + } + } + } + end + + let(:config_no_global_stylesheets) do + { + feeds: { + example: { + channel: { url: 'https://example.com' }, + selectors: { items: { selector: 'div' } } + } + } + } + end + + context 'when config has no stylesheets and global has stylesheets' do + before do + allow(described_class).to receive(:global).and_return(config_without_stylesheets) + end + + it 'merges global stylesheets into the config' do + config = { channel: { url: 'https://test.com' } } + result = described_class.merge_global_stylesheets(config) + + expect(result[:stylesheets]).to eq([{ href: '/rss.xsl', type: 'text/xsl' }]) + end + + it 'preserves original config data' do + config = { channel: { url: 'https://test.com' } } + result = described_class.merge_global_stylesheets(config) + + expect(result[:channel]).to eq({ url: 'https://test.com' }) + end + + it 'duplicates the config to avoid mutation' do + config = { channel: { url: 'https://test.com' } } + result = described_class.merge_global_stylesheets(config) + + expect(result).not_to be(config) + end + + it 'creates a new object instance' do + config = { channel: { url: 'https://test.com' } } + result = described_class.merge_global_stylesheets(config) + + expect(result.object_id).not_to eq(config.object_id) + end + end + + context 'when config already has stylesheets' do + before do + allow(described_class).to receive(:global).and_return(config_without_stylesheets) + end + + it 'does not override existing stylesheets' do + config = { stylesheets: [{ href: '/custom.xsl', type: 'text/xsl' }] } + result = described_class.merge_global_stylesheets(config) + + expect(result[:stylesheets]).to eq([{ href: '/custom.xsl', type: 'text/xsl' }]) + end + + it 'returns the original config without duplication' do + config = { stylesheets: [{ href: '/custom.xsl', type: 'text/xsl' }] } + result = described_class.merge_global_stylesheets(config) + + expect(result).to be(config) + end + end + + context 'when global config has no stylesheets' do + before do + allow(described_class).to receive(:global).and_return(config_no_global_stylesheets) + end + + it 'returns the original config unchanged' do + config = { channel: { url: 'https://test.com' } } + result = described_class.merge_global_stylesheets(config) + + expect(result).to be(config) + end + + it 'does not add stylesheets when none exist globally' do + config = { channel: { url: 'https://test.com' } } + result = described_class.merge_global_stylesheets(config) + + expect(result[:stylesheets]).to be_nil + end + end + end + + describe '.find with stylesheet merging' do + before do + allow(described_class).to receive_messages(feeds: { + example: { + channel: { url: 'https://example.com' }, + selectors: { items: { selector: 'div' } } + } + }, global: { + stylesheets: [{ href: '/rss.xsl', type: 'text/xsl' }] + }) + end + + it 'merges global stylesheets when finding a feed' do + result = described_class.find('example') + + expect(result[:stylesheets]).to eq([{ href: '/rss.xsl', type: 'text/xsl' }]) + end + + it 'preserves feed configuration when finding a feed' do + result = described_class.find('example') + + expect(result[:channel]).to eq({ url: 'https://example.com' }) + end + end end