diff --git a/doc/PLUGINS.md b/doc/PLUGINS.md index 8f752d0..9ef8cb2 100644 --- a/doc/PLUGINS.md +++ b/doc/PLUGINS.md @@ -313,9 +313,10 @@ Rules that follow from the shape: - **Return the shape, always.** Returning `nil`, a string or a bare array of items ends the pipeline for everything after it. -- **`link` may be `nil`, and so may any other field.** Filters signal "not - applicable" by setting `link` to `nil`, so a plugin that dereferences a field - without checking will be handed `nil` sooner or later. +- **`link` may be `nil`, and so may any other field.** A missing field is data, + not a framework-wide drop signal: `FeedMaker.create_pipeline` preserves an + item whose link is `nil`. A plugin that requires a link must check it itself; + a plugin that does not may keep processing the item. - **Guard the feed itself.** `@pipeline.each { |feeds| next if feeds.nil? }` is the prevailing idiom, because a subscription plugin that failed may have put a `nil` in the array. @@ -950,9 +951,10 @@ plugin has done the work, so that later plugins publish nothing. No settings. #### FilterImage — **Supported** -`filter/image.rb`. Sets `link` to `nil` unless it names an image. Note that it -does not remove the items — it blanks their links, and the plugins after it -skip items whose link is `nil`. No settings. +`filter/image.rb`. Sets `link` to `nil` unless it names an image. It does not +remove the item: a later plugin that requires a link skips it under that +plugin's own rules, while a link-independent plugin may continue to use it. No +settings. The extensions are `.jpg`, `.jpeg`, `.gif`, `.png`, `.tif`, `.tiff`, `.webp` and `.avif`, and the test is on the URL's **path**. Both of those changed: diff --git a/doc/VERSIONS b/doc/VERSIONS index d3a54f5..ffd5e7b 100644 --- a/doc/VERSIONS +++ b/doc/VERSIONS @@ -7,6 +7,8 @@ v26.09 (Release Date: TBD) make doc/PLUGINS.md section 6 the plugin catalogue's source of truth. - Harden the framework's execution boundary: fix CLI contract drift, distinguish dependency from load failures, and preflight-validate Recipe plugins. +- Preserve nil-link pipeline items and remove SubscriptionText placeholder + title and link values. v26.08 (2026-08-22) ------------------- diff --git a/lib/automatic/feed_maker.rb b/lib/automatic/feed_maker.rb index d285c54..6f17dcd 100644 --- a/lib/automatic/feed_maker.rb +++ b/lib/automatic/feed_maker.rb @@ -5,7 +5,7 @@ # License:: The GPL version 3, or LGPL version 3 (Dual License). # Contact:: idnanashi@gmail.com # Created:: Feb 21, 2014 -# Updated:: Sep 6, 2026 +# Updated:: Sep 7, 2026 # Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers. module Automatic @@ -16,8 +16,8 @@ module FeedMaker class FeedObject attr_accessor :title, :link, :description, :author, :comments def initialize - @link = 'http://dummy' - @title = 'dummy' + @link = nil + @title = nil @description = '' @author = '' @comments = '' @@ -37,7 +37,7 @@ def self.generate_feed(feed) # Plain-value standard fields, copied onto the rebuilt item as-is when the # item being rebuilt carries them. See doc/REQUIREMENTS.md and # doc/PLUGINS.md for the standard item field contract this preserves. - REBUILD_SIMPLE_FIELDS = %i[description author comments content_encoded].freeze + REBUILD_SIMPLE_FIELDS = %i[title link description author comments content_encoded].freeze # source and enclosure are RSS child elements. RSS::Maker exposes each of # them on a new item as a builder with its own sub-attributes rather than @@ -58,12 +58,8 @@ def self.create_pipeline(feeds = []) unless feeds.nil? feeds.each {|feed| - next if feed.link.nil? - Automatic::Log.puts("info", "Create Pipeline: #{feed.link}") item = maker.items.new_item - item.title = feed.title - item.link = feed.link item.date = (feed.pubDate if feed.respond_to?(:pubDate)) || Time.now REBUILD_SIMPLE_FIELDS.each { |field| copy_rebuild_field(feed, item, field) } diff --git a/spec/lib/automatic/feed_maker_spec.rb b/spec/lib/automatic/feed_maker_spec.rb index 296d517..450dbd2 100644 --- a/spec/lib/automatic/feed_maker_spec.rb +++ b/spec/lib/automatic/feed_maker_spec.rb @@ -5,7 +5,7 @@ # License:: The GPL version 3, or LGPL version 3 (Dual License). # Contact:: idnanashi@gmail.com # Created:: Sep 6, 2026 -# Updated:: Sep 6, 2026 +# Updated:: Sep 7, 2026 # Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers. # # create_pipeline rebuilds each item it is given into a new RSS feed. This is @@ -19,6 +19,22 @@ require 'automatic/feed_maker' describe Automatic::FeedMaker do + describe ".generate_feed" do + it "leaves link absent when only a title is given" do + item = Automatic::FeedMaker.generate_feed("title" => "A title") + + item.title.should == "A title" + item.link.should be_nil + end + + it "leaves title absent when only a URL is given" do + item = Automatic::FeedMaker.generate_feed("url" => "https://example.com/a") + + item.title.should be_nil + item.link.should == "https://example.com/a" + end + end + describe ".create_pipeline" do # Builds a pipeline item the way FeedParser or a previous create_pipeline # call would hand one on: a real RSS::Rss::Channel::Item, with real @@ -154,14 +170,20 @@ def build_item(link: "https://example.com/a", title: "A title", item.content_encoded.should == "
Full body
" end - it "keeps the existing item-count behaviour, skipping an item with no link" do - linked = build_item(link: "https://example.com/a") - unlinked = build_item(link: nil) + it "preserves both linked and linkless items" do + linked = build_item(link: "https://example.com/a", title: "Linked") + unlinked = build_item(link: nil, title: "Unlinked", + description: "A linkless description") rebuilt = Automatic::FeedMaker.create_pipeline([linked, unlinked]) - rebuilt.items.size.should == 1 - rebuilt.items.first.link.should == "https://example.com/a" + rebuilt.items.size.should == 2 + rebuilt.items.map(&:title).should include("Linked", "Unlinked") + + item = rebuilt.items.find { |candidate| candidate.title == "Unlinked" } + item.should_not be_nil + item.link.should be_nil + item.description.should == "A linkless description" end end end diff --git a/spec/plugins/filter/batch_spec.rb b/spec/plugins/filter/batch_spec.rb index 8802d90..b39a3bc 100644 --- a/spec/plugins/filter/batch_spec.rb +++ b/spec/plugins/filter/batch_spec.rb @@ -5,12 +5,13 @@ # License:: The GPL version 3, or LGPL version 3 (Dual License). # Contact:: idnanashi@gmail.com # Created:: Aug 24, 2026 -# Updated:: Aug 24, 2026 +# Updated:: Sep 7, 2026 # Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers. require File.expand_path(File.dirname(__FILE__) + '../../../spec_helper') require 'filter/batch' +require 'filter/limit' describe Automatic::Plugin::FilterBatch do def batch(config, pipeline) @@ -81,6 +82,23 @@ def batch(config, pipeline) end end + it 'keeps a linkless batch item through FilterLimit' do + batched = batch({ 'batch_items' => 3 }, pipeline) + returned = Automatic::Plugin::FilterLimit.new( + { 'max_items' => 1 }, batched + ).run + + returned.should have(1).feed + returned.first.items.should have(1).item + + item = returned.first.items.first + item.title.should == 'Batch 1' + item.link.should be_nil + item.description.should include('Title: A') + item.description.should include('Title: B') + item.description.should include('Title: C') + end + it 'accepts batch_items as a numeric string' do returned = batch({ 'batch_items' => '2' }, pipeline) diff --git a/spec/plugins/subscription/text_spec.rb b/spec/plugins/subscription/text_spec.rb index 4afa21a..93db206 100644 --- a/spec/plugins/subscription/text_spec.rb +++ b/spec/plugins/subscription/text_spec.rb @@ -5,7 +5,7 @@ # License:: The GPL version 3, or LGPL version 3 (Dual License). # Contact:: idnanashi@gmail.com # Created:: May 6, 2013 -# Updated:: Feb 19, 2014 +# Updated:: Sep 7, 2026 # Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers. require File.expand_path(File.dirname(__FILE__) + '../../../spec_helper') @@ -33,6 +33,16 @@ its(:run) { should have(1).feed } end + it "builds a title-only item with no placeholder link" do + returned = Automatic::Plugin::SubscriptionText.new( + { "titles" => ["hugehuge"] } + ).run + item = returned.first.items.first + + item.title.should == "hugehuge" + item.link.should be_nil + end + context "with urls whose return feed" do subject { Automatic::Plugin::SubscriptionText.new( @@ -43,6 +53,16 @@ its(:run) { should have(1).feed } end + it "builds a URL-only item with no placeholder title" do + returned = Automatic::Plugin::SubscriptionText.new( + { "urls" => ["http://hugehuge"] } + ).run + item = returned.first.items.first + + item.title.should be_nil + item.link.should == "http://hugehuge" + end + context "with feeds whose return feed" do subject { Automatic::Plugin::SubscriptionText.new( @@ -53,6 +73,16 @@ its(:run) { should have(1).feed } end + it "keeps an explicitly supplied title and URL" do + returned = Automatic::Plugin::SubscriptionText.new( + { "feeds" => [{ "title" => "huge", "url" => "http://hugehuge" }] } + ).run + item = returned.first.items.first + + item.title.should == "huge" + item.link.should == "http://hugehuge" + end + context "with feeds including full fields whose return feed" do subject { Automatic::Plugin::SubscriptionText.new(