uniword 1.4.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e697ff947a11616fb287bf07ce59d09216921223355b7e9315d81947a48e1bc4
4
- data.tar.gz: 10999851891264d1f1f75d8cb74d06966d790c5f464a82cf9c168fffddb57666
3
+ metadata.gz: b6093ee22688fc357250708811cdb7035b17ff9a036ec5dd48f86381632b2c72
4
+ data.tar.gz: b76ab89a27b22a8a7ed5293f771cedaa165c3263a853f3322eb9f3d06cd0625a
5
5
  SHA512:
6
- metadata.gz: 4043eba31c5334bc6b4770cd10e51dc3491a4ae76ee5f286e35131e415ee284f392c55aaa521980972973f511a29191c6eb2cf58fbea47d6e52aae0fd8140890
7
- data.tar.gz: fc8c60a46ee435a07f26c536991863e8bfc0ba9f16e6f4ba95c24c27e0ad65a1078cc455fc78f336e65f9fb54bf89356b0071ecdb8a28ca0842f8d92b46706c8
6
+ metadata.gz: dedfcb861076ff92eb99dda31c460e02a4c184fd9ce271abdfc63250dd100235f0889e4705899d2f4566eefa43b329094d77128f9ff9deeae3ff635ca4051218
7
+ data.tar.gz: 226c5d1acc52df7f5519935ab2817b73ac71382fb95132e66cd6bfc4f1d912dba604996d0e1eff9996791f0bf0a38df121d27d54e5a012c1236d582580f93484
data/CHANGELOG.md CHANGED
@@ -8,6 +8,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [1.5.0] - 2026-07-22
12
+
13
+ ### Added
14
+
15
+ - `Configuration#on_noncompliant_content` policy knob (default
16
+ `:strip`, alternative `:raise`). Controls what happens when a
17
+ loaded package contains parts with no content type declaration.
18
+ - `Docx::JunkClassifier` — classifies a package-relative path as
19
+ junk based on OS/tooling artifact patterns and the OPC "no content
20
+ type AND no referencing relationship" rule. Single source of truth
21
+ for the junk decision.
22
+ - `Docx::StrippedPart` value object (path + reason) — the reporting
23
+ record for parts stripped at load.
24
+ - `ContentTypes::Types#content_type_for(path)` — Override-then-Default
25
+ lookup. Single source of truth for content type resolution,
26
+ replacing duplicated lookup logic.
27
+ - `Package#stripped_parts` — `Array<StrippedPart>` populated by the
28
+ loader in `:strip` mode. Empty in `:raise` mode and for
29
+ programmatically-constructed packages.
30
+
31
+ ### Changed
32
+
33
+ - **Default behavior change**: `Docx::RawPartLoader` now strips
34
+ non-compliant parts at load by default (Word-identical behavior).
35
+ Previously, parts with no content type declaration were preserved
36
+ as `RawPart(content_type: nil)`, which then tripped OPC-005 at the
37
+ write-time integrity gate. Legitimate unmodelled parts (those with
38
+ a content type declaration, e.g. VBA, glossary, `docProps/meta.xml`)
39
+ continue to round-trip byte-for-byte per the 1.4.0 promise.
40
+ - Stripped parts are recorded on `Package#stripped_parts` for caller
41
+ introspection. Set `Configuration#on_noncompliant_content = :raise`
42
+ to preserve the previous behavior for QA pipelines that want to
43
+ catch every deviation.
44
+
45
+ ### Fixed
46
+
47
+ - Loading then saving a DOCX with `[trash]/*.dat` or other
48
+ undeclared parts no longer fails with
49
+ `OPC-005 No content type declared` at save. The junk is stripped
50
+ at load and reported via `Package#stripped_parts`.
51
+
52
+ ### Removed
53
+
54
+ - `FALLBACK_RAW_PART_CONTENT_TYPE` constant and the
55
+ `application/octet-stream` fallback in
56
+ `Docx::PackageSerialization#inject_raw_part_content_types`. The
57
+ fallback preserved junk that Word would strip and masked genuinely
58
+ missing declarations. (Introduced in 1.4.1; reverted before
59
+ broader release.)
60
+
11
61
  ## [1.4.0] - 2026-07-20
12
62
 
13
63
  ### Added
@@ -3,7 +3,7 @@
3
3
  module Uniword
4
4
  # Global runtime configuration for Uniword.
5
5
  #
6
- # Holds save-path policy as plain, explicitly typed boolean attributes.
6
+ # Holds save-path policy as plain, explicitly typed attributes.
7
7
  # This object is runtime policy only: it has no file-loading behavior
8
8
  # (see ConfigurationLoader for external YAML config files) and no
9
9
  # serialization behavior.
@@ -22,6 +22,15 @@ module Uniword
22
22
  autoload :ConfigurationLoader,
23
23
  "#{__dir__}/configuration/configuration_loader"
24
24
 
25
+ # Accepted values for `on_noncompliant_content`.
26
+ #
27
+ # `:strip` — Word-identical default: non-compliant parts are
28
+ # dropped at load and recorded on `Package#stripped_parts`.
29
+ # `:raise` — strict: parts are preserved; the save-time
30
+ # `PackageIntegrityChecker` raises `Uniword::ValidationError`
31
+ # with structured OPC-005 issues.
32
+ ON_NONCOMPLIANT_MODES = %i[strip raise].freeze
33
+
25
34
  # Whether documents are validated when saved.
26
35
  #
27
36
  # @return [Boolean]
@@ -37,10 +46,16 @@ module Uniword
37
46
  # @return [Boolean]
38
47
  attr_reader :log_save_fixes
39
48
 
49
+ # Load-time policy for non-compliant parts (no content type
50
+ # declaration, OS artifact, ...).
51
+ #
52
+ # @return [Symbol] `:strip` (default) or `:raise`
53
+ attr_reader :on_noncompliant_content
54
+
40
55
  # Create a configuration with default policy values.
41
56
  #
42
57
  # Defaults: validate_on_save: true, xsd_validation: false,
43
- # log_save_fixes: true.
58
+ # log_save_fixes: true, on_noncompliant_content: :strip.
44
59
  def initialize
45
60
  reset!
46
61
  end
@@ -52,6 +67,7 @@ module Uniword
52
67
  @validate_on_save = true
53
68
  @xsd_validation = false
54
69
  @log_save_fixes = true
70
+ @on_noncompliant_content = :strip
55
71
  self
56
72
  end
57
73
 
@@ -82,6 +98,17 @@ module Uniword
82
98
  @log_save_fixes = typed_boolean(value, :log_save_fixes)
83
99
  end
84
100
 
101
+ # Set the non-compliant-content policy. Accepts symbols or
102
+ # strings; stored as a symbol.
103
+ #
104
+ # @param value [Symbol, String] `:strip` or `:raise`
105
+ # @return [Symbol] the value set
106
+ # @raise [ArgumentError] if value is not one of
107
+ # `ON_NONCOMPLIANT_MODES`
108
+ def on_noncompliant_content=(value)
109
+ @on_noncompliant_content = typed_mode(value, :on_noncompliant_content)
110
+ end
111
+
85
112
  private
86
113
 
87
114
  # Validate that a value is strictly boolean.
@@ -96,5 +123,31 @@ module Uniword
96
123
  raise ArgumentError,
97
124
  "#{name} must be true or false, got #{value.inspect}"
98
125
  end
126
+
127
+ # Validate that a value is one of the accepted non-compliant modes.
128
+ #
129
+ # @param value [Object] value to check (Symbol or String)
130
+ # @param name [Symbol] attribute name used in the error message
131
+ # @return [Symbol] the value as a symbol
132
+ # @raise [ArgumentError] if value is not in ON_NONCOMPLIANT_MODES
133
+ def typed_mode(value, name)
134
+ sym = symbol_value(value)
135
+ return sym if ON_NONCOMPLIANT_MODES.include?(sym)
136
+
137
+ raise ArgumentError,
138
+ "#{name} must be one of #{ON_NONCOMPLIANT_MODES.inspect}, " \
139
+ "got #{value.inspect}"
140
+ end
141
+
142
+ # Coerce a value to a Symbol when it is a Symbol or String.
143
+ #
144
+ # @param value [Object]
145
+ # @return [Symbol, nil] nil when value is neither Symbol nor String
146
+ def symbol_value(value)
147
+ return value if value.is_a?(Symbol)
148
+ return value.to_sym if value.is_a?(String)
149
+
150
+ nil
151
+ end
99
152
  end
100
153
  end
@@ -18,6 +18,33 @@ module Uniword
18
18
  map_element "Default", to: :defaults, render_nil: false
19
19
  map_element "Override", to: :overrides, render_nil: false
20
20
  end
21
+
22
+ # Resolve the content type declared for a package-relative path.
23
+ #
24
+ # Override entries win; the Default extension match is the
25
+ # fallback. Returns nil when neither matches (the part is
26
+ # non-compliant per OPC).
27
+ #
28
+ # @param path [String] package-relative path (e.g.
29
+ # "word/document.xml")
30
+ # @return [String, nil] content type, or nil when undeclared
31
+ def content_type_for(path)
32
+ override_content_type(path) || default_content_type(path)
33
+ end
34
+
35
+ private
36
+
37
+ def override_content_type(path)
38
+ part_name = "/#{path}"
39
+ overrides.find { |o| o.part_name == part_name }&.content_type
40
+ end
41
+
42
+ def default_content_type(path)
43
+ ext = File.extname(path)[1..]
44
+ return nil unless ext
45
+
46
+ defaults.find { |d| d.extension == ext }&.content_type
47
+ end
21
48
  end
22
49
  end
23
50
  end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uniword
4
+ module Docx
5
+ # Classify a package-relative path as junk or legitimate.
6
+ #
7
+ # Used by `Docx::PartLoader::RawPartLoader` to decide which
8
+ # unclaimed ZIP entries to carry as `RawPart` and which to strip.
9
+ # The classifier is the single source of truth for the junk
10
+ # decision — adding new criteria means changing this class (or its
11
+ # pattern constants), not the loader.
12
+ #
13
+ # Classification is two-armed:
14
+ #
15
+ # 1. *OS/tooling artifact* — path matches a known pattern
16
+ # (`__MACOSX/`, `.DS_Store`, `Thumbs.db`, `._*`, `~$*`). These
17
+ # are never legitimate document content; matched unconditionally
18
+ # even when a Default content type happens to apply.
19
+ #
20
+ # 2. *Undeclared part* — no Override and no Default extension match
21
+ # in the loaded `ContentTypes::Types`, AND no modelled
22
+ # relationship targets the path. This is the OPC rule: every
23
+ # part must have a content type declaration.
24
+ #
25
+ # Both arms return a human-readable reason string; the loader
26
+ # records it on `Package#stripped_parts` for caller introspection.
27
+ #
28
+ # @example
29
+ # classifier = JunkClassifier.new(
30
+ # content_types: package.content_types,
31
+ # relationships_by_path: rel_targets,
32
+ # )
33
+ # classifier.reason("[trash]/0000.dat")
34
+ # # => "No content type declaration and no referencing relationship"
35
+ # classifier.reason("word/document.xml") # => nil
36
+ class JunkClassifier
37
+ # Path patterns for OS / tooling artifacts that are never
38
+ # legitimate document content. Append to extend; the classifier
39
+ # iterates them in order.
40
+ OS_ARTIFACT_PATTERNS = [
41
+ /\A__MACOSX\//, # macOS zip metadata directory
42
+ /\A\.DS_Store\z/, # macOS Finder
43
+ /Thumbs\.db\z/, # Windows Explorer
44
+ /\A\._/, # macOS AppleDouble resource fork
45
+ /\A~\$/, # Office lock files
46
+ ].freeze
47
+
48
+ OS_ARTIFACT_REASON = "OS or tooling artifact"
49
+
50
+ UNDECLARED_REASON =
51
+ "No content type declaration and no referencing relationship"
52
+
53
+ # @param content_types [ContentTypes::Types, nil] the loaded
54
+ # content types model (nil when absent — every path is then
55
+ # undeclared unless a relationship targets it)
56
+ # @param relationships_by_path [Hash{String => true}, nil] set of
57
+ # paths targeted by some modelled relationship. Encoded as a
58
+ # hash for O(1) lookup; nil means "no relationships recorded"
59
+ def initialize(content_types:, relationships_by_path: nil)
60
+ @content_types = content_types
61
+ @relationships_by_path = relationships_by_path || {}
62
+ end
63
+
64
+ # @param path [String] package-relative path
65
+ # @return [String, nil] reason string when the path is junk;
66
+ # nil when it is legitimate
67
+ def reason(path)
68
+ os_artifact_reason(path) || undeclared_reason(path)
69
+ end
70
+
71
+ # @param path [String] package-relative path
72
+ # @return [Boolean] true when the path is junk
73
+ def junk?(path)
74
+ !reason(path).nil?
75
+ end
76
+
77
+ private
78
+
79
+ def os_artifact_reason(path)
80
+ OS_ARTIFACT_REASON if OS_ARTIFACT_PATTERNS.any? do |pattern|
81
+ pattern.match?(path)
82
+ end
83
+ end
84
+
85
+ def undeclared_reason(path)
86
+ return nil if content_type_declared?(path)
87
+ return nil if referenced_by_relationship?(path)
88
+
89
+ UNDECLARED_REASON
90
+ end
91
+
92
+ def content_type_declared?(path)
93
+ @content_types&.content_type_for(path)
94
+ end
95
+
96
+ def referenced_by_relationship?(path)
97
+ @relationships_by_path.key?(path)
98
+ end
99
+ end
100
+ end
101
+ end
@@ -146,6 +146,29 @@ module Uniword
146
146
  @applied_fixes ||= []
147
147
  end
148
148
 
149
+ # Parts stripped at load because they were non-compliant (no
150
+ # content type declaration, OS artifact, ...). Populated by
151
+ # `Docx::PartLoader::RawPartLoader` when
152
+ # `Uniword.configuration.on_noncompliant_content` is `:strip`
153
+ # (the default). Empty in `:raise` mode and for packages
154
+ # constructed by hand.
155
+ #
156
+ # @return [Array<StrippedPart>] path + reason for every stripped
157
+ # part, in load order
158
+ def stripped_parts
159
+ @stripped_parts ||= []
160
+ end
161
+
162
+ # Record a stripped part. Append-only; used by the loader.
163
+ #
164
+ # @param path [String] package-relative path
165
+ # @param reason [String] why the part was stripped (from
166
+ # `JunkClassifier#reason`)
167
+ # @return [void]
168
+ def add_stripped_part(path:, reason:)
169
+ stripped_parts << StrippedPart.new(path: path, reason: reason)
170
+ end
171
+
149
172
  # Load DOCX package from file
150
173
  #
151
174
  # @param path [String] Path to .docx file
@@ -400,6 +400,11 @@ document_rels)
400
400
  # reconciliation untouched (non-standard entries are preserved);
401
401
  # a part whose content type came from a Default entry needs an
402
402
  # Override once the reconciler rebuilds the standard Defaults.
403
+ #
404
+ # Raw parts always carry a non-nil content type: the loader
405
+ # strips undeclared parts at load (default `:strip` policy), and
406
+ # API-created raw parts without a content type are a programmer
407
+ # error that the save-time gate catches as OPC-005.
403
408
  def inject_raw_part_content_types(content_types)
404
409
  return if raw_parts.empty?
405
410
 
@@ -415,7 +420,8 @@ document_rels)
415
420
  next if content_types.overrides.any? { |o| o.part_name == part_name }
416
421
 
417
422
  content_types.overrides << Uniword::ContentTypes::Override.new(
418
- part_name: part_name, content_type: part.content_type,
423
+ part_name: part_name,
424
+ content_type: part.content_type,
419
425
  )
420
426
  end
421
427
  end
@@ -17,6 +17,13 @@ module Uniword
17
17
  # sidecar) at a non-standard location, or when it is a customXml
18
18
  # item rels sidecar consumed by CustomXmlLoader.
19
19
  #
20
+ # Non-compliant parts (no content type declaration, OS artifact,
21
+ # ...) are stripped at load when
22
+ # `Uniword.configuration.on_noncompliant_content` is `:strip`
23
+ # (the default, matching Word's behavior). In `:raise` mode the
24
+ # parts are preserved and the save-time integrity gate raises
25
+ # with structured OPC-005 issues.
26
+ #
20
27
  # Bytes are re-read from the original ZIP when available (the
21
28
  # extracted hash may carry corrupted UTF-8 binary); content
22
29
  # types come from the loaded [Content_Types].xml model.
@@ -32,21 +39,84 @@ module Uniword
32
39
  # by definition matching)
33
40
  # @return [void]
34
41
  def load(context, _definition)
35
- paths = unclaimed_paths(context)
36
- return if paths.empty?
42
+ preservable, stripped = partition_unclaimed(context)
43
+
44
+ record_stripped(context, stripped)
45
+ return if preservable.empty?
37
46
 
38
- bytes = raw_bytes(context, paths)
39
- paths.each do |path|
47
+ bytes = raw_bytes(context, preservable)
48
+ rels_by_target = relationships_by_target(context.package)
49
+
50
+ preservable.each do |path|
40
51
  next unless bytes[path]
41
52
 
42
53
  context.package.raw_parts[path] = build_part(
43
- context.package, path, bytes[path]
54
+ context.package, path, bytes[path], rels_by_target
44
55
  )
45
56
  end
46
57
  end
47
58
 
48
59
  private
49
60
 
61
+ # Split unclaimed paths into preservable and stripped based on
62
+ # the configured policy. In `:raise` mode everything is
63
+ # preserved (existing behavior); the save-time gate handles
64
+ # non-compliance.
65
+ #
66
+ # @return [Array(Array<String>, Array<String>)] preservable
67
+ # paths and stripped paths (each in zip_content order)
68
+ def partition_unclaimed(context)
69
+ unclaimed = unclaimed_paths(context)
70
+ return [unclaimed, []] if raise_mode?
71
+
72
+ classifier = build_classifier(context.package)
73
+ unclaimed.partition { |path| classifier.reason(path).nil? }
74
+ end
75
+
76
+ # @return [Boolean] true when the configured policy is :raise
77
+ def raise_mode?
78
+ Uniword.configuration.on_noncompliant_content == :raise
79
+ end
80
+
81
+ # Record stripped parts on the package and log each one when
82
+ # save-fix logging is enabled.
83
+ #
84
+ # @return [void]
85
+ def record_stripped(context, stripped)
86
+ return if stripped.empty?
87
+
88
+ classifier = build_classifier(context.package)
89
+ stripped.each do |path|
90
+ reason = classifier.reason(path)
91
+ context.package.add_stripped_part(path: path, reason: reason)
92
+ log_strip(path, reason)
93
+ end
94
+ end
95
+
96
+ # @return [JunkClassifier] classifier wired to the package's
97
+ # content types and relationship targets
98
+ def build_classifier(package)
99
+ JunkClassifier.new(
100
+ content_types: package.content_types,
101
+ relationships_by_path: relationships_by_target(package),
102
+ )
103
+ end
104
+
105
+ # @return [Hash{String => Array<Ooxml::Relationships::Relationship>}]
106
+ # target path => relationships referencing it. Multiple rels
107
+ # may target the same path; the first is used for metadata.
108
+ # The classifier uses `key?` only.
109
+ def relationships_by_target(package)
110
+ rels_collections(package).each_with_object({}) do |(rels, base), memo|
111
+ rels&.relationships&.each do |rel|
112
+ target = rel.target.to_s
113
+ next if target.empty?
114
+
115
+ (memo[resolve_target(base, target)] ||= []) << rel
116
+ end
117
+ end
118
+ end
119
+
50
120
  # @return [Array<String>] ZIP entries claimed by no registry
51
121
  # loader, in zip_content order
52
122
  def unclaimed_paths(context)
@@ -71,8 +141,8 @@ module Uniword
71
141
  end
72
142
 
73
143
  # @return [RawPart]
74
- def build_part(package, path, content)
75
- rel = referencing_relationship(package, path)
144
+ def build_part(package, path, content, rels_by_target)
145
+ rel = first_referencing_relationship(rels_by_target, path)
76
146
  RawPart.new(
77
147
  path: path,
78
148
  content: content,
@@ -105,44 +175,23 @@ module Uniword
105
175
  end
106
176
 
107
177
  # Content type the source [Content_Types].xml declared for the
108
- # part: its Override first, else the Default for its extension.
178
+ # part. Delegates to `ContentTypes::Types#content_type_for`
179
+ # (single source of truth for the lookup).
109
180
  #
110
181
  # @return [String, nil] nil when the source declared neither
111
182
  def content_type_for(package, path)
112
183
  content_types = package.content_types
113
184
  return nil unless content_types
114
185
 
115
- override_type(content_types, path) ||
116
- default_type(content_types, path)
117
- end
118
-
119
- def override_type(content_types, path)
120
- part_name = "/#{path}"
121
- content_types.overrides.find do |o|
122
- o.part_name == part_name
123
- end&.content_type
124
- end
125
-
126
- def default_type(content_types, path)
127
- ext = File.extname(path)[1..]
128
- return nil unless ext
129
-
130
- content_types.defaults.find do |d|
131
- d.extension == ext
132
- end&.content_type
186
+ content_types.content_type_for(path)
133
187
  end
134
188
 
135
- # The first relationship in a modelled rels part whose
136
- # resolved target is this part, or nil. Recorded as metadata
137
- # only; the rel itself stays in its rels collection.
138
- def referencing_relationship(package, path)
139
- rels_collections(package).each do |rels, base_dir|
140
- rel = rels&.relationships&.find do |r|
141
- resolve_target(base_dir, r.target.to_s) == path
142
- end
143
- return rel if rel
144
- end
145
- nil
189
+ # The first relationship referencing the given path, or nil.
190
+ # Recorded as metadata only; the rel itself stays in its rels
191
+ # collection.
192
+ def first_referencing_relationship(rels_by_target, path)
193
+ rels = rels_by_target[path]
194
+ rels.is_a?(Array) ? rels.first : nil
146
195
  end
147
196
 
148
197
  # [rels collection, base directory] pairs mirroring the rels
@@ -177,6 +226,15 @@ module Uniword
177
226
  end
178
227
  segments.join("/")
179
228
  end
229
+
230
+ # @return [void]
231
+ def log_strip(path, reason)
232
+ return unless Uniword.configuration.log_save_fixes
233
+
234
+ Uniword.logger&.info do
235
+ "Stripped non-compliant part #{path} (#{reason})"
236
+ end
237
+ end
180
238
  end
181
239
  end
182
240
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uniword
4
+ module Docx
5
+ # Reporting record for a part stripped at load time.
6
+ #
7
+ # Created by `Docx::PartLoader` (in `:strip` policy mode) for
8
+ # every ZIP entry the `JunkClassifier` flagged. Stored on
9
+ # `Package#stripped_parts` for caller introspection.
10
+ #
11
+ # Lightweight by design: no lutaml-model, no XML serialization.
12
+ # These are load-time metadata, never written to a package.
13
+ #
14
+ # @example
15
+ # part = StrippedPart.new(
16
+ # path: "[trash]/0000.dat",
17
+ # reason: "No content type declaration and no referencing " \
18
+ # "relationship",
19
+ # )
20
+ # part.path # => "[trash]/0000.dat"
21
+ # part.reason # => "No content type declaration..."
22
+ class StrippedPart
23
+ attr_reader :path, :reason
24
+
25
+ # @param path [String] package-relative path of the stripped part
26
+ # @param reason [String] why the part was stripped (human-readable,
27
+ # produced by `JunkClassifier#reason`)
28
+ def initialize(path:, reason:)
29
+ @path = path
30
+ @reason = reason
31
+ end
32
+
33
+ # Value-object equality by path + reason.
34
+ #
35
+ # @param other [Object]
36
+ # @return [Boolean]
37
+ def eql?(other)
38
+ other.is_a?(StrippedPart) && path == other.path &&
39
+ reason == other.reason
40
+ end
41
+
42
+ # Combined with `eql?` for Hash/Set membership.
43
+ #
44
+ # @return [Integer]
45
+ def hash
46
+ [path, reason].hash
47
+ end
48
+
49
+ # @param other [Object]
50
+ # @return [Boolean]
51
+ def ==(other)
52
+ eql?(other)
53
+ end
54
+ end
55
+ end
56
+ end
data/lib/uniword/docx.rb CHANGED
@@ -10,6 +10,8 @@ module Uniword
10
10
  autoload :IdAllocator, "#{__dir__}/docx/id_allocator"
11
11
  autoload :Part, "#{__dir__}/docx/part"
12
12
  autoload :RawPart, "#{__dir__}/docx/raw_part"
13
+ autoload :StrippedPart, "#{__dir__}/docx/stripped_part"
14
+ autoload :JunkClassifier, "#{__dir__}/docx/junk_classifier"
13
15
  autoload :ChartPart, "#{__dir__}/docx/chart_part"
14
16
  autoload :ImagePart, "#{__dir__}/docx/image_part"
15
17
  autoload :HeaderFooterPart, "#{__dir__}/docx/header_footer_part"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Uniword
4
- VERSION = "1.4.1"
4
+ VERSION = "1.5.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: uniword
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.1
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -823,6 +823,7 @@ files:
823
823
  - lib/uniword/docx/header_footer_view.rb
824
824
  - lib/uniword/docx/id_allocator.rb
825
825
  - lib/uniword/docx/image_part.rb
826
+ - lib/uniword/docx/junk_classifier.rb
826
827
  - lib/uniword/docx/package.rb
827
828
  - lib/uniword/docx/package_defaults.rb
828
829
  - lib/uniword/docx/package_integrity_checker.rb
@@ -852,6 +853,7 @@ files:
852
853
  - lib/uniword/docx/reconciler/referential_integrity.rb
853
854
  - lib/uniword/docx/reconciler/tables.rb
854
855
  - lib/uniword/docx/reconciler/theme.rb
856
+ - lib/uniword/docx/stripped_part.rb
855
857
  - lib/uniword/drawingml.rb
856
858
  - lib/uniword/drawingml/adjust_value_list.rb
857
859
  - lib/uniword/drawingml/alpha.rb