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
|
@@ -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|
|
|
@@ -89,7 +89,7 @@ module Uniword
|
|
|
89
89
|
raw = context.part_raw("word/document.xml")
|
|
90
90
|
return unless raw
|
|
91
91
|
|
|
92
|
-
doc =
|
|
92
|
+
doc = Moxml.parse(raw)
|
|
93
93
|
doc.xpath("//a:blip", "a" => A_NS).each do |blip|
|
|
94
94
|
embed = blip["r:embed"] ||
|
|
95
95
|
blip.attributes.find { |a| a.name == "embed" }&.value
|
|
@@ -59,7 +59,7 @@ module Uniword
|
|
|
59
59
|
raw = context.part_raw("word/theme/theme1.xml")
|
|
60
60
|
return unless raw
|
|
61
61
|
|
|
62
|
-
doc =
|
|
62
|
+
doc = Moxml.parse(raw)
|
|
63
63
|
root = doc.root
|
|
64
64
|
return if root&.name == "theme"
|
|
65
65
|
|
|
@@ -71,7 +71,7 @@ module Uniword
|
|
|
71
71
|
suggestion: "The theme part should have <a:theme> as its " \
|
|
72
72
|
"root element per DrawingML specification.",
|
|
73
73
|
)
|
|
74
|
-
rescue
|
|
74
|
+
rescue Moxml::ParseError => e
|
|
75
75
|
issues << issue(
|
|
76
76
|
"Theme part is malformed: #{e.message}",
|
|
77
77
|
code: "DOC-081",
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "moxml"
|
|
4
|
-
require "
|
|
4
|
+
require "leptris/xml/xsd"
|
|
5
5
|
|
|
6
6
|
module Uniword
|
|
7
7
|
module Validation
|
|
8
8
|
# Maps XML namespace URIs to XSD schema files and loads schemas for validation.
|
|
9
9
|
#
|
|
10
10
|
# Uses Moxml for namespace detection (parsing XML to find declared namespaces)
|
|
11
|
-
# and
|
|
11
|
+
# and Moxml::XSD (Leptris engine) for actual XSD validation.
|
|
12
12
|
#
|
|
13
13
|
# The registry knows which OOXML namespace URIs correspond to which XSD files
|
|
14
14
|
# bundled in data/schemas/.
|
|
@@ -17,7 +17,7 @@ module Uniword
|
|
|
17
17
|
# registry = SchemaRegistry.new
|
|
18
18
|
# ns_uris = registry.detect_namespaces(xml_string)
|
|
19
19
|
# schema = registry.schema_for_namespaces(ns_uris)
|
|
20
|
-
# errors = schema.validate(
|
|
20
|
+
# errors = schema.validate(xml_string)
|
|
21
21
|
class SchemaRegistry
|
|
22
22
|
# Map of namespace URI => XSD file (relative to data/schemas/)
|
|
23
23
|
NAMESPACE_XSD_MAP = {
|
|
@@ -85,7 +85,7 @@ module Uniword
|
|
|
85
85
|
|
|
86
86
|
def initialize(schemas_dir: nil)
|
|
87
87
|
@schemas_dir = schemas_dir || default_schemas_dir
|
|
88
|
-
@moxml = Moxml.new
|
|
88
|
+
@moxml = Moxml.new
|
|
89
89
|
@schema_cache = {}
|
|
90
90
|
end
|
|
91
91
|
|
|
@@ -145,7 +145,7 @@ module Uniword
|
|
|
145
145
|
# Load and cache an XSD schema for validation.
|
|
146
146
|
#
|
|
147
147
|
# @param xsd_relative_path [String] Path relative to schemas_dir
|
|
148
|
-
# @return [
|
|
148
|
+
# @return [Moxml::XSD::Schema] Compiled schema
|
|
149
149
|
def load_schema(xsd_relative_path)
|
|
150
150
|
return @schema_cache[xsd_relative_path] if @schema_cache.key?(xsd_relative_path)
|
|
151
151
|
|
|
@@ -155,18 +155,11 @@ module Uniword
|
|
|
155
155
|
"XSD schema not found: #{xsd_path}"
|
|
156
156
|
end
|
|
157
157
|
|
|
158
|
-
#
|
|
159
|
-
#
|
|
160
|
-
#
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
schema_dir = File.dirname(xsd_path)
|
|
164
|
-
begin
|
|
165
|
-
Dir.chdir(schema_dir)
|
|
166
|
-
schema = Nokogiri::XML::Schema(File.read(File.basename(xsd_path)))
|
|
167
|
-
ensure
|
|
168
|
-
Dir.chdir(original_dir)
|
|
169
|
-
end
|
|
158
|
+
# Path-based compilation resolves xsd:import chains relative
|
|
159
|
+
# to the schema file's own directory; string compilation does
|
|
160
|
+
# not resolve them.
|
|
161
|
+
handle = Leptris::XML::XSD.compile_file(xsd_path)
|
|
162
|
+
schema = Moxml::XSD::Schema.new(handle, @moxml)
|
|
170
163
|
|
|
171
164
|
@schema_cache[xsd_relative_path] = schema
|
|
172
165
|
schema
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "zip"
|
|
4
|
-
require "
|
|
4
|
+
require "moxml"
|
|
5
5
|
# LayerValidator autoloaded via lib/uniword/validation.rb
|
|
6
6
|
|
|
7
7
|
module Uniword
|
|
@@ -14,7 +14,8 @@ module Uniword
|
|
|
14
14
|
# - XSD validation: parts validated against OOXML XSD schemas
|
|
15
15
|
#
|
|
16
16
|
# XSD validation uses SchemaRegistry for namespace-aware schema selection.
|
|
17
|
-
# Uses Moxml
|
|
17
|
+
# Uses Moxml throughout: parse, namespace detection, and the
|
|
18
|
+
# Moxml::XSD (Leptris engine) validator.
|
|
18
19
|
#
|
|
19
20
|
# @example Validate with XSD
|
|
20
21
|
# validator = XmlSchemaValidator.new("xml_schema" => { "xsd_validation" => true })
|
|
@@ -66,8 +67,8 @@ module Uniword
|
|
|
66
67
|
|
|
67
68
|
def validate_entry(entry, issues)
|
|
68
69
|
content = entry.get_input_stream.read
|
|
69
|
-
|
|
70
|
-
rescue
|
|
70
|
+
moxml.parse(content)
|
|
71
|
+
rescue Moxml::ParseError => e
|
|
71
72
|
issues << Report::ValidationIssue.new(
|
|
72
73
|
severity: "error",
|
|
73
74
|
code: "XSD-001",
|
|
@@ -87,16 +88,15 @@ module Uniword
|
|
|
87
88
|
begin
|
|
88
89
|
content = entry.get_input_stream.read
|
|
89
90
|
schema = registry.load_schema(schema_path)
|
|
90
|
-
doc =
|
|
91
|
+
doc = moxml.parse(content)
|
|
91
92
|
preprocess_mce(doc)
|
|
92
93
|
|
|
93
94
|
schema.validate(doc).each do |error|
|
|
94
95
|
issues << Report::ValidationIssue.new(
|
|
95
96
|
severity: "error",
|
|
96
97
|
code: "XSD-002",
|
|
97
|
-
message: error.
|
|
98
|
+
message: error.to_s.gsub("\n", " ").strip,
|
|
98
99
|
part: entry.name,
|
|
99
|
-
line: error.line,
|
|
100
100
|
)
|
|
101
101
|
end
|
|
102
102
|
|
|
@@ -120,42 +120,87 @@ module Uniword
|
|
|
120
120
|
end
|
|
121
121
|
end
|
|
122
122
|
|
|
123
|
-
#
|
|
124
|
-
# declaration
|
|
125
|
-
#
|
|
126
|
-
#
|
|
123
|
+
# Apply MCE preprocessing per the part's own mc:Ignorable
|
|
124
|
+
# declaration so strict XSD validation is not drowned in errors
|
|
125
|
+
# for extension content:
|
|
126
|
+
#
|
|
127
|
+
# - mc:AlternateContent is unwrapped to the first understood
|
|
128
|
+
# mc:Choice (else the mc:Fallback) per MCE branch selection;
|
|
129
|
+
# - elements in ignorable namespaces are removed (w15:docId,
|
|
130
|
+
# w16se:* settings extensions);
|
|
131
|
+
# - attributes in ignorable namespaces are removed (w14:paraId,
|
|
132
|
+
# w14:textId), plus the mc:Ignorable attribute itself, which
|
|
133
|
+
# is undeclared on most roots in the transitional XSDs.
|
|
127
134
|
#
|
|
128
|
-
# Extension ELEMENTS are deliberately kept: removing them changes
|
|
129
|
-
# libxml2's content-model evaluation and can expose pre-existing
|
|
130
|
-
# Word-vs-schema quirks (e.g. rsids placed late, tolerated via the
|
|
131
|
-
# sequence's trailing xsd:any) as false baseline errors.
|
|
132
135
|
# Operates in place on the parsed document; never touches the
|
|
133
136
|
# packaged bytes.
|
|
134
137
|
#
|
|
135
|
-
# @param doc [
|
|
138
|
+
# @param doc [Moxml::Document] parsed part to clean
|
|
136
139
|
# @return [void]
|
|
137
140
|
def preprocess_mce(doc)
|
|
138
141
|
root = doc.root
|
|
139
142
|
return unless root
|
|
140
143
|
|
|
141
|
-
|
|
142
|
-
|
|
144
|
+
uris = ignorable_namespace_uris(root)
|
|
145
|
+
unwrap_alternate_content(doc, uris)
|
|
146
|
+
remove_ignorable_elements(doc, uris) unless uris.empty?
|
|
147
|
+
strip_mce_attributes(root, uris + [MCE_NAMESPACE_URI])
|
|
148
|
+
end
|
|
143
149
|
|
|
144
|
-
|
|
145
|
-
|
|
150
|
+
# Namespace URIs the root declares ignorable via mc:Ignorable.
|
|
151
|
+
def ignorable_namespace_uris(root)
|
|
152
|
+
ignorable = root["mc:Ignorable"]
|
|
153
|
+
return [] if ignorable.nil? || ignorable.empty?
|
|
154
|
+
|
|
155
|
+
prefix_uris = root.namespaces.to_h { |ns| [ns.prefix, ns.uri] }
|
|
156
|
+
ignorable.split(/\s+/).filter_map { |prefix| prefix_uris[prefix] }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Replace each mc:AlternateContent with the content of its first
|
|
160
|
+
# understood mc:Choice, or its mc:Fallback when no Choice applies.
|
|
161
|
+
def unwrap_alternate_content(doc, ignorable_uris)
|
|
162
|
+
doc.xpath("//mc:AlternateContent", "mc" => MCE_NAMESPACE_URI)
|
|
163
|
+
.to_a.reverse.each do |alternate|
|
|
164
|
+
branch = chosen_branch(alternate, ignorable_uris)
|
|
165
|
+
if branch
|
|
166
|
+
anchor = alternate
|
|
167
|
+
branch.children.each do |child|
|
|
168
|
+
anchor.add_next_sibling(child)
|
|
169
|
+
anchor = child
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
alternate.remove
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# The first mc:Choice whose required namespaces are all
|
|
177
|
+
# non-ignorable, else the mc:Fallback, else nil.
|
|
178
|
+
def chosen_branch(alternate, ignorable_uris)
|
|
179
|
+
choices = alternate.xpath("./mc:Choice", "mc" => MCE_NAMESPACE_URI)
|
|
180
|
+
choices.find do |choice|
|
|
181
|
+
requires = choice.attributes
|
|
182
|
+
.find { |attr| attr.name == "Requires" }
|
|
183
|
+
&.value.to_s.split(/\s+/)
|
|
184
|
+
requires.all? { |prefix| !ignorable_uris.include?(prefix) }
|
|
185
|
+
end || alternate.at_xpath("./mc:Fallback", "mc" => MCE_NAMESPACE_URI)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Remove elements whose namespace is declared ignorable.
|
|
189
|
+
def remove_ignorable_elements(doc, uris)
|
|
190
|
+
doc.traverse do |node|
|
|
191
|
+
next unless node.is_a?(Moxml::Element)
|
|
192
|
+
|
|
193
|
+
node.remove if uris.include?(node.namespace&.uri)
|
|
146
194
|
end
|
|
147
|
-
# The mc:Ignorable attribute itself is MCE machinery and is
|
|
148
|
-
# undeclared on most root elements in the transitional XSDs.
|
|
149
|
-
strip_mce_attributes(root, uris + [MCE_NAMESPACE_URI])
|
|
150
195
|
end
|
|
151
196
|
|
|
152
197
|
# Remove attributes in ignorable namespaces from every element.
|
|
153
198
|
def strip_mce_attributes(doc, uris)
|
|
154
199
|
doc.traverse do |node|
|
|
155
|
-
next unless node.
|
|
200
|
+
next unless node.is_a?(Moxml::Element)
|
|
156
201
|
|
|
157
|
-
node.
|
|
158
|
-
attr.remove if uris.include?(attr.namespace&.
|
|
202
|
+
node.attributes.each do |attr|
|
|
203
|
+
attr.remove if uris.include?(attr.namespace&.uri)
|
|
159
204
|
end
|
|
160
205
|
end
|
|
161
206
|
end
|
|
@@ -192,6 +237,10 @@ module Uniword
|
|
|
192
237
|
layer_config["xsd_validation"] == true
|
|
193
238
|
end
|
|
194
239
|
|
|
240
|
+
def moxml
|
|
241
|
+
@moxml ||= Moxml.new
|
|
242
|
+
end
|
|
243
|
+
|
|
195
244
|
def layer_config
|
|
196
245
|
@config["xml_schema"] || {}
|
|
197
246
|
end
|
|
@@ -8,7 +8,7 @@ module Uniword
|
|
|
8
8
|
#
|
|
9
9
|
# Coordinates:
|
|
10
10
|
# 1. OPC Package validation (ZIP, content types, relationships)
|
|
11
|
-
# 2. XSD Schema validation (namespace-aware,
|
|
11
|
+
# 2. XSD Schema validation (namespace-aware, via Moxml::XSD)
|
|
12
12
|
# 3. Word Document semantic validation (extensible rule system)
|
|
13
13
|
#
|
|
14
14
|
# Returns a VerificationReport that can be serialized to JSON/YAML
|
data/lib/uniword/version.rb
CHANGED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module Uniword
|
|
6
|
+
module Wordprocessingml
|
|
7
|
+
# Tracked deletion
|
|
8
|
+
#
|
|
9
|
+
# Element: <w:del> (CT_RunTrackChange) — wraps the runs Word
|
|
10
|
+
# deleted while tracking changes. The wrapped runs carry their
|
|
11
|
+
# text in <w:delText> elements (deleted content, excluded from
|
|
12
|
+
# paragraph text until the deletion is rejected).
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# del = Deletion.new(id: "8", author: "Alice")
|
|
16
|
+
class Deletion < Lutaml::Model::Serializable
|
|
17
|
+
attribute :id, Ooxml::Types::WmlVal
|
|
18
|
+
attribute :author, Ooxml::Types::WmlVal
|
|
19
|
+
attribute :date, Ooxml::Types::WmlVal
|
|
20
|
+
attribute :runs, Run, collection: true, initialize_empty: true
|
|
21
|
+
|
|
22
|
+
xml do
|
|
23
|
+
element "del"
|
|
24
|
+
namespace Uniword::Ooxml::Namespaces::WordProcessingML
|
|
25
|
+
mixed_content
|
|
26
|
+
|
|
27
|
+
map_attribute "id", to: :id, render_nil: false
|
|
28
|
+
map_attribute "author", to: :author, render_nil: false
|
|
29
|
+
map_attribute "date", to: :date, render_nil: false
|
|
30
|
+
map_element "r", to: :runs, render_nil: false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Text of the deleted content (from <w:delText> elements).
|
|
34
|
+
#
|
|
35
|
+
# @return [String]
|
|
36
|
+
def text
|
|
37
|
+
runs.map { |run| run.del_text&.content.to_s }.join
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module Uniword
|
|
6
|
+
module Wordprocessingml
|
|
7
|
+
# Tracked insertion
|
|
8
|
+
#
|
|
9
|
+
# Element: <w:ins> (CT_RunTrackChange) — wraps the runs Word
|
|
10
|
+
# inserted while tracking changes. Attributes identify the
|
|
11
|
+
# revision; the wrapped runs are live content.
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# ins = Insertion.new(id: "7", author: "Alice",
|
|
15
|
+
# runs: [Run.new(text: "inserted")])
|
|
16
|
+
class Insertion < Lutaml::Model::Serializable
|
|
17
|
+
attribute :id, Ooxml::Types::WmlVal
|
|
18
|
+
attribute :author, Ooxml::Types::WmlVal
|
|
19
|
+
attribute :date, Ooxml::Types::WmlVal
|
|
20
|
+
attribute :runs, Run, collection: true, initialize_empty: true
|
|
21
|
+
|
|
22
|
+
xml do
|
|
23
|
+
element "ins"
|
|
24
|
+
namespace Uniword::Ooxml::Namespaces::WordProcessingML
|
|
25
|
+
mixed_content
|
|
26
|
+
|
|
27
|
+
map_attribute "id", to: :id, render_nil: false
|
|
28
|
+
map_attribute "author", to: :author, render_nil: false
|
|
29
|
+
map_attribute "date", to: :date, render_nil: false
|
|
30
|
+
map_element "r", to: :runs, render_nil: false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Text of the wrapped runs (live content).
|
|
34
|
+
#
|
|
35
|
+
# @return [String]
|
|
36
|
+
def text
|
|
37
|
+
runs.map(&:text_string).join
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -9,13 +9,13 @@ module Uniword
|
|
|
9
9
|
# Generated from OOXML schema: wordprocessingml.yml
|
|
10
10
|
# Element: <w:pgMar>
|
|
11
11
|
class PageMargins < Lutaml::Model::Serializable
|
|
12
|
-
attribute :top,
|
|
13
|
-
attribute :bottom,
|
|
14
|
-
attribute :left,
|
|
15
|
-
attribute :right,
|
|
16
|
-
attribute :header,
|
|
17
|
-
attribute :footer,
|
|
18
|
-
attribute :gutter,
|
|
12
|
+
attribute :top, Ooxml::Types::WmlInt
|
|
13
|
+
attribute :bottom, Ooxml::Types::WmlInt
|
|
14
|
+
attribute :left, Ooxml::Types::WmlInt
|
|
15
|
+
attribute :right, Ooxml::Types::WmlInt
|
|
16
|
+
attribute :header, Ooxml::Types::WmlInt
|
|
17
|
+
attribute :footer, Ooxml::Types::WmlInt
|
|
18
|
+
attribute :gutter, Ooxml::Types::WmlInt
|
|
19
19
|
|
|
20
20
|
xml do
|
|
21
21
|
element "pgMar"
|
|
@@ -12,6 +12,8 @@ module Uniword
|
|
|
12
12
|
class Paragraph < Lutaml::Model::Serializable
|
|
13
13
|
attribute :properties, ParagraphProperties
|
|
14
14
|
attribute :runs, Run, collection: true, initialize_empty: true
|
|
15
|
+
attribute :insertions, Insertion, collection: true, initialize_empty: true
|
|
16
|
+
attribute :deletions, Deletion, collection: true, initialize_empty: true
|
|
15
17
|
attribute :hyperlinks, Hyperlink, collection: true, initialize_empty: true
|
|
16
18
|
attribute :bookmark_starts, BookmarkStart, collection: true, initialize_empty: true
|
|
17
19
|
attribute :bookmark_ends, BookmarkEnd, collection: true, initialize_empty: true
|
|
@@ -61,6 +63,8 @@ module Uniword
|
|
|
61
63
|
|
|
62
64
|
map_element "pPr", to: :properties, render_nil: false
|
|
63
65
|
map_element "r", to: :runs, render_nil: false
|
|
66
|
+
map_element "ins", to: :insertions, render_nil: false
|
|
67
|
+
map_element "del", to: :deletions, render_nil: false
|
|
64
68
|
map_element "hyperlink", to: :hyperlinks, render_nil: false
|
|
65
69
|
map_element "bookmarkStart", to: :bookmark_starts, render_nil: false
|
|
66
70
|
map_element "bookmarkEnd", to: :bookmark_ends, render_nil: false
|
|
@@ -83,11 +87,33 @@ module Uniword
|
|
|
83
87
|
|
|
84
88
|
# Get paragraph text
|
|
85
89
|
#
|
|
86
|
-
#
|
|
90
|
+
# Insertion runs are live content and included, in document
|
|
91
|
+
# order; deletion runs are deleted content and excluded.
|
|
92
|
+
#
|
|
93
|
+
# @return [String] Combined text from runs and insertions
|
|
87
94
|
def text
|
|
88
|
-
|
|
95
|
+
text_sources.map do |source|
|
|
96
|
+
source.is_a?(Insertion) ? source.text : run_text(source)
|
|
97
|
+
end.join
|
|
98
|
+
end
|
|
89
99
|
|
|
90
|
-
|
|
100
|
+
# Runs and insertions in document order when element_order is
|
|
101
|
+
# available; typed collections in declaration order otherwise.
|
|
102
|
+
#
|
|
103
|
+
# @return [Array<Run, Insertion>]
|
|
104
|
+
def text_sources
|
|
105
|
+
order = element_order
|
|
106
|
+
return runs + insertions if order.nil? || order.empty?
|
|
107
|
+
|
|
108
|
+
runs_left = runs.dup
|
|
109
|
+
insertions_left = insertions.dup
|
|
110
|
+
walked = order.filter_map do |entry|
|
|
111
|
+
case entry.name
|
|
112
|
+
when "r" then runs_left.shift
|
|
113
|
+
when "ins" then insertions_left.shift
|
|
114
|
+
end
|
|
115
|
+
end.compact
|
|
116
|
+
walked + runs_left + insertions_left
|
|
91
117
|
end
|
|
92
118
|
|
|
93
119
|
# Extract text from a run or SDT element
|
|
@@ -107,7 +133,7 @@ module Uniword
|
|
|
107
133
|
end
|
|
108
134
|
|
|
109
135
|
def empty?
|
|
110
|
-
|
|
136
|
+
text.empty?
|
|
111
137
|
end
|
|
112
138
|
|
|
113
139
|
def style
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "lutaml/model"
|
|
4
|
-
require "nokogiri"
|
|
5
4
|
|
|
6
5
|
module Uniword
|
|
7
6
|
module Wordprocessingml
|
|
@@ -135,11 +134,11 @@ module Uniword
|
|
|
135
134
|
def self.from_xml(xml_content)
|
|
136
135
|
settings = super
|
|
137
136
|
|
|
138
|
-
doc =
|
|
137
|
+
doc = Moxml.parse(xml_content)
|
|
139
138
|
doc_ids = doc.xpath('//*[local-name()="docId"]')
|
|
140
139
|
doc_ids.each do |elem|
|
|
141
|
-
ns_uri = elem.namespace&.
|
|
142
|
-
val = elem.
|
|
140
|
+
ns_uri = elem.namespace&.uri
|
|
141
|
+
val = elem.attribute("val")&.value
|
|
143
142
|
next unless val
|
|
144
143
|
|
|
145
144
|
if (ns_uri == "http://schemas.microsoft.com/office/word/2012/wordml") && !settings.w15_doc_id&.val
|
|
@@ -141,21 +141,12 @@ module Uniword
|
|
|
141
141
|
end
|
|
142
142
|
|
|
143
143
|
# Paragraphs in body (incl. table cells), headers/footers,
|
|
144
|
-
# notes, comments.
|
|
144
|
+
# notes, comments. Reuses the typed-dispatch walker (Body maps
|
|
145
|
+
# tables and SDTs; TableCell/Header/Footer map tables;
|
|
146
|
+
# Footnote/Endnote/Comment map paragraphs only).
|
|
145
147
|
def walk_all_paragraphs(&block)
|
|
146
|
-
each_container
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
def walk_paragraphs(container, &block)
|
|
150
|
-
return unless container
|
|
151
|
-
|
|
152
|
-
(container.paragraphs || []).each(&block)
|
|
153
|
-
walk_tables(container) { |table| walk_table_rows(table, &block) }
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
def walk_table_rows(table, &block)
|
|
157
|
-
(table.rows || []).each do |row|
|
|
158
|
-
(row.cells || []).each { |cell| walk_paragraphs(cell, &block) }
|
|
148
|
+
each_container do |container|
|
|
149
|
+
FindReplace::ParagraphWalker.each_paragraph([container], &block)
|
|
159
150
|
end
|
|
160
151
|
end
|
|
161
152
|
|
|
@@ -22,8 +22,8 @@ module Uniword
|
|
|
22
22
|
mixed_content
|
|
23
23
|
|
|
24
24
|
map_element "top", to: :top, render_nil: false
|
|
25
|
-
map_element "bottom", to: :bottom, render_nil: false
|
|
26
25
|
map_element "left", to: :left, render_nil: false
|
|
26
|
+
map_element "bottom", to: :bottom, render_nil: false
|
|
27
27
|
map_element "right", to: :right, render_nil: false
|
|
28
28
|
map_element "insideH", to: :inside_h, render_nil: false
|
|
29
29
|
map_element "insideV", to: :inside_v, render_nil: false
|
|
@@ -20,8 +20,8 @@ module Uniword
|
|
|
20
20
|
mixed_content
|
|
21
21
|
|
|
22
22
|
map_element "top", to: :top, render_nil: false
|
|
23
|
-
map_element "bottom", to: :bottom, render_nil: false
|
|
24
23
|
map_element "left", to: :left, render_nil: false
|
|
24
|
+
map_element "bottom", to: :bottom, render_nil: false
|
|
25
25
|
map_element "right", to: :right, render_nil: false
|
|
26
26
|
end
|
|
27
27
|
end
|
|
@@ -31,6 +31,11 @@ module Uniword
|
|
|
31
31
|
autoload :Pict, "uniword/wordprocessingml/pict"
|
|
32
32
|
autoload :Symbol, "uniword/wordprocessingml/symbol"
|
|
33
33
|
autoload :DeletedText, "uniword/wordprocessingml/deleted_text"
|
|
34
|
+
|
|
35
|
+
# Tracked changes (CT_RunTrackChange wrappers)
|
|
36
|
+
autoload :Insertion, "uniword/wordprocessingml/insertion"
|
|
37
|
+
autoload :Deletion, "uniword/wordprocessingml/deletion"
|
|
38
|
+
|
|
34
39
|
autoload :NoBreakHyphen, "uniword/wordprocessingml/no_break_hyphen"
|
|
35
40
|
autoload :LastRenderedPageBreak,
|
|
36
41
|
"uniword/wordprocessingml/last_rendered_page_break"
|
|
@@ -18,7 +18,7 @@ module Uniword
|
|
|
18
18
|
namespace Uniword::Ooxml::Namespaces::WordProcessingDrawing
|
|
19
19
|
mixed_content
|
|
20
20
|
|
|
21
|
-
map_attribute "
|
|
21
|
+
map_attribute "relativeFrom", to: :relative_from
|
|
22
22
|
map_element "align", to: :align, render_nil: false
|
|
23
23
|
map_element "posOffset", to: :pos_offset, render_nil: false
|
|
24
24
|
end
|
|
@@ -18,7 +18,7 @@ module Uniword
|
|
|
18
18
|
namespace Uniword::Ooxml::Namespaces::WordProcessingDrawing
|
|
19
19
|
mixed_content
|
|
20
20
|
|
|
21
|
-
map_attribute "
|
|
21
|
+
map_attribute "relativeFrom", to: :relative_from
|
|
22
22
|
map_element "align", to: :align, render_nil: false
|
|
23
23
|
map_element "posOffset", to: :pos_offset, render_nil: false
|
|
24
24
|
end
|
|
@@ -17,7 +17,7 @@ module Uniword
|
|
|
17
17
|
namespace Uniword::Ooxml::Namespaces::WordProcessingDrawing
|
|
18
18
|
mixed_content
|
|
19
19
|
|
|
20
|
-
map_attribute "
|
|
20
|
+
map_attribute "relativeFrom", to: :relative_from
|
|
21
21
|
map_element "pctWidth", to: :pct_width, render_nil: false
|
|
22
22
|
end
|
|
23
23
|
end
|
|
@@ -17,7 +17,7 @@ module Uniword
|
|
|
17
17
|
namespace Uniword::Ooxml::Namespaces::WordProcessingDrawing
|
|
18
18
|
mixed_content
|
|
19
19
|
|
|
20
|
-
map_attribute "
|
|
20
|
+
map_attribute "relativeFrom", to: :relative_from
|
|
21
21
|
map_element "pctHeight", to: :pct_height, render_nil: false
|
|
22
22
|
end
|
|
23
23
|
end
|
data/lib/uniword.rb
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "lutaml/model"
|
|
4
|
-
require "nokogiri"
|
|
5
4
|
require "zip"
|
|
6
5
|
|
|
7
|
-
#
|
|
8
|
-
|
|
6
|
+
# XML hydration, XSD validation, and HTML parsing all route through
|
|
7
|
+
# lutaml-model's Moxml layer with the runtime default adapter
|
|
8
|
+
# (Leptris). Nokogiri remains available as an alternative Moxml
|
|
9
|
+
# adapter for development and testing.
|
|
9
10
|
|
|
10
11
|
# Uniword is a comprehensive Ruby library for reading and writing Microsoft Word
|
|
11
12
|
# documents in DOCX format using a schema-driven architecture.
|
|
@@ -147,6 +148,11 @@ module Uniword
|
|
|
147
148
|
autoload :Plugin, "uniword/plugin"
|
|
148
149
|
|
|
149
150
|
# CLI
|
|
151
|
+
module Cli
|
|
152
|
+
autoload :Completions, "uniword/cli/completions"
|
|
153
|
+
autoload :HelpSwitch, "uniword/cli/help_switch"
|
|
154
|
+
autoload :NoColor, "uniword/cli/no_color"
|
|
155
|
+
end
|
|
150
156
|
autoload :CLI, "uniword/cli/main"
|
|
151
157
|
autoload :GenerateCLI, "uniword/cli/generate_cli"
|
|
152
158
|
autoload :FontsCLI, "uniword/cli/fonts_cli"
|
|
@@ -372,10 +378,6 @@ module Uniword
|
|
|
372
378
|
doc
|
|
373
379
|
end
|
|
374
380
|
|
|
375
|
-
# Convert HTML to a document with paragraphs.
|
|
376
|
-
# Alias for {#from_html} for backward compatibility.
|
|
377
|
-
alias html_to_doc from_html
|
|
378
|
-
|
|
379
381
|
# Convert HTML directly to a DOCX file
|
|
380
382
|
#
|
|
381
383
|
# @param html [String] HTML content
|