From 95ca9e8b6b13acd4eeef6e62c98f5fa22e7f8b6a Mon Sep 17 00:00:00 2001 From: Lucas Carlson Date: Sat, 12 Sep 2026 10:08:42 -0700 Subject: [PATCH 1/2] fix: repair Atom links and add changelog Expose documented relation accessors alongside legacy keys. Extract relation URLs from direct child links without borrowing neighboring attributes or returning nested media markup. Preserve legacy link selection and document the additional hash and JSON keys. Backfill the changelog from tagged releases and version-changing commits through the first Git import. Include it in the gem and its metadata. Move shared guidance to AGENTS.md, import it from CLAUDE.md, and require changelog updates with behavior changes and releases. Closes #57 --- AGENTS.md | 87 +++++++++++++++++ CHANGELOG.md | 123 +++++++++++++++++++++++ CLAUDE.md | 56 +---------- README.md | 19 +++- lib/simple-rss.rb | 62 +++++++++++- simple-rss.gemspec | 3 +- test/base/relation_links_test.rb | 162 +++++++++++++++++++++++++++++++ test/data/atom_nested_link.xml | 13 +++ 8 files changed, 462 insertions(+), 63 deletions(-) create mode 100644 AGENTS.md create mode 100644 CHANGELOG.md create mode 100644 test/base/relation_links_test.rb create mode 100644 test/data/atom_nested_link.xml diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ddc17f3 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,87 @@ +# Repository Guidelines + +This file is the shared source of project instructions. Keep `CLAUDE.md` as an +`@AGENTS.md` import so both tools use the same guidance. + +## Commands + +```bash +bundle exec rake test # Run all tests +bundle exec ruby -Ilib:test test/base/base_test.rb # Run single test file +bundle exec rubocop # Lint +bundle exec rubocop -A # Auto-fix lint issues +bundle exec rbs-inline --output sig lib/ # Generate RBS from annotations +bundle exec rbs -I sig validate # Validate generated types +bundle exec steep check # Type check +bundle exec rake console # Interactive console +``` + +## Architecture + +Single-file library (`lib/simple-rss.rb`) using regex-based XML parsing for flexibility with malformed feeds. + +**Tag Syntax** (extend via `SimpleRSS.item_tags <<`): +- `tag` - simple element extraction +- `tag#attr` - attribute value (e.g., `media:content#url` → `media_content_url`) +- `tag+rel` - rel attribute matching (e.g., `link+alternate` → `link_alternate`) + +**Dynamic Accessors**: Feed tags become `attr_reader` methods at parse time. Item tags are hash keys with `method_missing` for dot notation. + +## Type Annotations + +Uses RBS inline syntax: +```ruby +# @rbs (String, Integer) -> Bool # method signature +attr_reader :name #: String # attribute type +@items = [] #: Array[untyped] # inline variable +``` + +## Changelog + +Update `CHANGELOG.md` in the same pull request as any user-visible feature, +behavior change, or bug fix. Record unreleased work under `## Unreleased` at the +top. Keep released versions newest first with headings formatted as +`## X.Y.Z - YYYY-MM-DD`. + +Write concise bullets that explain what changed for callers and why it matters. +Mention affected APIs, compatibility changes, and migration steps when relevant. +For bug fixes, describe the failing behavior and the corrected result. Link the +issue or pull request when useful. Include performance numbers only when they +were measured, and distinguish evidence from intended behavior. + +Do not copy commit logs into the changelog, claim planned work is implemented, +or add a release date before publishing. Correct inaccurate history when found; +do not silently move a change into a version that did not contain it. Routine +test, tooling, and prose-only edits do not need their own release-note bullet. + +When backfilling history, verify tag and commit boundaries, cite the source, and +distinguish source-history dates from package publication dates. Identify +intermediate source versions and the earliest repository import explicitly. + +## Release + +Before tagging a release, bump the version in both files: +- `simple-rss.gemspec` (`s.version`) +- `lib/simple-rss.rb` (`VERSION`) + +Both values must match. If they do not, CI may build and attempt to push the wrong gem version. + +Move the completed `Unreleased` notes into a section for that exact version and +the release date, then leave an empty `Unreleased` section for future work. +Check that the changelog, both version declarations, and the tag agree. Run the +test suite, RuboCop, RBS generation and validation, and Steep before publishing. +`Gemfile.lock` is ignored in this library; do not add it as part of a release. + +Then tag with `v*` to trigger automated RubyGems release: +```bash +git tag -a vX.Y.Z -m "Version X.Y.Z" +git push origin vX.Y.Z +``` + +Quick preflight checks: +```bash +rg -n "s.version" simple-rss.gemspec +rg -n "VERSION" lib/simple-rss.rb +rg '^## ' CHANGELOG.md +gem build simple-rss.gemspec +``` diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..286baf1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,123 @@ +# Changelog + +Entries through 2.2.0 were backfilled from Git history. Their dates identify +release tags or version-changing commits; older package publication dates can +differ. Linked source boundaries identify the evidence for each entry. The +repository begins with a 1.1 import, so earlier releases are not reconstructed. + +## Unreleased + +- Use the first valid `pubDate`, `updated`, or `published` timestamp in + `latest`, `items_since`, and merge ordering. Malformed dates no longer make + `latest` raise or hide a valid fallback. Preserve the original field values, + retain source order for equal dates, and place undated entries after dated + entries, including those before 1970. Preserve merge identity rules and the + placement of unidentified entries. ([#62](https://github.com/cardmagic/simple-rss/pull/62)) +- Make documented relation accessors such as `item.link_alternate` and + `item[:link_alternate]` return the link's `href`, including when attributes + precede `rel` or the link contains nested media. Keep relation matching within + direct child links of the current entry. Preserve legacy keys such as + `item[:"link+alternate"]` and the existing `item.link` behavior; hash and JSON + serialization now include both relation keys. + ([#57](https://github.com/cardmagic/simple-rss/issues/57)) + +## 2.2.0 - 2026-03-29 + +- Add `feed_type`, feed validation through instance and class `valid?` methods, + and filtering with `items_since`, `items_by_category`, and `search`. +- Add instance and class `merge` methods to combine feeds, deduplicate identified + entries, and order them by date. Add `diff` to report added and removed entries + between feed snapshots. +- Add item `has_media?` and `media_url` helpers, feed-level `enclosures` and + `images` collections, and parsing for `media:description`, `itunes:duration`, + and `itunes:image#href`. Treat blank media URLs as absent when choosing a + usable URL. ([Source](https://github.com/cardmagic/simple-rss/compare/v2.1.0...v2.2.0)) + +## 2.1.0 - 2025-12-29 + +- Add `SimpleRSS.fetch` with timeouts, custom headers, redirects, and conditional + GET support. Expose ETag and Last-Modified values and return `nil` for a + `304 Not Modified` response. +- Add `as_json`, `to_json`, and `to_hash`, serializing time values as ISO 8601. + Add `to_xml` to generate RSS 2.0 or Atom output from a parsed feed. +- Include `Enumerable` so feeds support iteration, mapping, and filtering. + Add index access with `feed[index]` and date ordering with `latest(count)`. + ([Source](https://github.com/cardmagic/simple-rss/compare/v2.0.0...v2.1.0)) + +## 2.0.0 - 2025-12-28 + +- Add the `array_tags` option to collect multiple values for selected item tags. +- Support extracting attributes from channel/feed and item/entry elements with + the `tag#attribute` syntax. +- Normalize parsed strings to UTF-8 and skip empty or whitespace-only tag + definitions that could make parsing hang. +- Add inline RBS type annotations, Steep validation, and GitHub Actions checks + for modern Ruby versions. + ([Source](https://github.com/cardmagic/simple-rss/compare/f5e879b...v2.0.0)) + +## 1.3.3 - 2018-04-23 + +- Parse `media:content#duration`, making video duration available through + `media_content_duration`. Synchronize the gemspec and library version at + 1.3.3. ([Source](https://github.com/cardmagic/simple-rss/compare/51f0eb6...f5e879b)) + +## 1.3.2 - 2015-08-17 + +- Stop forcing parsed content to binary encoding during unescaping. Apply CDATA + removal and whitespace trimming after either unescaping branch, and add UTF-8 + fixture coverage. This date follows the version change in source; + [RubyGems](https://rubygems.org/gems/simple-rss/versions/1.3.2) records package + publication in April 2018. + ([Source](https://github.com/cardmagic/simple-rss/compare/f3768bd...76a56c6)) + +## 1.3.1 - 2013-12-16 + +- Restore Ruby 1.8 compatibility by checking for `force_encoding` before calling + it while unescaping content. + ([Source](https://github.com/cardmagic/simple-rss/commit/f3768bd)) + +## 1.3.0 - 2013-12-16 + +- Make dynamically generated feed accessors usable on Ruby 1.9 without changing + the visibility of private parser methods. +- Adjust regular expressions and encoding handling for Ruby 1.9, including + suppression of UTF-8 regexp warnings. + ([Source](https://github.com/cardmagic/simple-rss/compare/aaf86ad...0e2ce64)) + +## 1.2.3 - 2010-07-06 + +- Add `tag#attribute` mapping for item child elements. Parse Media RSS content + URLs, types, dimensions, thumbnails, titles, credits, and categories through + the corresponding attribute and element accessors. + ([Source](https://github.com/cardmagic/simple-rss/compare/0915f34...aaf86ad)) + +## 1.2.2 - 2009-06-22 + +- Replace evaluation of feed-field assignments with `instance_variable_set` + and generate feed readers directly. Copy the options hash when constructing + the parser. ([Source](https://github.com/cardmagic/simple-rss/commit/0915f34)) + +## 1.2.1 - 2009-06-22 + +- Introduce an options hash for `parse` and initialization, including a + `marshalable` option that skips feed-reader generation. This intermediate + version is recorded in Git; a package publication was not verified. + ([Source](https://github.com/cardmagic/simple-rss/commit/c9757d2)) + +## 1.2 - 2009-02-25 + +- Add `tag+rel` matching and built-in Atom link relations for alternate, self, + edit, and replies links. Add `feedburner:origLink` support. +- Remove CDATA delimiters and trim whitespace even when percent-unescaping is + unnecessary. ([Source](https://github.com/cardmagic/simple-rss/compare/40f3ca5...fa54025)) + +## 1.1 - 2006-02-02 + +- Earliest repository import: parse RSS and Atom from strings or IO objects, + expose feed and item fields through method access, provide RSS/Atom aliases, + and allow callers to extend the recognized feed and item tags. + ([Source](https://github.com/cardmagic/simple-rss/commit/437b6a6)) +- Subsequent commits retaining version 1.1 make percent-unescaping conditional + and add gem packaging metadata. These changes postdate the initial import. + ([Unescaping](https://github.com/cardmagic/simple-rss/commit/ac95fb4), + [packaging](https://github.com/cardmagic/simple-rss/commit/379f639)) diff --git a/CLAUDE.md b/CLAUDE.md index 529c522..43c994c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,55 +1 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Commands - -```bash -bundle exec rake test # Run all tests -bundle exec ruby -Ilib:test test/base/base_test.rb # Run single test file -bundle exec rubocop # Lint -bundle exec rubocop -A # Auto-fix lint issues -bundle exec rbs-inline --output sig lib/ # Generate RBS from annotations -bundle exec steep check # Type check -bundle exec rake console # Interactive console -``` - -## Architecture - -Single-file library (`lib/simple-rss.rb`) using regex-based XML parsing for flexibility with malformed feeds. - -**Tag Syntax** (extend via `SimpleRSS.item_tags <<`): -- `tag` - simple element extraction -- `tag#attr` - attribute value (e.g., `media:content#url` → `media_content_url`) -- `tag+rel` - rel attribute matching (e.g., `link+alternate` → `link_alternate`) - -**Dynamic Accessors**: Feed tags become `attr_reader` methods at parse time. Item tags are hash keys with `method_missing` for dot notation. - -## Type Annotations - -Uses RBS inline syntax: -```ruby -# @rbs (String, Integer) -> Bool # method signature -attr_reader :name #: String # attribute type -@items = [] #: Array[untyped] # inline variable -``` - -## Release - -Before tagging a release, bump the version in both files: -- `simple-rss.gemspec` (`s.version`) -- `lib/simple-rss.rb` (`VERSION`) - -Both values must match. If they do not, CI may build and attempt to push the wrong gem version. - -Then tag with `v*` to trigger automated RubyGems release: -```bash -git tag -a v1.3.4 -m "v1.3.4" && git push origin v1.3.4 -``` - -Quick preflight checks: -```bash -grep -n "s.version" simple-rss.gemspec -grep -n "VERSION" lib/simple-rss.rb -gem build simple-rss.gemspec -``` +@AGENTS.md diff --git a/README.md b/README.md index 079e1bb..749ceb8 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,11 @@ A simple, flexible, extensible, and liberal RSS and Atom reader for Ruby. Design - Extensible tag definitions - Zero runtime dependencies -## What's New in 2.0 +## What's New in 2.x -Version 2.0 is a major update with powerful new capabilities: +See the [changelog](CHANGELOG.md) for release history and unreleased changes. + +The 2.x releases add: - **URL Fetching** - One-liner feed fetching with `SimpleRSS.fetch(url)`. Supports timeouts, custom headers, and automatic redirect following. @@ -237,6 +239,19 @@ SimpleRSS.item_tags << :"entry#xml:lang" | `tag#attr` | `:"media:content#url"` | `.media_content_url` | Attribute value | | `tag+rel` | `:"link+alternate"` | `.link_alternate` | Element with specific `rel` attribute | +Relation tags provide both underscore and legacy `+` hash keys. For example, +`item.link_alternate`, `item[:link_alternate]`, and `item[:"link+alternate"]` +return the same parsed value. Both keys appear in `to_hash`, `as_json`, and +`to_json`; they are ordinary hash entries, so reassigning one does not update the +other. The built-in link relations are `alternate`, `self`, `edit`, and `replies`. + +Link relation accessors read `href` from the first direct child link with the +requested explicit `rel` attribute. Attribute order and child markup do not +affect the URL. Links inside source metadata, content, or other nested elements +are excluded. A missing relation or `href` returns `nil`. Relative href values +remain relative. `item.link` retains its existing extraction behavior; it does +not select a canonical article URL across relations or feed formats. + ### Collecting Multiple Values By default, SimpleRSS returns only the first occurrence of each tag. To collect all values: diff --git a/lib/simple-rss.rb b/lib/simple-rss.rb index b364c56..08328de 100644 --- a/lib/simple-rss.rb +++ b/lib/simple-rss.rb @@ -445,15 +445,67 @@ def parse_item_tag(item, tag, content, item_attrs = nil) # @rbs (Hash[Symbol, untyped], String, String) -> void def parse_rel_tag(item, tag_str, content) - tag, rel = tag_str.split("+") + tag, rel = tag_str.split("+", 2) return unless tag && rel - content =~ %r{<(rss:|atom:)?#{tag}(.*?)rel=['"]#{rel}['"](.*?)>(.*?)}mi || - content =~ %r{<(rss:|atom:)?#{tag}(.*?)rel=['"]#{rel}['"](.*?)/\s*>}mi + value = if tag == "link" + link_relation_href(content, rel) + else + content =~ %r{<(rss:|atom:)?#{tag}(.*?)rel=['"]#{rel}['"](.*?)>(.*?)}mi || + content =~ %r{<(rss:|atom:)?#{tag}(.*?)rel=['"]#{rel}['"](.*?)/\s*>}mi - return unless Regexp.last_match(3) || Regexp.last_match(4) + return unless Regexp.last_match(3) || Regexp.last_match(4) - item[clean_tag("#{tag}+#{rel}")] = clean_content(tag.to_sym, Regexp.last_match(3), Regexp.last_match(4)) + clean_content(tag.to_sym, Regexp.last_match(3), Regexp.last_match(4)) + end + return if value.nil? + + item[clean_tag("#{tag}+#{rel}")] = value + item[clean_tag("#{tag}_#{rel}")] = value + end + + # @rbs (String, String) -> String? + def link_relation_href(content, relation) + attributes = entry_link_attributes(content).find { |link| link["rel"]&.casecmp?(relation) } + return unless attributes + + attributes["href"] + end + + # @rbs (String) -> Array[Hash[String, String]] + def entry_link_attributes(content) + links = [] #: Array[Hash[String, String]] + depth = 0 + tokens = %r{||<\?.*?\?>|<(/?)([\w:.-]+)((?:[^<>"']|"[^"]*"|'[^']*')*)>}m + + content.scan(tokens) do + closing = Regexp.last_match(1) + tag = Regexp.last_match(2) + attributes = Regexp.last_match(3) + next unless tag + next unless attributes + + if closing == "/" + depth = [depth - 1, 0].max + next + end + + links << xml_attributes(attributes) if depth.zero? && %w[link atom:link rss:link].include?(tag.downcase) + depth += 1 unless attributes.rstrip.end_with?("/") + end + + links + end + + # @rbs (String) -> Hash[String, String] + def xml_attributes(attributes) + values = {} #: Hash[String, String] + attributes.scan(/([\w:.-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/m) do + name = Regexp.last_match(1) + value = Regexp.last_match(2) || Regexp.last_match(3) + values[name.downcase] = value if name && value + end + values end # @rbs (String, String?) -> void diff --git a/simple-rss.gemspec b/simple-rss.gemspec index 0b3f750..5a97deb 100644 --- a/simple-rss.gemspec +++ b/simple-rss.gemspec @@ -5,9 +5,10 @@ Gem::Specification.new do |s| s.summary = "A simple, flexible, extensible, and liberal RSS and Atom reader for Ruby. It is designed to be backwards compatible with the standard RSS parser, but will never do RSS generation." s.email = "lucas@rufy.com" s.homepage = "https://github.com/cardmagic/simple-rss" + s.metadata["changelog_uri"] = "https://github.com/cardmagic/simple-rss/blob/master/CHANGELOG.md" s.description = "A simple, flexible, extensible, and liberal RSS and Atom reader for Ruby. It is designed to be backwards compatible with the standard RSS parser, but will never do RSS generation." s.authors = ["Lucas Carlson"] - s.files = Dir["lib/**/*", "test/**/*", "LICENSE", "README.md", "Rakefile", "simple-rss.gemspec"] + s.files = Dir["lib/**/*", "test/**/*", "LICENSE", "README.md", "CHANGELOG.md", "Rakefile", "simple-rss.gemspec"] s.required_ruby_version = ">= 3.1" s.add_development_dependency "rake" s.add_development_dependency "rdoc" diff --git a/test/base/relation_links_test.rb b/test/base/relation_links_test.rb new file mode 100644 index 0000000..c09f9a9 --- /dev/null +++ b/test/base/relation_links_test.rb @@ -0,0 +1,162 @@ +require "test_helper" +require "json" + +class RelationLinksTest < Test::Unit::TestCase + def test_documented_accessor_and_legacy_key_return_the_same_link + feed = parse_entry(<<~XML) + + + XML + item = feed.first + + assert_equal "https://example.com/posts/1", item.link_alternate + assert_equal item.link_alternate, item[:link_alternate] + assert_equal item.link_alternate, item[:"link+alternate"] + assert_equal "https://example.com/api/1", item.link + end + + def test_relations_accept_attribute_order_quotes_whitespace_and_paired_tags + item = parse_entry(<<~XML).first + + + Edit entry + + XML + + assert_equal "/posts/1?view=full", item.link_alternate + assert_equal "https://example.com/api/1", item.link_self + assert_equal "https://example.com/edit/1", item.link_edit + assert_equal "/posts/1/replies", item.link_replies + %w[alternate self edit replies].each do |relation| + assert_equal item[:"link_#{relation}"], item[:"link+#{relation}"] + end + end + + def test_nested_media_does_not_replace_the_link_href + source = File.read(File.join(__dir__, "../data/atom_nested_link.xml")) + feed = SimpleRSS.parse(source) + + assert_equal "https://example.com/story", feed.first.link_alternate + assert_equal "https://example.com/story", feed.first[:"link+alternate"] + assert_equal source, feed.source + end + + def test_relations_do_not_borrow_neighboring_attributes_or_content + item = parse_entry(<<~XML).first + + + + + XML + + assert_equal "/self", item.link_self + assert_equal "/replies", item.link_replies + assert_nil item.link_alternate + assert_nil item[:"link+alternate"] + assert_nil item.link_edit + end + + def test_relations_ignore_nested_source_content_comments_and_cdata + item = parse_entry(<<~XML).first + +
+ + ]]> + ?> + + + XML + + assert_equal "/entry", item.link_alternate + assert_nil item.link_self + assert_nil item.link_edit + assert_nil item.link_replies + end + + def test_relations_match_exact_element_and_attribute_names + item = parse_entry(<<~XML).first + + + + + + + XML + + assert_equal "/correct", item.link_alternate + assert_nil item.link_self + end + + def test_relations_stay_within_their_entry + feed = SimpleRSS.parse <<~XML + + Separate entries + + First + Second + + XML + + assert_equal "/first", feed.first.link_self + assert_nil feed.first.link_edit + assert_equal "/second", feed[1].link_edit + assert_nil feed[1].link_self + end + + def test_first_matching_relation_wins + item = parse_entry(<<~XML).first + + + XML + + assert_equal "/first", item.link_alternate + end + + def test_relations_preserve_case_insensitive_matching + item = parse_entry('').first + + assert_equal "/article", item.link_alternate + assert_equal "/article", item[:"link+alternate"] + end + + def test_hash_and_json_serialization_include_both_relation_keys + feed = parse_entry('') + expected = { title: "Entry", link: "/article", "link+alternate": "/article", link_alternate: "/article" } + + assert_equal expected, feed.first + assert_equal expected, feed.to_hash[:items].first + assert_equal expected, feed.as_json[:items].first + assert_equal expected, JSON.parse(feed.to_json, symbolize_names: true)[:items].first + end + + def test_custom_relations_preserve_legacy_keys_and_unrelated_tag_names + original_tags = SimpleRSS.item_tags.dup + SimpleRSS.item_tags.push(:"link+enclosure", :"custom:label+featured", :"custom:value") + item = parse_entry(<<~XML).first + + Featured + Unchanged + XML + + assert_equal "/episode.mp3", item.link_enclosure + assert_equal "/episode.mp3", item[:"link+enclosure"] + assert_equal "Featured", item.custom_label_featured + assert_equal "Featured", item[:"custom_label+featured"] + assert_equal "Unchanged", item.custom_value + ensure + SimpleRSS.item_tags.replace(original_tags) + end + + private + + def parse_entry(content) + SimpleRSS.parse <<~XML + + Relation links + Entry#{content} + + XML + end +end diff --git a/test/data/atom_nested_link.xml b/test/data/atom_nested_link.xml new file mode 100644 index 0000000..a8f90e4 --- /dev/null +++ b/test/data/atom_nested_link.xml @@ -0,0 +1,13 @@ + + Nested link media + + Story + + + + Thumbnail + + + + + From 8661e42d5810f52cf093170f006457220771c09a Mon Sep 17 00:00:00 2001 From: Lucas Carlson Date: Sat, 12 Sep 2026 10:14:42 -0700 Subject: [PATCH 2/2] refactor: simplify relation token guards Use the attribute capture to skip non-element tokens with one guard. Cover paired elements whose quoted URLs end in a slash. The case already preserves nesting because the captured closing quote follows the slash. --- lib/simple-rss.rb | 5 ++--- test/base/relation_links_test.rb | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/simple-rss.rb b/lib/simple-rss.rb index 08328de..5362efb 100644 --- a/lib/simple-rss.rb +++ b/lib/simple-rss.rb @@ -480,9 +480,8 @@ def entry_link_attributes(content) content.scan(tokens) do closing = Regexp.last_match(1) - tag = Regexp.last_match(2) + tag = Regexp.last_match(2)&.downcase attributes = Regexp.last_match(3) - next unless tag next unless attributes if closing == "/" @@ -490,7 +489,7 @@ def entry_link_attributes(content) next end - links << xml_attributes(attributes) if depth.zero? && %w[link atom:link rss:link].include?(tag.downcase) + links << xml_attributes(attributes) if depth.zero? && %w[link atom:link rss:link].include?(tag) depth += 1 unless attributes.rstrip.end_with?("/") end diff --git a/test/base/relation_links_test.rb b/test/base/relation_links_test.rb index c09f9a9..f994f4d 100644 --- a/test/base/relation_links_test.rb +++ b/test/base/relation_links_test.rb @@ -60,8 +60,8 @@ def test_relations_do_not_borrow_neighboring_attributes_or_content def test_relations_ignore_nested_source_content_comments_and_cdata item = parse_entry(<<~XML).first - -
+ +
]]> ?>