uniword 1.5.4 → 1.6.1
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 +4 -4
- data/exe/uniword +17 -0
- data/lib/uniword/accessibility/accessibility_report.rb +8 -0
- data/lib/uniword/cli/completions.rb +99 -0
- data/lib/uniword/cli/generate_cli.rb +5 -0
- data/lib/uniword/cli/help_switch.rb +45 -0
- data/lib/uniword/cli/helpers.rb +1 -0
- data/lib/uniword/cli/main.rb +118 -21
- data/lib/uniword/cli/no_color.rb +21 -0
- data/lib/uniword/document_factory.rb +2 -2
- data/lib/uniword/document_writer.rb +32 -10
- data/lib/uniword/find_replace/paragraph_walker.rb +49 -12
- data/lib/uniword/find_replace/scope.rb +7 -4
- data/lib/uniword/mhtml/xml_part.rb +3 -3
- data/lib/uniword/ooxml/additional_characteristics.rb +4 -4
- data/lib/uniword/ooxml/schema_library.rb +2 -2
- data/lib/uniword/ooxml/types/characteristics_val.rb +16 -0
- data/lib/uniword/ooxml/types/schema_library_val.rb +17 -0
- data/lib/uniword/ooxml/types/variant_types.rb +10 -2
- data/lib/uniword/ooxml/types/vt_attr.rb +19 -0
- data/lib/uniword/ooxml/types/wml_int.rb +17 -0
- data/lib/uniword/ooxml/types/wml_val.rb +19 -0
- data/lib/uniword/ooxml/types.rb +12 -0
- data/lib/uniword/properties/alignment.rb +4 -0
- data/lib/uniword/properties/boolean_element_factory.rb +1 -1
- data/lib/uniword/properties/font_size.rb +3 -0
- data/lib/uniword/review/review_manager.rb +34 -5
- data/lib/uniword/review/revision_resolver.rb +190 -0
- data/lib/uniword/review.rb +1 -0
- data/lib/uniword/revision.rb +3 -3
- data/lib/uniword/serialization/ooxml_serializer.rb +3 -3
- data/lib/uniword/theme/theme_xml_parser.rb +1 -3
- data/lib/uniword/validation/opc_validator.rb +6 -7
- data/lib/uniword/validation/rules/document_context.rb +3 -3
- data/lib/uniword/validation/rules/images_rule.rb +1 -1
- data/lib/uniword/validation/rules/theme_rule.rb +2 -2
- data/lib/uniword/validation/schema_registry.rb +1 -1
- data/lib/uniword/version.rb +1 -1
- data/lib/uniword/wordprocessingml/deletion.rb +41 -0
- data/lib/uniword/wordprocessingml/insertion.rb +41 -0
- data/lib/uniword/wordprocessingml/page_margins.rb +7 -7
- data/lib/uniword/wordprocessingml/paragraph.rb +30 -4
- data/lib/uniword/wordprocessingml/settings.rb +3 -4
- data/lib/uniword/wordprocessingml/style_cleanup.rb +5 -14
- data/lib/uniword/wordprocessingml/table_borders.rb +1 -1
- data/lib/uniword/wordprocessingml/table_cell_borders.rb +1 -1
- data/lib/uniword/wordprocessingml/text.rb +0 -1
- data/lib/uniword/wordprocessingml.rb +5 -0
- data/lib/uniword.rb +9 -3
- metadata +15 -10
|
@@ -59,16 +59,19 @@ module Uniword
|
|
|
59
59
|
|
|
60
60
|
# Walk every paragraph in `containers` and yield each run's
|
|
61
61
|
# text elements. Shared by body / headers / footers / footnotes
|
|
62
|
-
# / endnotes / comments scopes.
|
|
62
|
+
# / endnotes / comments scopes. Runs inside tracked insertions
|
|
63
|
+
# are live content and included; runs inside deletions are
|
|
64
|
+
# deleted content and left untouched.
|
|
63
65
|
#
|
|
64
|
-
# @param containers [Enumerable<#paragraphs
|
|
65
|
-
# #structured_document_tags>]
|
|
66
|
+
# @param containers [Enumerable<#paragraphs>]
|
|
66
67
|
# @yieldparam text_element [Wordprocessingml::Text]
|
|
67
68
|
# @yieldparam accessor [TextAccessor]
|
|
68
69
|
# @return [void]
|
|
69
70
|
def each_text_in_containers(containers)
|
|
70
71
|
ParagraphWalker.each_paragraph(containers) do |paragraph|
|
|
71
|
-
paragraph.runs
|
|
72
|
+
runs = paragraph.runs +
|
|
73
|
+
paragraph.insertions.flat_map(&:runs)
|
|
74
|
+
runs.each do |run|
|
|
72
75
|
each_text_in_run(run) { |*a| yield(*a) }
|
|
73
76
|
end
|
|
74
77
|
end
|
|
@@ -5,9 +5,9 @@ module Uniword
|
|
|
5
5
|
# XML MIME part — filelist.xml, props*.xml, colorschememapping.xml, etc.
|
|
6
6
|
class XmlPart < MimePart
|
|
7
7
|
def xml_content
|
|
8
|
-
@xml_content ||=
|
|
9
|
-
rescue
|
|
10
|
-
@xml_content ||=
|
|
8
|
+
@xml_content ||= Moxml.parse(decoded_content)
|
|
9
|
+
rescue Moxml::ParseError
|
|
10
|
+
@xml_content ||= Moxml.parse(decoded_content)
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def to_xml
|
|
@@ -11,10 +11,10 @@ module Uniword
|
|
|
11
11
|
#
|
|
12
12
|
# Namespace: http://schemas.openxmlformats.org/officeDocument/2006/characteristics
|
|
13
13
|
class Characteristic < Lutaml::Model::Serializable
|
|
14
|
-
attribute :name,
|
|
15
|
-
attribute :relation,
|
|
16
|
-
attribute :val,
|
|
17
|
-
attribute :vocabulary,
|
|
14
|
+
attribute :name, Types::CharacteristicsVal
|
|
15
|
+
attribute :relation, Types::CharacteristicsVal
|
|
16
|
+
attribute :val, Types::CharacteristicsVal
|
|
17
|
+
attribute :vocabulary, Types::CharacteristicsVal
|
|
18
18
|
|
|
19
19
|
xml do
|
|
20
20
|
element "characteristic"
|
|
@@ -12,8 +12,8 @@ module Uniword
|
|
|
12
12
|
# Namespace: http://schemas.openxmlformats.org/schemaLibrary/2006/main
|
|
13
13
|
# Prefix: sl
|
|
14
14
|
class SchemaLibEntry < Lutaml::Model::Serializable
|
|
15
|
-
attribute :uri,
|
|
16
|
-
attribute :manifest_location,
|
|
15
|
+
attribute :uri, Types::SchemaLibraryVal, default: -> { "" }
|
|
16
|
+
attribute :manifest_location, Types::SchemaLibraryVal
|
|
17
17
|
|
|
18
18
|
xml do
|
|
19
19
|
element "schema"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module Uniword
|
|
6
|
+
module Ooxml
|
|
7
|
+
module Types
|
|
8
|
+
# String type in the Additional Characteristics namespace.
|
|
9
|
+
class CharacteristicsVal < Lutaml::Model::Type::String
|
|
10
|
+
xml do
|
|
11
|
+
namespace Uniword::Ooxml::Namespaces::Characteristics
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module Uniword
|
|
6
|
+
module Ooxml
|
|
7
|
+
module Types
|
|
8
|
+
# String type in the schemaLibrary namespace (sl:uri,
|
|
9
|
+
# sl:manifestLocation qualified attributes).
|
|
10
|
+
class SchemaLibraryVal < Lutaml::Model::Type::String
|
|
11
|
+
xml do
|
|
12
|
+
namespace Uniword::Ooxml::Namespaces::SchemaLibrary
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -13,6 +13,14 @@ module Uniword
|
|
|
13
13
|
module VariantTypes
|
|
14
14
|
VT_NS = Uniword::Ooxml::Namespaces::VariantTypes
|
|
15
15
|
|
|
16
|
+
# String type for qualified vt: attributes (e.g. baseType on
|
|
17
|
+
# vector elements).
|
|
18
|
+
class VtAttr < Lutaml::Model::Type::String
|
|
19
|
+
xml do
|
|
20
|
+
namespace VT_NS
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
16
24
|
# Base class for simple variant type values (text content only)
|
|
17
25
|
class VTValue < Lutaml::Model::Serializable
|
|
18
26
|
attribute :value, :string
|
|
@@ -259,7 +267,7 @@ module Uniword
|
|
|
259
267
|
|
|
260
268
|
# vt:vector - Typed array of values
|
|
261
269
|
class VtVector < Lutaml::Model::Serializable
|
|
262
|
-
attribute :base_type,
|
|
270
|
+
attribute :base_type, VtAttr
|
|
263
271
|
attribute :size, :string
|
|
264
272
|
attribute :lpwstr_values, VtLpwstr, collection: true
|
|
265
273
|
attribute :lpstr_values, VtLpstr, collection: true
|
|
@@ -287,7 +295,7 @@ module Uniword
|
|
|
287
295
|
|
|
288
296
|
# vt:array - Typed array with bounds
|
|
289
297
|
class VtArray < Lutaml::Model::Serializable
|
|
290
|
-
attribute :base_type,
|
|
298
|
+
attribute :base_type, VtAttr
|
|
291
299
|
attribute :size, :string
|
|
292
300
|
attribute :l_bound, :string
|
|
293
301
|
attribute :u_bound, :string
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module Uniword
|
|
6
|
+
module Ooxml
|
|
7
|
+
module Types
|
|
8
|
+
module VariantTypes
|
|
9
|
+
# String type for qualified vt: attributes (e.g. baseType on
|
|
10
|
+
# vector elements).
|
|
11
|
+
class VtAttr < Lutaml::Model::Type::String
|
|
12
|
+
xml do
|
|
13
|
+
namespace VT_NS
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module Uniword
|
|
6
|
+
module Ooxml
|
|
7
|
+
module Types
|
|
8
|
+
# Integer type in the WordProcessingML namespace (w:top, w:gutter,
|
|
9
|
+
# w:sz et al. qualified attributes).
|
|
10
|
+
class WmlInt < Lutaml::Model::Type::Integer
|
|
11
|
+
xml do
|
|
12
|
+
namespace Uniword::Ooxml::Namespaces::WordProcessingML
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module Uniword
|
|
6
|
+
module Ooxml
|
|
7
|
+
module Types
|
|
8
|
+
# String type in the WordProcessingML namespace.
|
|
9
|
+
# Binds mapped attribute rules to their qualified (URI, local)
|
|
10
|
+
# identity — e.g. w:val on boolean/formatting elements — so
|
|
11
|
+
# strict adapters (Leptris) match exactly what DOCX emits.
|
|
12
|
+
class WmlVal < Lutaml::Model::Type::String
|
|
13
|
+
xml do
|
|
14
|
+
namespace Uniword::Ooxml::Namespaces::WordProcessingML
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/uniword/ooxml/types.rb
CHANGED
|
@@ -53,6 +53,18 @@ module Uniword
|
|
|
53
53
|
# Relationships namespace type for r:embed/r:link cross-namespace attrs
|
|
54
54
|
autoload :RelationshipId, "#{__dir__}/types/relationship_id"
|
|
55
55
|
|
|
56
|
+
# WordProcessingML namespace type for qualified w:val attributes
|
|
57
|
+
autoload :WmlVal, "#{__dir__}/types/wml_val"
|
|
58
|
+
|
|
59
|
+
# WordProcessingML namespace type for qualified integer attributes
|
|
60
|
+
autoload :WmlInt, "#{__dir__}/types/wml_int"
|
|
61
|
+
|
|
62
|
+
# schemaLibrary namespace type for sl: qualified attributes
|
|
63
|
+
autoload :SchemaLibraryVal, "#{__dir__}/types/schema_library_val"
|
|
64
|
+
|
|
65
|
+
# Additional Characteristics namespace type
|
|
66
|
+
autoload :CharacteristicsVal, "#{__dir__}/types/characteristics_val"
|
|
67
|
+
|
|
56
68
|
# Variant Types (vt: namespace) for OLE property values
|
|
57
69
|
autoload :VariantTypes, "#{__dir__}/types/variant_types"
|
|
58
70
|
|
|
@@ -6,6 +6,10 @@ module Uniword
|
|
|
6
6
|
module Properties
|
|
7
7
|
# Namespaced custom type for alignment value
|
|
8
8
|
class AlignmentValue < Lutaml::Model::Type::String
|
|
9
|
+
xml do
|
|
10
|
+
namespace Ooxml::Namespaces::WordProcessingML
|
|
11
|
+
end
|
|
12
|
+
|
|
9
13
|
# Full ST_Jc enumeration from ECMA-376 (wml.xsd)
|
|
10
14
|
VALUES = %w[
|
|
11
15
|
start center end both mediumKashida distribute numTab
|
|
@@ -27,6 +27,8 @@ module Uniword
|
|
|
27
27
|
def initialize(document)
|
|
28
28
|
@document = document
|
|
29
29
|
@accept_reject = AcceptReject.new
|
|
30
|
+
@resolver = RevisionResolver.new(document)
|
|
31
|
+
@resolver_hydrated = false
|
|
30
32
|
end
|
|
31
33
|
|
|
32
34
|
# --- Comments ---
|
|
@@ -141,26 +143,41 @@ module Uniword
|
|
|
141
143
|
|
|
142
144
|
# Accept a single revision by ID.
|
|
143
145
|
#
|
|
146
|
+
# Applies the decision to the document models (tracked-change
|
|
147
|
+
# nodes are spliced or removed) and keeps the facade list in
|
|
148
|
+
# sync.
|
|
149
|
+
#
|
|
144
150
|
# @param revision_id [String] The revision ID to accept
|
|
145
151
|
# @return [Boolean] true if accepted, false if not found
|
|
146
|
-
def accept(revision_id)
|
|
152
|
+
def accept(revision_id) # rubocop:disable Naming/PredicateMethod
|
|
147
153
|
revision = tracked_changes.find_revision(revision_id)
|
|
148
154
|
return false unless revision
|
|
149
155
|
|
|
150
|
-
@
|
|
156
|
+
if @resolver.ids.include?(revision_id.to_s)
|
|
157
|
+
@resolver.accept(revision_id)
|
|
158
|
+
else
|
|
159
|
+
@accept_reject.accept(revision)
|
|
160
|
+
end
|
|
151
161
|
tracked_changes.remove_revision(revision_id)
|
|
152
162
|
true
|
|
153
163
|
end
|
|
154
164
|
|
|
155
165
|
# Reject a single revision by ID.
|
|
156
166
|
#
|
|
167
|
+
# Applies the decision to the document models and keeps the
|
|
168
|
+
# facade list in sync.
|
|
169
|
+
#
|
|
157
170
|
# @param revision_id [String] The revision ID to reject
|
|
158
171
|
# @return [Boolean] true if rejected, false if not found
|
|
159
|
-
def reject(revision_id)
|
|
172
|
+
def reject(revision_id) # rubocop:disable Naming/PredicateMethod
|
|
160
173
|
revision = tracked_changes.find_revision(revision_id)
|
|
161
174
|
return false unless revision
|
|
162
175
|
|
|
163
|
-
@
|
|
176
|
+
if @resolver.ids.include?(revision_id.to_s)
|
|
177
|
+
@resolver.reject(revision_id)
|
|
178
|
+
else
|
|
179
|
+
@accept_reject.reject(revision)
|
|
180
|
+
end
|
|
164
181
|
tracked_changes.remove_revision(revision_id)
|
|
165
182
|
true
|
|
166
183
|
end
|
|
@@ -169,6 +186,8 @@ module Uniword
|
|
|
169
186
|
#
|
|
170
187
|
# @return [Integer] Number of changes accepted
|
|
171
188
|
def accept_all
|
|
189
|
+
tracked_changes # hydrates the resolver's node registry
|
|
190
|
+
@resolver.ids.each { |id| @resolver.accept(id) }
|
|
172
191
|
tracked_changes.accept_all
|
|
173
192
|
end
|
|
174
193
|
|
|
@@ -176,6 +195,8 @@ module Uniword
|
|
|
176
195
|
#
|
|
177
196
|
# @return [Integer] Number of changes rejected
|
|
178
197
|
def reject_all
|
|
198
|
+
tracked_changes # hydrates the resolver's node registry
|
|
199
|
+
@resolver.ids.each { |id| @resolver.reject(id) }
|
|
179
200
|
tracked_changes.reject_all
|
|
180
201
|
end
|
|
181
202
|
|
|
@@ -242,7 +263,10 @@ module Uniword
|
|
|
242
263
|
end
|
|
243
264
|
end
|
|
244
265
|
|
|
245
|
-
# Get or initialize TrackedChanges for the document.
|
|
266
|
+
# Get or initialize TrackedChanges for the document. On first
|
|
267
|
+
# access, parsed <w:ins>/<w:del> nodes are registered on the
|
|
268
|
+
# facade (via RevisionResolver) so listing and accept/reject
|
|
269
|
+
# reflect the document's real tracked changes.
|
|
246
270
|
#
|
|
247
271
|
# @return [Uniword::TrackedChanges] The tracked changes collection
|
|
248
272
|
def tracked_changes
|
|
@@ -256,6 +280,11 @@ module Uniword
|
|
|
256
280
|
tc
|
|
257
281
|
end
|
|
258
282
|
end
|
|
283
|
+
unless @resolver_hydrated
|
|
284
|
+
@resolver.hydrate(@tracked_changes)
|
|
285
|
+
@resolver_hydrated = true
|
|
286
|
+
end
|
|
287
|
+
@tracked_changes
|
|
259
288
|
end
|
|
260
289
|
end
|
|
261
290
|
end
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uniword
|
|
4
|
+
module Review
|
|
5
|
+
# Bridges parsed tracked-change nodes (<w:ins>/<w:del> on
|
|
6
|
+
# paragraphs) to the TrackedChanges facade and applies
|
|
7
|
+
# accept/reject decisions to the document models.
|
|
8
|
+
#
|
|
9
|
+
# Hydration walks the body (tables and SDT content included, via
|
|
10
|
+
# ParagraphWalker) and registers one facade Revision per node,
|
|
11
|
+
# keyed by the OOXML revision id (w:id). Decisions then mutate
|
|
12
|
+
# the models:
|
|
13
|
+
#
|
|
14
|
+
# - accept insert → splice wrapped runs into the paragraph
|
|
15
|
+
# - reject insert → remove the node
|
|
16
|
+
# - accept delete → remove the node (deleted content goes away)
|
|
17
|
+
# - reject delete → splice runs back, delText converted to t
|
|
18
|
+
#
|
|
19
|
+
# element_order arrays are updated alongside the collections so
|
|
20
|
+
# serialization keeps the runs at the revision's position.
|
|
21
|
+
class RevisionResolver
|
|
22
|
+
# @param document [Wordprocessingml::DocumentRoot]
|
|
23
|
+
def initialize(document)
|
|
24
|
+
@document = document
|
|
25
|
+
@nodes = {}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Register every parsed ins/del node on the facade.
|
|
29
|
+
#
|
|
30
|
+
# @param tracked_changes [Uniword::TrackedChanges]
|
|
31
|
+
# @return [Uniword::TrackedChanges]
|
|
32
|
+
def hydrate(tracked_changes)
|
|
33
|
+
each_tracked_node do |paragraph, node|
|
|
34
|
+
id = node.id.to_s
|
|
35
|
+
next if id.empty? || @nodes.key?(id)
|
|
36
|
+
|
|
37
|
+
@nodes[id] = [paragraph, node]
|
|
38
|
+
tracked_changes.add_revision(facade_revision_for(node))
|
|
39
|
+
end
|
|
40
|
+
tracked_changes
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Registered revision ids, in document order.
|
|
44
|
+
#
|
|
45
|
+
# @return [Array<String>]
|
|
46
|
+
def ids
|
|
47
|
+
@nodes.keys
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Accept one revision: keep inserted content / drop deleted
|
|
51
|
+
# content.
|
|
52
|
+
#
|
|
53
|
+
# @param revision_id [String]
|
|
54
|
+
# @return [Boolean] true when the revision was found and applied
|
|
55
|
+
def accept(revision_id) # rubocop:disable Naming/PredicateMethod
|
|
56
|
+
paragraph, node = @nodes[revision_id.to_s]
|
|
57
|
+
return false unless paragraph
|
|
58
|
+
|
|
59
|
+
if insertion?(node)
|
|
60
|
+
splice_into_paragraph(paragraph, node, node.runs)
|
|
61
|
+
else
|
|
62
|
+
remove_node(paragraph, node)
|
|
63
|
+
end
|
|
64
|
+
@nodes.delete(revision_id.to_s)
|
|
65
|
+
true
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Reject one revision: drop inserted content / restore deleted
|
|
69
|
+
# content.
|
|
70
|
+
#
|
|
71
|
+
# @param revision_id [String]
|
|
72
|
+
# @return [Boolean] true when the revision was found and applied
|
|
73
|
+
def reject(revision_id) # rubocop:disable Naming/PredicateMethod
|
|
74
|
+
paragraph, node = @nodes[revision_id.to_s]
|
|
75
|
+
return false unless paragraph
|
|
76
|
+
|
|
77
|
+
if insertion?(node)
|
|
78
|
+
remove_node(paragraph, node)
|
|
79
|
+
else
|
|
80
|
+
restored = node.runs.map { |run| restore_run_text(run) }
|
|
81
|
+
splice_into_paragraph(paragraph, node, restored)
|
|
82
|
+
end
|
|
83
|
+
@nodes.delete(revision_id.to_s)
|
|
84
|
+
true
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def insertion?(node)
|
|
90
|
+
node.is_a?(Wordprocessingml::Insertion)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def facade_revision_for(node)
|
|
94
|
+
Uniword::Revision.new(
|
|
95
|
+
type: insertion?(node) ? :insert : :delete,
|
|
96
|
+
revision_id: node.id.to_s,
|
|
97
|
+
author: node.author,
|
|
98
|
+
date: node.date,
|
|
99
|
+
text: node.text,
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def each_tracked_node(&block)
|
|
104
|
+
containers = [@document.body].compact
|
|
105
|
+
FindReplace::ParagraphWalker.each_paragraph(containers) do |p|
|
|
106
|
+
p.insertions.each { |node| yield(p, node) }
|
|
107
|
+
p.deletions.each { |node| yield(p, node) }
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def splice_into_paragraph(paragraph, node, runs)
|
|
112
|
+
tag = node_tag(node)
|
|
113
|
+
collection = tracked_collection(paragraph, node)
|
|
114
|
+
index = collection.index(node)
|
|
115
|
+
|
|
116
|
+
paragraph.runs.concat(runs)
|
|
117
|
+
collection.delete(node)
|
|
118
|
+
replace_order_entry(paragraph, tag, index, runs.size)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def remove_node(paragraph, node)
|
|
122
|
+
tag = node_tag(node)
|
|
123
|
+
collection = tracked_collection(paragraph, node)
|
|
124
|
+
index = collection.index(node)
|
|
125
|
+
|
|
126
|
+
collection.delete(node)
|
|
127
|
+
replace_order_entry(paragraph, tag, index, 0)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Convert a deleted run's delText back into live text so a
|
|
131
|
+
# rejected deletion re-enters the document as content.
|
|
132
|
+
def restore_run_text(run)
|
|
133
|
+
deleted = run.del_text
|
|
134
|
+
if deleted
|
|
135
|
+
run.text = [] if run.text.nil?
|
|
136
|
+
run.text << Wordprocessingml::Text.new(content: deleted.content)
|
|
137
|
+
run.del_text = nil
|
|
138
|
+
retag_order_entry(run, "delText", "t")
|
|
139
|
+
end
|
|
140
|
+
run
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def node_tag(node)
|
|
144
|
+
insertion?(node) ? "ins" : "del"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def tracked_collection(paragraph, node)
|
|
148
|
+
insertion?(node) ? paragraph.insertions : paragraph.deletions
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Replace the index-th `tag` entry in element_order with `count`
|
|
152
|
+
# run entries (count 0 removes it). Parsed models carry
|
|
153
|
+
# element_order; built models serialize by collection order.
|
|
154
|
+
def replace_order_entry(model, tag, index, count)
|
|
155
|
+
order = Ooxml::ElementOrder.mutable_order(model)
|
|
156
|
+
return unless order
|
|
157
|
+
return unless index
|
|
158
|
+
|
|
159
|
+
position = nth_entry_index(order, tag, index)
|
|
160
|
+
return unless position
|
|
161
|
+
|
|
162
|
+
entries = Array.new(count) { xml_entry("r") }
|
|
163
|
+
order[position, 1] = entries
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def retag_order_entry(model, old_tag, new_tag)
|
|
167
|
+
order = Ooxml::ElementOrder.mutable_order(model)
|
|
168
|
+
return unless order
|
|
169
|
+
|
|
170
|
+
position = nth_entry_index(order, old_tag, 0)
|
|
171
|
+
order[position] = xml_entry(new_tag) if position
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def nth_entry_index(order, tag, nth)
|
|
175
|
+
seen = -1
|
|
176
|
+
order.each_with_index do |entry, i|
|
|
177
|
+
next unless entry.name == tag
|
|
178
|
+
|
|
179
|
+
seen += 1
|
|
180
|
+
return i if seen == nth
|
|
181
|
+
end
|
|
182
|
+
nil
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def xml_entry(name)
|
|
186
|
+
Lutaml::Xml::Element.new("Element", name)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
data/lib/uniword/review.rb
CHANGED
|
@@ -14,6 +14,7 @@ module Uniword
|
|
|
14
14
|
module Review
|
|
15
15
|
autoload :AcceptReject, "uniword/review/accept_reject"
|
|
16
16
|
autoload :ReviewManager, "uniword/review/review_manager"
|
|
17
|
+
autoload :RevisionResolver, "uniword/review/revision_resolver"
|
|
17
18
|
autoload :InteractiveReview, "uniword/review/interactive_review"
|
|
18
19
|
end
|
|
19
20
|
end
|
data/lib/uniword/revision.rb
CHANGED
|
@@ -35,13 +35,13 @@ module Uniword
|
|
|
35
35
|
# @see TrackedChanges For revision collection management
|
|
36
36
|
class Revision < Lutaml::Model::Serializable
|
|
37
37
|
# Unique revision identifier
|
|
38
|
-
attribute :revision_id,
|
|
38
|
+
attribute :revision_id, Ooxml::Types::WmlVal
|
|
39
39
|
|
|
40
40
|
# Author name
|
|
41
|
-
attribute :author,
|
|
41
|
+
attribute :author, Ooxml::Types::WmlVal
|
|
42
42
|
|
|
43
43
|
# Revision date/time
|
|
44
|
-
attribute :date,
|
|
44
|
+
attribute :date, Ooxml::Types::WmlVal
|
|
45
45
|
|
|
46
46
|
# OOXML namespace configuration
|
|
47
47
|
xml do
|
|
@@ -18,12 +18,12 @@ module Uniword
|
|
|
18
18
|
document.to_xml(encoding: "UTF-8", prefix: true)
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
# Serialize a document to XML and return as a
|
|
21
|
+
# Serialize a document to XML and return as a Moxml document
|
|
22
22
|
#
|
|
23
23
|
# @param document [DocumentRoot] The document to serialize
|
|
24
|
-
# @return [
|
|
24
|
+
# @return [Moxml::Document] The serialized document
|
|
25
25
|
def serialize_to_doc(document)
|
|
26
|
-
|
|
26
|
+
Moxml.parse(serialize(document))
|
|
27
27
|
end
|
|
28
28
|
end
|
|
29
29
|
end
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "nokogiri"
|
|
4
|
-
|
|
5
3
|
module Uniword
|
|
6
4
|
module Themes
|
|
7
5
|
# Parses theme XML files into Theme models
|
|
@@ -26,7 +24,7 @@ module Uniword
|
|
|
26
24
|
# @return [Theme] Parsed theme
|
|
27
25
|
# @raise [ArgumentError] if XML is invalid or missing theme element
|
|
28
26
|
def parse(xml)
|
|
29
|
-
doc =
|
|
27
|
+
doc = Moxml.parse(xml)
|
|
30
28
|
theme_node = doc.at_xpath("//a:theme", THEME_NS)
|
|
31
29
|
|
|
32
30
|
unless theme_node
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "zip"
|
|
4
|
-
require "nokogiri"
|
|
5
4
|
|
|
6
5
|
module Uniword
|
|
7
6
|
module Validation
|
|
@@ -107,7 +106,7 @@ module Uniword
|
|
|
107
106
|
ct_entry = zip.find_entry("[Content_Types].xml")
|
|
108
107
|
return unless ct_entry
|
|
109
108
|
|
|
110
|
-
ct_doc =
|
|
109
|
+
ct_doc = Moxml.parse(ct_entry.get_input_stream.read)
|
|
111
110
|
|
|
112
111
|
# Get declared extensions
|
|
113
112
|
declared_exts = ct_doc.xpath("//xmlns:Default", "xmlns" => CT_NS)
|
|
@@ -137,7 +136,7 @@ module Uniword
|
|
|
137
136
|
suggestion: "Add a Default or Override entry in [Content_Types].xml.",
|
|
138
137
|
)
|
|
139
138
|
end
|
|
140
|
-
rescue
|
|
139
|
+
rescue Moxml::ParseError => e
|
|
141
140
|
issues << Report::ValidationIssue.new(
|
|
142
141
|
severity: "error",
|
|
143
142
|
code: "OPC-008",
|
|
@@ -155,7 +154,7 @@ module Uniword
|
|
|
155
154
|
end
|
|
156
155
|
|
|
157
156
|
def check_relationship_file(zip, entry, issues)
|
|
158
|
-
doc =
|
|
157
|
+
doc = Moxml.parse(entry.get_input_stream.read)
|
|
159
158
|
|
|
160
159
|
base_dir = compute_base_dir(entry.name)
|
|
161
160
|
|
|
@@ -181,7 +180,7 @@ module Uniword
|
|
|
181
180
|
"from the package. Remove the relationship or add the part.",
|
|
182
181
|
)
|
|
183
182
|
end
|
|
184
|
-
rescue
|
|
183
|
+
rescue Moxml::ParseError => e
|
|
185
184
|
issues << Report::ValidationIssue.new(
|
|
186
185
|
severity: "error",
|
|
187
186
|
code: "OPC-008",
|
|
@@ -195,8 +194,8 @@ module Uniword
|
|
|
195
194
|
|
|
196
195
|
xml_entries.each do |entry|
|
|
197
196
|
content = entry.get_input_stream.read
|
|
198
|
-
|
|
199
|
-
rescue
|
|
197
|
+
Moxml.parse(content)
|
|
198
|
+
rescue Moxml::ParseError => e
|
|
200
199
|
issues << Report::ValidationIssue.new(
|
|
201
200
|
severity: "error",
|
|
202
201
|
code: "OPC-008",
|
|
@@ -29,7 +29,7 @@ module Uniword
|
|
|
29
29
|
@path = path
|
|
30
30
|
@zip = nil
|
|
31
31
|
@parsed_parts = {}
|
|
32
|
-
@moxml = Moxml.new
|
|
32
|
+
@moxml = Moxml.new
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
# Context type used by the Engine to select rules.
|
|
@@ -139,7 +139,7 @@ module Uniword
|
|
|
139
139
|
raw = part_raw(rels_path)
|
|
140
140
|
return [] unless raw
|
|
141
141
|
|
|
142
|
-
doc =
|
|
142
|
+
doc = Moxml.parse(raw)
|
|
143
143
|
doc.xpath("//xmlns:Relationship", "xmlns" => RELS_NS).map do |rel|
|
|
144
144
|
{
|
|
145
145
|
id: rel["Id"],
|
|
@@ -157,7 +157,7 @@ module Uniword
|
|
|
157
157
|
raw = part_raw("[Content_Types].xml")
|
|
158
158
|
return {} unless raw
|
|
159
159
|
|
|
160
|
-
doc =
|
|
160
|
+
doc = Moxml.parse(raw)
|
|
161
161
|
types = {}
|
|
162
162
|
|
|
163
163
|
doc.xpath("//xmlns:Default", "xmlns" => CT_NS).each do |node|
|