Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions doc/PLUGINS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions doc/VERSIONS
Original file line number Diff line number Diff line change
Expand Up @@ -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)
-------------------
Expand Down
12 changes: 4 additions & 8 deletions lib/automatic/feed_maker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 = ''
Expand All @@ -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
Expand All @@ -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) }
Expand Down
34 changes: 28 additions & 6 deletions spec/lib/automatic/feed_maker_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -154,14 +170,20 @@ def build_item(link: "https://example.com/a", title: "A title",
item.content_encoded.should == "<p>Full body</p>"
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
20 changes: 19 additions & 1 deletion spec/plugins/filter/batch_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand Down
32 changes: 31 additions & 1 deletion spec/plugins/subscription/text_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand Down
Loading