From 06e5ad8393f900cea103a3f81d819ca37734f230 Mon Sep 17 00:00:00 2001 From: Tatsuya Sato Date: Sun, 31 May 2026 23:07:46 +0900 Subject: [PATCH] Decouple load_rels from load_styles (#158) Previously load_rels was called from inside load_styles, so when a document had no word/styles.xml, load_styles raised Errno::ENOENT, was rescued, and load_rels never ran -- leaving @rels nil. Any later call to #hyperlinks then crashed with "undefined method 'xpath' for nil". load_rels is now called independently from initialize and handles a missing rels file gracefully, so relationships load regardless of whether styles.xml is present. Adds a regression fixture (no_styles_with_hyperlink.docx: a document with a hyperlink relationship but no styles.xml) and a spec asserting #hyperlinks works in that case. Co-Authored-By: Claude Opus 4.8 --- lib/docx/document.rb | 7 ++++++- spec/docx/document_spec.rb | 14 ++++++++++++++ spec/fixtures/no_styles_with_hyperlink.docx | Bin 0 -> 9089 bytes 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 spec/fixtures/no_styles_with_hyperlink.docx diff --git a/lib/docx/document.rb b/lib/docx/document.rb index bd2d659..498360d 100755 --- a/lib/docx/document.rb +++ b/lib/docx/document.rb @@ -40,6 +40,7 @@ def initialize(path_or_io, options = {}) @document_xml = document.get_input_stream.read @doc = Nokogiri::XML(@document_xml) load_styles + load_rels load_headers load_footers yield(self) if block_given? @@ -226,18 +227,22 @@ def load_footers def load_styles @styles_xml = @zip.read('word/styles.xml') @styles = Nokogiri::XML(@styles_xml) - load_rels rescue Errno::ENOENT => e warn e.message nil end + # Loaded independently of styles so that a document without word/styles.xml + # still initializes @rels (see #158). def load_rels rels_entry = @zip.glob('word/_rels/document*.xml.rels').first raise Errno::ENOENT unless rels_entry @rels_xml = rels_entry.get_input_stream.read @rels = Nokogiri::XML(@rels_xml) + rescue Errno::ENOENT => e + warn e.message + nil end #-- diff --git a/spec/docx/document_spec.rb b/spec/docx/document_spec.rb index d6144ef..2f2d1b7 100755 --- a/spec/docx/document_spec.rb +++ b/spec/docx/document_spec.rb @@ -612,6 +612,20 @@ end end + # Regression test for #158: relationships (and therefore hyperlinks) must be + # loaded independently of styles, so a document without word/styles.xml does + # not leave @rels uninitialized. + describe 'reading relationships when styles.xml is missing' do + before do + @doc = Docx::Document.open(@fixtures_path + '/no_styles_with_hyperlink.docx') + end + + it 'still loads hyperlink relationships' do + expect { @doc.hyperlinks }.to_not raise_error + expect(@doc.hyperlinks).to eq('rId4' => 'http://www.google.com/') + end + end + describe 'replacing contents' do let(:replacement_file_path) { @fixtures_path + '/replacement.png' } let(:temp_file_path) { Tempfile.new(['docx_gem', '.docx']).path } diff --git a/spec/fixtures/no_styles_with_hyperlink.docx b/spec/fixtures/no_styles_with_hyperlink.docx new file mode 100644 index 0000000000000000000000000000000000000000..7c42bd28489c52a06dca7e5afb132a0e6f64f3f1 GIT binary patch literal 9089 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 U9OQ%n0A$E76cP;gDSrF+fA0simH+?% literal 0 HcmV?d00001