From b8dfcb6cd705f7f45bb630f8c23da520e04b6c35 Mon Sep 17 00:00:00 2001 From: Tatsuya Sato Date: Sun, 18 Oct 2026 01:08:05 +0900 Subject: [PATCH] Read documents that have no relationships part (#192) load_rels already rescues a missing word/_rels/document*.xml.rels and leaves @rels nil, but hyperlink_relationships then called @rels.xpath, so #hyperlinks, and with it #paragraphs/#tables/#to_s, raised NoMethodError. Return an empty NodeSet instead, so #hyperlinks is {}. Add a fixture_without spec helper that drops zip entries from a fixture in memory, so such documents need no extra binary fixture. Co-Authored-By: Claude Opus 5.5 --- lib/docx/document.rb | 3 +++ spec/docx/document_spec.rb | 16 ++++++++++++++++ spec/support/fixture_helpers.rb | 15 +++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/lib/docx/document.rb b/lib/docx/document.rb index 5cd222e..7900ad0 100755 --- a/lib/docx/document.rb +++ b/lib/docx/document.rb @@ -113,6 +113,9 @@ def hyperlinks end def hyperlink_relationships + # @rels is nil when the document has no relationships part (#192) + return Nokogiri::XML::NodeSet.new(Nokogiri::XML::Document.new) unless @rels + @rels.xpath("//xmlns:Relationship[contains(@Type,'hyperlink')]") end diff --git a/spec/docx/document_spec.rb b/spec/docx/document_spec.rb index cf9a49c..007cf02 100755 --- a/spec/docx/document_spec.rb +++ b/spec/docx/document_spec.rb @@ -157,6 +157,22 @@ expect { doc.hyperlinks }.not_to raise_error expect(doc.hyperlinks).to be_a(Hash) end + + # Regression tests for #192 + context 'when the document has no relationships part' do + let(:doc) do + described_class.open(fixture_without('basic.docx', 'word/_rels/document.xml.rels')) + end + + it 'is empty' do + expect(doc.hyperlinks).to eq({}) + end + + it 'still reads the document' do + expect(doc.paragraphs.map(&:text)).to eq(%w[hello world]) + expect(doc.to_html).to include('hello') + end + end end describe '#document_properties' do diff --git a/spec/support/fixture_helpers.rb b/spec/support/fixture_helpers.rb index 416d375..dd506c2 100644 --- a/spec/support/fixture_helpers.rb +++ b/spec/support/fixture_helpers.rb @@ -9,6 +9,21 @@ def fixture_path(name) File.join(FIXTURES_DIR, name) end + # The binary contents of fixture +name+ with the given zip entries left out, + # for testing documents that lack a part (open it with Docx::Document.open). + def fixture_without(name, *excluded_entries) + Zip::File.open(fixture_path(name)) do |zip| + Zip::OutputStream.write_buffer do |out| + zip.each do |entry| + next if !entry.file? || excluded_entries.include?(entry.name) + + out.put_next_entry(entry.name) + out.write(zip.read(entry.name)) + end + end.string + end + end + # A path inside a per-example temporary directory, so saved documents never # land in spec/fixtures and are always cleaned up. def tmp_path(name)