uniword 1.6.0 → 1.7.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 +4 -4
- data/exe/uniword +9 -0
- data/lib/uniword/accessibility/accessibility_report.rb +8 -0
- data/lib/uniword/builder/chart_builder.rb +129 -134
- 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/diff/package_differ.rb +16 -15
- data/lib/uniword/document_factory.rb +2 -2
- data/lib/uniword/document_writer.rb +32 -10
- data/lib/uniword/docx/package_integrity_checker.rb +10 -11
- data/lib/uniword/docx/reconciler/body.rb +1 -0
- data/lib/uniword/docx/reconciler/helpers.rb +27 -12
- data/lib/uniword/docx/reconciler/parts.rb +5 -0
- data/lib/uniword/find_replace/paragraph_walker.rb +49 -12
- data/lib/uniword/find_replace/scope.rb +7 -4
- data/lib/uniword/infrastructure/mime_packager.rb +3 -10
- data/lib/uniword/infrastructure/mime_parser.rb +23 -1
- data/lib/uniword/math/plurimath_adapter.rb +28 -24
- data/lib/uniword/math_equation.rb +1 -1
- data/lib/uniword/mhtml/html_part.rb +19 -13
- data/lib/uniword/mhtml/math_converter.rb +18 -12
- data/lib/uniword/mhtml/mime_part.rb +5 -16
- data/lib/uniword/mhtml/xml_part.rb +3 -3
- data/lib/uniword/ooxml/additional_characteristics.rb +4 -4
- data/lib/uniword/ooxml/element_order.rb +21 -0
- data/lib/uniword/ooxml/schema/element_serializer.rb +27 -35
- 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/streaming_parser.rb +22 -20
- data/lib/uniword/theme/theme_xml_parser.rb +4 -6
- data/lib/uniword/transformation/html_element_builder.rb +62 -48
- data/lib/uniword/transformation/html_formatting_mapper.rb +3 -3
- data/lib/uniword/transformation/html_to_ooxml_converter.rb +11 -11
- data/lib/uniword/transformation/mhtml_element_renderer.rb +1 -2
- 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 +10 -17
- data/lib/uniword/validation/validators/xml_schema_validator.rb +75 -26
- data/lib/uniword/validation/verify_orchestrator.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/wp_drawing/position_h.rb +1 -1
- data/lib/uniword/wp_drawing/position_v.rb +1 -1
- data/lib/uniword/wp_drawing/size_rel_h.rb +1 -1
- data/lib/uniword/wp_drawing/size_rel_v.rb +1 -1
- data/lib/uniword.rb +9 -7
- metadata +19 -15
- data/lib/uniword/infrastructure/xml_namespace_normalizer.rb +0 -107
|
@@ -67,7 +67,7 @@ module Uniword
|
|
|
67
67
|
schema_def = @schema.definition_for(element.class)
|
|
68
68
|
|
|
69
69
|
# Build XML document
|
|
70
|
-
doc =
|
|
70
|
+
doc = Moxml.new.create_document
|
|
71
71
|
|
|
72
72
|
# Create root element with proper namespace
|
|
73
73
|
root = serialize_element_to_node(doc, element, schema_def, options)
|
|
@@ -105,21 +105,21 @@ module Uniword
|
|
|
105
105
|
OoxmlSchema.load(schema_path)
|
|
106
106
|
end
|
|
107
107
|
|
|
108
|
-
# Serialize element to
|
|
108
|
+
# Serialize element to Moxml node
|
|
109
109
|
#
|
|
110
|
-
# @param doc [
|
|
110
|
+
# @param doc [Moxml::Document] XML document
|
|
111
111
|
# @param element [Element] Element to serialize
|
|
112
112
|
# @param schema_def [ElementDefinition] Schema definition
|
|
113
113
|
# @param options [Hash] Serialization options
|
|
114
|
-
# @return [
|
|
114
|
+
# @return [Moxml::Element] Created node
|
|
115
115
|
def serialize_element_to_node(doc, element, schema_def, options)
|
|
116
116
|
# Create element node with namespace
|
|
117
117
|
prefix = schema_def.prefix
|
|
118
118
|
local_name = schema_def.local_name
|
|
119
119
|
namespace_uri = @schema.namespace[:uri]
|
|
120
120
|
|
|
121
|
-
node =
|
|
122
|
-
node.
|
|
121
|
+
node = doc.create_element(local_name)
|
|
122
|
+
node.add_namespace(prefix, namespace_uri)
|
|
123
123
|
|
|
124
124
|
# Serialize attributes according to schema
|
|
125
125
|
serialize_attributes_to_node(node, element, schema_def)
|
|
@@ -132,7 +132,7 @@ module Uniword
|
|
|
132
132
|
|
|
133
133
|
# Serialize element attributes to node
|
|
134
134
|
#
|
|
135
|
-
# @param node [
|
|
135
|
+
# @param node [Moxml::Element] Node to add attributes to
|
|
136
136
|
# @param element [Element] Element with attributes
|
|
137
137
|
# @param schema_def [ElementDefinition] Schema definition
|
|
138
138
|
def serialize_attributes_to_node(node, element, schema_def)
|
|
@@ -157,8 +157,8 @@ module Uniword
|
|
|
157
157
|
|
|
158
158
|
# Serialize element children to node
|
|
159
159
|
#
|
|
160
|
-
# @param doc [
|
|
161
|
-
# @param node [
|
|
160
|
+
# @param doc [Moxml::Document] XML document
|
|
161
|
+
# @param node [Moxml::Element] Parent node
|
|
162
162
|
# @param element [Element] Element with children
|
|
163
163
|
# @param schema_def [ElementDefinition] Schema definition
|
|
164
164
|
# @param options [Hash] Serialization options
|
|
@@ -190,11 +190,11 @@ module Uniword
|
|
|
190
190
|
|
|
191
191
|
# Serialize a single child element to node
|
|
192
192
|
#
|
|
193
|
-
# @param doc [
|
|
193
|
+
# @param doc [Moxml::Document] XML document
|
|
194
194
|
# @param child [Element, Object] Child element or value
|
|
195
195
|
# @param child_def [ChildDefinition] Child schema definition
|
|
196
196
|
# @param options [Hash] Serialization options
|
|
197
|
-
# @return [
|
|
197
|
+
# @return [Moxml::Element, nil] Created node
|
|
198
198
|
def serialize_child_to_node(doc, child, child_def, options)
|
|
199
199
|
# Get namespace prefix from tag (e.g., 'w:p' -> 'w')
|
|
200
200
|
tag_parts = child_def.tag.split(":")
|
|
@@ -204,24 +204,21 @@ module Uniword
|
|
|
204
204
|
|
|
205
205
|
if child_def.presence_only?
|
|
206
206
|
# Presence-only element (e.g., <w:b/> for bold)
|
|
207
|
-
node =
|
|
208
|
-
node.
|
|
209
|
-
namespace_uri)
|
|
207
|
+
node = doc.create_element(local_name)
|
|
208
|
+
node.add_namespace(prefix, namespace_uri)
|
|
210
209
|
node
|
|
211
210
|
elsif child.is_a?(String)
|
|
212
211
|
# Simple string value (e.g., text content)
|
|
213
|
-
node =
|
|
214
|
-
node.
|
|
215
|
-
namespace_uri)
|
|
212
|
+
node = doc.create_element(local_name)
|
|
213
|
+
node.add_namespace(prefix, namespace_uri)
|
|
216
214
|
node.content = child
|
|
217
215
|
node
|
|
218
216
|
elsif child.is_a?(Uniword::Element)
|
|
219
217
|
# Check if this is a TextElement that needs special handling
|
|
220
218
|
if child.instance_of?(::Uniword::TextElement) && child.is_a?(Uniword::TextElement)
|
|
221
219
|
# TextElement: serialize as element with text content
|
|
222
|
-
node =
|
|
223
|
-
node.
|
|
224
|
-
namespace_uri)
|
|
220
|
+
node = doc.create_element(local_name)
|
|
221
|
+
node.add_namespace(prefix, namespace_uri)
|
|
225
222
|
node.content = child.content.to_s if child.content
|
|
226
223
|
node
|
|
227
224
|
else
|
|
@@ -238,9 +235,8 @@ module Uniword
|
|
|
238
235
|
else
|
|
239
236
|
# For properties without schema, create empty element
|
|
240
237
|
# (will be filled with children if schema definition exists for element type)
|
|
241
|
-
node =
|
|
242
|
-
node.
|
|
243
|
-
namespace_uri)
|
|
238
|
+
node = doc.create_element(local_name)
|
|
239
|
+
node.add_namespace(prefix, namespace_uri)
|
|
244
240
|
|
|
245
241
|
if child_def.has_attributes?
|
|
246
242
|
child_def.attributes.each do |attr_config|
|
|
@@ -256,9 +252,8 @@ module Uniword
|
|
|
256
252
|
end
|
|
257
253
|
elsif child_def.has_attributes?
|
|
258
254
|
# Element with attributes but no child elements
|
|
259
|
-
node =
|
|
260
|
-
node.
|
|
261
|
-
namespace_uri)
|
|
255
|
+
node = doc.create_element(local_name)
|
|
256
|
+
node.add_namespace(prefix, namespace_uri)
|
|
262
257
|
|
|
263
258
|
child_def.attributes.each do |attr_config|
|
|
264
259
|
attr_def = AttributeDefinition.new(attr_config)
|
|
@@ -271,23 +266,20 @@ module Uniword
|
|
|
271
266
|
node
|
|
272
267
|
elsif child.is_a?(String)
|
|
273
268
|
# Simple string value
|
|
274
|
-
node =
|
|
275
|
-
node.
|
|
276
|
-
namespace_uri)
|
|
269
|
+
node = doc.create_element(local_name)
|
|
270
|
+
node.add_namespace(prefix, namespace_uri)
|
|
277
271
|
node.content = child
|
|
278
272
|
node
|
|
279
273
|
elsif child.is_a?(Uniword::TextElement)
|
|
280
274
|
# TextElement or similar - get the content
|
|
281
|
-
node =
|
|
282
|
-
node.
|
|
283
|
-
namespace_uri)
|
|
275
|
+
node = doc.create_element(local_name)
|
|
276
|
+
node.add_namespace(prefix, namespace_uri)
|
|
284
277
|
node.content = child.content.to_s if child.content
|
|
285
278
|
node
|
|
286
279
|
elsif child
|
|
287
280
|
# Other value element - convert to string
|
|
288
|
-
node =
|
|
289
|
-
node.
|
|
290
|
-
namespace_uri)
|
|
281
|
+
node = doc.create_element(local_name)
|
|
282
|
+
node.add_namespace(prefix, namespace_uri)
|
|
291
283
|
node.content = child.to_s
|
|
292
284
|
node
|
|
293
285
|
end
|
|
@@ -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
|