Class: Uniword::Validation::Rules::DocumentContext

Inherits:
Object
  • Object
show all
Defined in:
lib/uniword/validation/rules/document_context.rb

Overview

Provides unified access to a DOCX package for validation rules.

Lazy-loads and caches parsed XML parts. Rules use this to access the document content without knowing about ZIP internals.

Examples:

Access a parsed part

context.document_xml  # => Moxml::Document
context.part_exists?("word/styles.xml")  # => true

Constant Summary collapse

W_NS =
"http://schemas.openxmlformats.org/wordprocessingml/2006/main"
R_NS =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
RELS_NS =
"http://schemas.openxmlformats.org/package/2006/relationships"
CT_NS =
"http://schemas.openxmlformats.org/package/2006/content-types"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path) ⇒ DocumentContext

Initialize context for a DOCX file.

Parameters:

  • path (String)

    Path to .docx file



28
29
30
31
32
33
# File 'lib/uniword/validation/rules/document_context.rb', line 28

def initialize(path)
  @path = path
  @zip = nil
  @parsed_parts = {}
  @moxml = Moxml.new(:nokogiri)
end

Instance Attribute Details

#parsed_partsObject (readonly)

Returns the value of attribute parsed_parts.



23
24
25
# File 'lib/uniword/validation/rules/document_context.rb', line 23

def parsed_parts
  @parsed_parts
end

#pathObject (readonly)

Returns the value of attribute path.



23
24
25
# File 'lib/uniword/validation/rules/document_context.rb', line 23

def path
  @path
end

Instance Method Details

#closeObject

Close the ZIP archive.



50
51
52
53
54
# File 'lib/uniword/validation/rules/document_context.rb', line 50

def close
  @zip&.close
  @zip = nil
  @parsed_parts.clear
end

#content_typesHash

Get all declared content types.

Returns:

  • (Hash)

    { extension => content_type, part_name => content_type }



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/uniword/validation/rules/document_context.rb', line 156

def content_types
  raw = part_raw("[Content_Types].xml")
  return {} unless raw

  doc = Nokogiri::XML(raw)
  types = {}

  doc.xpath("//xmlns:Default", "xmlns" => CT_NS).each do |node|
    types[node["Extension"]] = node["ContentType"] if node["Extension"]
  end

  doc.xpath("//xmlns:Override", "xmlns" => CT_NS).each do |node|
    types[node["PartName"]] = node["ContentType"] if node["PartName"]
  end

  types
end

#context_typeSymbol

Context type used by the Engine to select rules.

Returns:

  • (Symbol)

    :package — this context wraps an on-disk DOCX



38
39
40
# File 'lib/uniword/validation/rules/document_context.rb', line 38

def context_type
  :package
end

#document_xmlObject

Convenience: parsed word/document.xml



99
100
101
# File 'lib/uniword/validation/rules/document_context.rb', line 99

def document_xml
  part("word/document.xml")
end

#font_table_xmlObject

Convenience: parsed word/fontTable.xml



119
120
121
# File 'lib/uniword/validation/rules/document_context.rb', line 119

def font_table_xml
  part("word/fontTable.xml")
end

#modelUniword::Docx::Package?

Convenience: Uniword model-level access to the DOCX package. Lazy-loads via Package.from_file for model-driven validation rules.

Returns:



127
128
129
130
131
132
# File 'lib/uniword/validation/rules/document_context.rb', line 127

def model
  @model ||= Uniword::Docx::Package.from_file(@path)
rescue StandardError => e
  Uniword.logger&.debug { "Package model load failed: #{e.message}" }
  @model = nil
end

#numbering_idsSet<String>

Collect all numId values from numbering.xml.

Returns:

  • (Set<String>)

    numId values



191
192
193
194
195
196
197
198
199
200
# File 'lib/uniword/validation/rules/document_context.rb', line 191

def numbering_ids
  doc = numbering_xml
  return Set.new unless doc

  ids = Set.new
  doc.root.xpath(".//w:num/@w:numId", "w" => W_NS).each do |attr|
    ids << attr.value
  end
  ids
end

#numbering_xmlObject

Convenience: parsed word/numbering.xml



109
110
111
# File 'lib/uniword/validation/rules/document_context.rb', line 109

def numbering_xml
  part("word/numbering.xml")
end

#part(name) ⇒ Moxml::Document?

Get a parsed (Moxml) document for a part. Results are cached.

Parameters:

  • name (String)

    Part path

Returns:

  • (Moxml::Document, nil)


87
88
89
90
91
92
93
94
95
96
# File 'lib/uniword/validation/rules/document_context.rb', line 87

def part(name)
  return @parsed_parts[name] if @parsed_parts.key?(name)
  return @parsed_parts[name] = nil unless part_exists?(name)

  raw = part_raw(name)
  @parsed_parts[name] = raw ? @moxml.parse(raw) : nil
rescue StandardError => e
  Uniword.logger&.debug { "Part parse failed (#{name}): #{e.message}" }
  @parsed_parts[name] = nil
end

#part_exists?(name) ⇒ Boolean

Check if a part exists in the package.

Parameters:

  • name (String)

    Part path (e.g., "word/document.xml")

Returns:

  • (Boolean)


67
68
69
# File 'lib/uniword/validation/rules/document_context.rb', line 67

def part_exists?(name)
  !!zip.find_entry(name)
end

#part_raw(name) ⇒ String?

Get raw content of a part.

Parameters:

  • name (String)

    Part path

Returns:

  • (String, nil)

    Raw XML content



75
76
77
78
79
80
# File 'lib/uniword/validation/rules/document_context.rb', line 75

def part_raw(name)
  entry = zip.find_entry(name)
  return nil unless entry

  entry.get_input_stream.read
end

#relationships(rels_path = "word/_rels/document.xml.rels") ⇒ Array<Hash>

Get all relationships from a .rels file.

Parameters:

  • rels_path (String) (defaults to: "word/_rels/document.xml.rels")

    Path to .rels file

Returns:

  • (Array<Hash>)

    [{ id:, type:, target:, target_mode: }]



138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/uniword/validation/rules/document_context.rb', line 138

def relationships(rels_path = "word/_rels/document.xml.rels")
  raw = part_raw(rels_path)
  return [] unless raw

  doc = Nokogiri::XML(raw)
  doc.xpath("//xmlns:Relationship", "xmlns" => RELS_NS).map do |rel|
    {
      id: rel["Id"],
      type: rel["Type"],
      target: rel["Target"],
      target_mode: rel["TargetMode"],
    }
  end
end

#settings_xmlObject

Convenience: parsed word/settings.xml



114
115
116
# File 'lib/uniword/validation/rules/document_context.rb', line 114

def settings_xml
  part("word/settings.xml")
end

#style_idsSet<String>

Collect all style IDs from styles.xml.

Returns:

  • (Set<String>)

    Style ID values



177
178
179
180
181
182
183
184
185
186
# File 'lib/uniword/validation/rules/document_context.rb', line 177

def style_ids
  doc = styles_xml
  return Set.new unless doc

  ids = Set.new
  doc.root.xpath(".//w:style/@w:styleId", "w" => W_NS).each do |attr|
    ids << attr.value
  end
  ids
end

#styles_xmlObject

Convenience: parsed word/styles.xml



104
105
106
# File 'lib/uniword/validation/rules/document_context.rb', line 104

def styles_xml
  part("word/styles.xml")
end

#zipZip::File

Open the ZIP archive.

Returns:

  • (Zip::File)


45
46
47
# File 'lib/uniword/validation/rules/document_context.rb', line 45

def zip
  @zip ||= Zip::File.open(@path)
end

#zip_entriesArray<String>

List all entries in the ZIP.

Returns:

  • (Array<String>)

    Entry names



59
60
61
# File 'lib/uniword/validation/rules/document_context.rb', line 59

def zip_entries
  zip.entries.map(&:name)
end