uniword 1.6.1 → 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/lib/uniword/builder/chart_builder.rb +129 -134
- data/lib/uniword/diff/package_differ.rb +16 -15
- 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/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/ooxml/element_order.rb +21 -0
- data/lib/uniword/ooxml/schema/element_serializer.rb +27 -35
- data/lib/uniword/streaming_parser.rb +22 -20
- data/lib/uniword/theme/theme_xml_parser.rb +3 -3
- 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/schema_registry.rb +9 -16
- 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/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 +4 -8
- metadata +8 -9
- data/lib/uniword/infrastructure/xml_namespace_normalizer.rb +0 -107
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "nokogiri"
|
|
4
|
-
|
|
5
3
|
module Uniword
|
|
6
4
|
# Streaming parser for large DOCX documents.
|
|
7
5
|
#
|
|
@@ -48,21 +46,20 @@ module Uniword
|
|
|
48
46
|
# @param zip_content [Hash] Extracted ZIP content from DOCX
|
|
49
47
|
# @return [Document] The parsed document
|
|
50
48
|
def parse_streaming(zip_content)
|
|
51
|
-
document =
|
|
49
|
+
document = Wordprocessingml::DocumentRoot.new
|
|
52
50
|
document_xml = zip_content["word/document.xml"]
|
|
53
51
|
|
|
54
52
|
return document unless document_xml
|
|
55
53
|
|
|
56
|
-
#
|
|
54
|
+
# Stream via Moxml's SAX parser
|
|
57
55
|
handler = DocumentSaxHandler.new(document, @paragraph_limit, @table_limit)
|
|
58
|
-
|
|
59
|
-
parser.parse(document_xml)
|
|
56
|
+
Moxml.new.config.adapter.sax_parse(document_xml, handler)
|
|
60
57
|
|
|
61
58
|
document
|
|
62
59
|
end
|
|
63
60
|
|
|
64
61
|
# SAX handler for streaming document parsing
|
|
65
|
-
class DocumentSaxHandler <
|
|
62
|
+
class DocumentSaxHandler < Moxml::SAX::Handler
|
|
66
63
|
attr_reader :document
|
|
67
64
|
|
|
68
65
|
def initialize(document, paragraph_limit = nil, table_limit = nil)
|
|
@@ -86,8 +83,8 @@ module Uniword
|
|
|
86
83
|
end
|
|
87
84
|
|
|
88
85
|
# Called when an element starts
|
|
89
|
-
def
|
|
90
|
-
|
|
86
|
+
def on_start_element(name, _attributes = {}, _namespaces = {})
|
|
87
|
+
name = local_name(name)
|
|
91
88
|
@element_stack.push(name)
|
|
92
89
|
|
|
93
90
|
case name
|
|
@@ -96,7 +93,7 @@ module Uniword
|
|
|
96
93
|
when "r"
|
|
97
94
|
start_run
|
|
98
95
|
when "t"
|
|
99
|
-
start_text
|
|
96
|
+
start_text
|
|
100
97
|
when "tbl"
|
|
101
98
|
start_table
|
|
102
99
|
when "tr"
|
|
@@ -109,7 +106,8 @@ module Uniword
|
|
|
109
106
|
end
|
|
110
107
|
|
|
111
108
|
# Called when an element ends
|
|
112
|
-
def
|
|
109
|
+
def on_end_element(name)
|
|
110
|
+
name = local_name(name)
|
|
113
111
|
@element_stack.pop
|
|
114
112
|
|
|
115
113
|
case name
|
|
@@ -131,7 +129,7 @@ module Uniword
|
|
|
131
129
|
end
|
|
132
130
|
|
|
133
131
|
# Called when text content is encountered
|
|
134
|
-
def
|
|
132
|
+
def on_characters(string)
|
|
135
133
|
return unless @current_text
|
|
136
134
|
|
|
137
135
|
@current_text << string
|
|
@@ -139,6 +137,11 @@ module Uniword
|
|
|
139
137
|
|
|
140
138
|
private
|
|
141
139
|
|
|
140
|
+
# Strip any namespace prefix from a SAX element name.
|
|
141
|
+
def local_name(name)
|
|
142
|
+
name.split(":").last
|
|
143
|
+
end
|
|
144
|
+
|
|
142
145
|
# Check if we've hit paragraph limit
|
|
143
146
|
def paragraph_limit_reached?
|
|
144
147
|
@paragraph_limit && @paragraph_count >= @paragraph_limit
|
|
@@ -153,7 +156,7 @@ module Uniword
|
|
|
153
156
|
def start_paragraph
|
|
154
157
|
return if paragraph_limit_reached?
|
|
155
158
|
|
|
156
|
-
@current_paragraph = Paragraph.new
|
|
159
|
+
@current_paragraph = Wordprocessingml::Paragraph.new
|
|
157
160
|
@paragraph_count += 1
|
|
158
161
|
end
|
|
159
162
|
|
|
@@ -176,7 +179,7 @@ module Uniword
|
|
|
176
179
|
def start_run
|
|
177
180
|
return unless @current_paragraph
|
|
178
181
|
|
|
179
|
-
@current_run = Run.new
|
|
182
|
+
@current_run = Wordprocessingml::Run.new
|
|
180
183
|
end
|
|
181
184
|
|
|
182
185
|
# End parsing a run
|
|
@@ -188,7 +191,7 @@ module Uniword
|
|
|
188
191
|
end
|
|
189
192
|
|
|
190
193
|
# Start parsing text
|
|
191
|
-
def start_text
|
|
194
|
+
def start_text
|
|
192
195
|
return unless @current_run
|
|
193
196
|
|
|
194
197
|
@current_text = +""
|
|
@@ -198,8 +201,7 @@ module Uniword
|
|
|
198
201
|
def end_text
|
|
199
202
|
return unless @current_text && @current_run
|
|
200
203
|
|
|
201
|
-
|
|
202
|
-
@current_run.text_element = text_element
|
|
204
|
+
@current_run.text = [Wordprocessingml::Text.new(content: @current_text)]
|
|
203
205
|
@current_text = nil
|
|
204
206
|
end
|
|
205
207
|
|
|
@@ -207,7 +209,7 @@ module Uniword
|
|
|
207
209
|
def start_table
|
|
208
210
|
return if table_limit_reached?
|
|
209
211
|
|
|
210
|
-
@current_table = Table.new
|
|
212
|
+
@current_table = Wordprocessingml::Table.new
|
|
211
213
|
@table_count += 1
|
|
212
214
|
end
|
|
213
215
|
|
|
@@ -223,7 +225,7 @@ module Uniword
|
|
|
223
225
|
def start_table_row
|
|
224
226
|
return unless @current_table
|
|
225
227
|
|
|
226
|
-
@current_row = TableRow.new
|
|
228
|
+
@current_row = Wordprocessingml::TableRow.new
|
|
227
229
|
end
|
|
228
230
|
|
|
229
231
|
# End parsing a table row
|
|
@@ -238,7 +240,7 @@ module Uniword
|
|
|
238
240
|
def start_table_cell
|
|
239
241
|
return unless @current_row
|
|
240
242
|
|
|
241
|
-
@current_cell = TableCell.new
|
|
243
|
+
@current_cell = Wordprocessingml::TableCell.new
|
|
242
244
|
end
|
|
243
245
|
|
|
244
246
|
# End parsing a table cell
|
|
@@ -61,7 +61,7 @@ module Uniword
|
|
|
61
61
|
|
|
62
62
|
# Parse color scheme from XML
|
|
63
63
|
#
|
|
64
|
-
# @param node [
|
|
64
|
+
# @param node [Moxml::Element] Color scheme node
|
|
65
65
|
# @return [ColorScheme] Parsed color scheme
|
|
66
66
|
def parse_color_scheme(node)
|
|
67
67
|
scheme = ColorScheme.new
|
|
@@ -138,7 +138,7 @@ module Uniword
|
|
|
138
138
|
# - schemeClr: Reference to another theme color
|
|
139
139
|
# - prstClr: Preset color name
|
|
140
140
|
#
|
|
141
|
-
# @param node [
|
|
141
|
+
# @param node [Moxml::Element] Color node
|
|
142
142
|
# @return [String, nil] RGB hex value (6 characters, no #)
|
|
143
143
|
def extract_color_value(node)
|
|
144
144
|
# Try sRGB color first (direct RGB value)
|
|
@@ -180,7 +180,7 @@ module Uniword
|
|
|
180
180
|
|
|
181
181
|
# Parse font scheme from XML
|
|
182
182
|
#
|
|
183
|
-
# @param node [
|
|
183
|
+
# @param node [Moxml::Element] Font scheme node
|
|
184
184
|
# @return [FontScheme] Parsed font scheme
|
|
185
185
|
def parse_font_scheme(node)
|
|
186
186
|
scheme = FontScheme.new
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Uniword
|
|
4
4
|
module Transformation
|
|
5
|
-
# Builds OOXML element objects from parsed HTML (
|
|
5
|
+
# Builds OOXML element objects from parsed HTML (Moxml nodes).
|
|
6
6
|
#
|
|
7
7
|
# Pure functions — no state, no side effects.
|
|
8
8
|
# Used by HtmlToOoxmlConverter to construct OOXML paragraphs, tables, runs.
|
|
@@ -11,7 +11,7 @@ module Uniword
|
|
|
11
11
|
class HtmlElementBuilder
|
|
12
12
|
# Build an OOXML Paragraph from an HTML element.
|
|
13
13
|
#
|
|
14
|
-
# @param element [
|
|
14
|
+
# @param element [Moxml::Element] HTML element
|
|
15
15
|
# @return [Uniword::Wordprocessingml::Paragraph, nil]
|
|
16
16
|
def self.build_paragraph(element)
|
|
17
17
|
paragraph = Uniword::Wordprocessingml::Paragraph.new
|
|
@@ -32,11 +32,11 @@ module Uniword
|
|
|
32
32
|
# cell insertion. The grid layout is computed row-by-row to correctly
|
|
33
33
|
# place vMerge continuation cells where HTML rowspan omits cells.
|
|
34
34
|
#
|
|
35
|
-
# @param html_table [
|
|
35
|
+
# @param html_table [Moxml::Element] HTML table element
|
|
36
36
|
# @return [Uniword::Wordprocessingml::Table, nil]
|
|
37
37
|
def self.build_table(html_table)
|
|
38
38
|
table = Uniword::Wordprocessingml::Table.new
|
|
39
|
-
html_rows = html_table.
|
|
39
|
+
html_rows = html_table.xpath(".//tr")
|
|
40
40
|
return nil if html_rows.empty?
|
|
41
41
|
|
|
42
42
|
# Track columns occupied by rowspan: col_idx → remaining continuation rows
|
|
@@ -47,7 +47,7 @@ module Uniword
|
|
|
47
47
|
row = Uniword::Wordprocessingml::TableRow.new
|
|
48
48
|
col_idx = 0
|
|
49
49
|
|
|
50
|
-
html_row.
|
|
50
|
+
html_row.xpath(".//td | .//th").each do |html_cell|
|
|
51
51
|
# Insert vMerge continuation cells for columns occupied by rowspan
|
|
52
52
|
while occupied.key?(col_idx)
|
|
53
53
|
row.cells << create_vmerge_continuation_cell
|
|
@@ -60,13 +60,13 @@ module Uniword
|
|
|
60
60
|
row.cells << cell
|
|
61
61
|
|
|
62
62
|
# Track rowspan for continuation cell insertion in subsequent rows
|
|
63
|
-
rowspan = html_cell.
|
|
63
|
+
rowspan = html_cell.attribute("rowspan")&.value&.to_i
|
|
64
64
|
if rowspan && rowspan > 1
|
|
65
|
-
colspan = html_cell.
|
|
65
|
+
colspan = html_cell.attribute("colspan")&.value&.to_i || 1
|
|
66
66
|
colspan.times { |c| occupied[col_idx + c] = rowspan - 1 }
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
-
col_idx += html_cell.
|
|
69
|
+
col_idx += html_cell.attribute("colspan")&.value&.to_i || 1
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
# Handle remaining occupied columns at end of row
|
|
@@ -88,13 +88,13 @@ module Uniword
|
|
|
88
88
|
|
|
89
89
|
# Build an OOXML TableRow from an HTML tr element.
|
|
90
90
|
#
|
|
91
|
-
# @param html_row [
|
|
91
|
+
# @param html_row [Moxml::Element] HTML tr element
|
|
92
92
|
# @return [Uniword::Wordprocessingml::TableRow, nil]
|
|
93
93
|
def self.build_row(html_row)
|
|
94
94
|
row = Uniword::Wordprocessingml::TableRow.new
|
|
95
95
|
cells = []
|
|
96
96
|
|
|
97
|
-
html_row.
|
|
97
|
+
html_row.xpath(".//td | .//th").each do |html_cell|
|
|
98
98
|
cell = build_cell(html_cell)
|
|
99
99
|
cells << cell if cell
|
|
100
100
|
end
|
|
@@ -109,7 +109,7 @@ module Uniword
|
|
|
109
109
|
#
|
|
110
110
|
# Handles colspan → gridSpan, rowspan → vMerge restart, <th> → header.
|
|
111
111
|
#
|
|
112
|
-
# @param html_cell [
|
|
112
|
+
# @param html_cell [Moxml::Element] HTML td/th element
|
|
113
113
|
# @return [Uniword::Wordprocessingml::TableCell, nil]
|
|
114
114
|
def self.build_cell(html_cell)
|
|
115
115
|
cell = Uniword::Wordprocessingml::TableCell.new
|
|
@@ -118,8 +118,8 @@ module Uniword
|
|
|
118
118
|
cell.header = html_cell.name.downcase == "th"
|
|
119
119
|
|
|
120
120
|
# Handle colspan → gridSpan, rowspan → vMerge restart
|
|
121
|
-
colspan = html_cell.
|
|
122
|
-
rowspan = html_cell.
|
|
121
|
+
colspan = html_cell.attribute("colspan")&.value
|
|
122
|
+
rowspan = html_cell.attribute("rowspan")&.value
|
|
123
123
|
|
|
124
124
|
if (colspan && colspan.to_i > 1) || (rowspan && rowspan.to_i > 1)
|
|
125
125
|
cell.properties ||= Uniword::Wordprocessingml::TableCellProperties.new
|
|
@@ -134,7 +134,9 @@ module Uniword
|
|
|
134
134
|
end
|
|
135
135
|
|
|
136
136
|
# Convert cell content to paragraphs
|
|
137
|
-
|
|
137
|
+
block_tags = ".//p | .//div | .//h1 | .//h2 | .//h3 | .//h4 | " \
|
|
138
|
+
".//h5 | .//h6"
|
|
139
|
+
html_cell.xpath(block_tags).each do |para_element|
|
|
138
140
|
paragraph = build_paragraph(para_element)
|
|
139
141
|
cell.paragraphs << paragraph if paragraph
|
|
140
142
|
end
|
|
@@ -165,7 +167,7 @@ module Uniword
|
|
|
165
167
|
|
|
166
168
|
# Create an OOXML Run from an HTML element with inline formatting.
|
|
167
169
|
#
|
|
168
|
-
# @param element [
|
|
170
|
+
# @param element [Moxml::Element] HTML element
|
|
169
171
|
# @return [Uniword::Wordprocessingml::Run, nil]
|
|
170
172
|
def self.create_run_from_element(element)
|
|
171
173
|
text = element.text.strip
|
|
@@ -182,7 +184,7 @@ module Uniword
|
|
|
182
184
|
|
|
183
185
|
# Create an OOXML SDT from an HTML w:sdt element.
|
|
184
186
|
#
|
|
185
|
-
# @param element [
|
|
187
|
+
# @param element [Moxml::Element] HTML w:sdt element
|
|
186
188
|
# @return [Uniword::Wordprocessingml::StructuredDocumentTag, nil]
|
|
187
189
|
def self.create_sdt_from_element(element)
|
|
188
190
|
text = element.text.strip
|
|
@@ -213,7 +215,7 @@ module Uniword
|
|
|
213
215
|
|
|
214
216
|
# Apply CSS class → OOXML style mapping.
|
|
215
217
|
def self.apply_css_style(paragraph, element)
|
|
216
|
-
css_class = element.
|
|
218
|
+
css_class = element.attribute("class")&.value
|
|
217
219
|
return unless css_class && !css_class.empty?
|
|
218
220
|
|
|
219
221
|
mapped_style = HtmlFormattingMapper.map_css_class_to_style(css_class)
|
|
@@ -230,13 +232,12 @@ module Uniword
|
|
|
230
232
|
# → FootnoteReference run, <span class="MsoEndnoteReference"> → EndnoteReference run.
|
|
231
233
|
def self.build_children(paragraph, element)
|
|
232
234
|
element.children.each do |child|
|
|
233
|
-
|
|
234
|
-
when Nokogiri::XML::Node::TEXT_NODE
|
|
235
|
+
if child.is_a?(Moxml::Text)
|
|
235
236
|
text = child.text.strip
|
|
236
237
|
next if text.empty?
|
|
237
238
|
|
|
238
239
|
paragraph.runs << create_run(text)
|
|
239
|
-
|
|
240
|
+
elsif child.is_a?(Moxml::Element)
|
|
240
241
|
case child.name.downcase
|
|
241
242
|
when "w:sdt", "sdt"
|
|
242
243
|
sdt = create_sdt_from_element(child)
|
|
@@ -273,44 +274,53 @@ module Uniword
|
|
|
273
274
|
|
|
274
275
|
# Parse SDT attributes from HTML element.
|
|
275
276
|
def self.parse_sdt_attributes(element)
|
|
276
|
-
|
|
277
|
-
return nil if attrs.empty?
|
|
277
|
+
return nil if element.attributes.empty?
|
|
278
278
|
|
|
279
279
|
sdt_props =
|
|
280
280
|
Uniword::Wordprocessingml::StructuredDocumentTagProperties.new
|
|
281
281
|
|
|
282
|
-
if
|
|
282
|
+
if attr_value(element, "showingplchdr") ||
|
|
283
|
+
attr_value(element, "ShowingPlcHdr")
|
|
283
284
|
sdt_props.showing_placeholder_header =
|
|
284
285
|
Uniword::Wordprocessingml::StructuredDocumentTag::ShowingPlaceholderHeader.new
|
|
285
286
|
end
|
|
286
287
|
|
|
287
|
-
if
|
|
288
|
+
if attr_value(element, "temporary") ||
|
|
289
|
+
attr_value(element, "Temporary")
|
|
288
290
|
sdt_props.temporary =
|
|
289
291
|
Uniword::Wordprocessingml::StructuredDocumentTag::Temporary.new
|
|
290
292
|
end
|
|
291
293
|
|
|
292
|
-
doc_part =
|
|
294
|
+
doc_part = attr_value(element, "docpart") ||
|
|
295
|
+
attr_value(element, "DocPart")
|
|
293
296
|
if doc_part
|
|
294
297
|
placeholder =
|
|
295
298
|
Uniword::Wordprocessingml::StructuredDocumentTag::Placeholder.new
|
|
296
299
|
doc_part_ref =
|
|
297
|
-
Uniword::Wordprocessingml::StructuredDocumentTag::DocPartReference.new(
|
|
300
|
+
Uniword::Wordprocessingml::StructuredDocumentTag::DocPartReference.new(
|
|
301
|
+
value: doc_part,
|
|
302
|
+
)
|
|
298
303
|
placeholder.doc_part = doc_part_ref
|
|
299
304
|
sdt_props.placeholder = placeholder
|
|
300
305
|
end
|
|
301
306
|
|
|
302
|
-
if
|
|
307
|
+
if attr_value(element, "text") || attr_value(element, "Text")
|
|
303
308
|
sdt_props.text =
|
|
304
|
-
Uniword::Wordprocessingml::StructuredDocumentTag::Text.new(
|
|
309
|
+
Uniword::Wordprocessingml::StructuredDocumentTag::Text.new(
|
|
310
|
+
value: "whole",
|
|
311
|
+
)
|
|
305
312
|
end
|
|
306
313
|
|
|
307
|
-
id_attr =
|
|
314
|
+
id_attr = attr_value(element, "id") || attr_value(element, "ID")
|
|
308
315
|
if id_attr
|
|
309
316
|
sdt_props.id =
|
|
310
|
-
Uniword::Wordprocessingml::StructuredDocumentTag::Id.new(
|
|
317
|
+
Uniword::Wordprocessingml::StructuredDocumentTag::Id.new(
|
|
318
|
+
value: id_attr.to_i,
|
|
319
|
+
)
|
|
311
320
|
end
|
|
312
321
|
|
|
313
|
-
if
|
|
322
|
+
if attr_value(element, "bibliography") ||
|
|
323
|
+
attr_value(element, "Bibliography")
|
|
314
324
|
sdt_props.bibliography =
|
|
315
325
|
Uniword::Wordprocessingml::StructuredDocumentTag::Bibliography.new
|
|
316
326
|
end
|
|
@@ -318,15 +328,20 @@ module Uniword
|
|
|
318
328
|
sdt_props
|
|
319
329
|
end
|
|
320
330
|
|
|
331
|
+
# Read an attribute value from a Moxml element by name.
|
|
332
|
+
def self.attr_value(element, name)
|
|
333
|
+
element.attribute(name)&.value
|
|
334
|
+
end
|
|
335
|
+
|
|
321
336
|
# Create a Run with Break from an HTML <br> element.
|
|
322
337
|
#
|
|
323
|
-
# @param element [
|
|
338
|
+
# @param element [Moxml::Element] <br> element
|
|
324
339
|
# @return [Uniword::Wordprocessingml::Run]
|
|
325
340
|
def self.create_break_run(element)
|
|
326
341
|
run = Uniword::Wordprocessingml::Run.new
|
|
327
342
|
brk = Uniword::Wordprocessingml::Break.new
|
|
328
|
-
style = element.
|
|
329
|
-
clear = element.
|
|
343
|
+
style = element.attribute("style")&.value.to_s
|
|
344
|
+
clear = element.attribute("clear")&.value.to_s
|
|
330
345
|
if style.include?("page-break-before") || style.include?("page-break-after") || clear == "all"
|
|
331
346
|
brk.type = "page"
|
|
332
347
|
end
|
|
@@ -336,27 +351,27 @@ module Uniword
|
|
|
336
351
|
|
|
337
352
|
# Check if element is a footnote reference span.
|
|
338
353
|
#
|
|
339
|
-
# @param element [
|
|
354
|
+
# @param element [Moxml::Element]
|
|
340
355
|
# @return [Boolean]
|
|
341
356
|
def self.footnote_reference_span?(element)
|
|
342
357
|
return false unless element.name.downcase == "span"
|
|
343
358
|
|
|
344
|
-
element.
|
|
359
|
+
element.attribute("class")&.value.to_s.split.include?("MsoFootnoteReference")
|
|
345
360
|
end
|
|
346
361
|
|
|
347
362
|
# Check if element is an endnote reference span.
|
|
348
363
|
#
|
|
349
|
-
# @param element [
|
|
364
|
+
# @param element [Moxml::Element]
|
|
350
365
|
# @return [Boolean]
|
|
351
366
|
def self.endnote_reference_span?(element)
|
|
352
367
|
return false unless element.name.downcase == "span"
|
|
353
368
|
|
|
354
|
-
element.
|
|
369
|
+
element.attribute("class")&.value.to_s.split.include?("MsoEndnoteReference")
|
|
355
370
|
end
|
|
356
371
|
|
|
357
372
|
# Create a Run with FootnoteReference from an HTML footnote reference span.
|
|
358
373
|
#
|
|
359
|
-
# @param element [
|
|
374
|
+
# @param element [Moxml::Element] Span with MsoFootnoteReference class
|
|
360
375
|
# @return [Uniword::Wordprocessingml::Run]
|
|
361
376
|
def self.create_footnote_reference_run(element)
|
|
362
377
|
run = Uniword::Wordprocessingml::Run.new
|
|
@@ -368,7 +383,7 @@ module Uniword
|
|
|
368
383
|
|
|
369
384
|
# Create a Run with EndnoteReference from an HTML endnote reference span.
|
|
370
385
|
#
|
|
371
|
-
# @param element [
|
|
386
|
+
# @param element [Moxml::Element] Span with MsoEndnoteReference class
|
|
372
387
|
# @return [Uniword::Wordprocessingml::Run]
|
|
373
388
|
def self.create_endnote_reference_run(element)
|
|
374
389
|
run = Uniword::Wordprocessingml::Run.new
|
|
@@ -382,13 +397,13 @@ module Uniword
|
|
|
382
397
|
#
|
|
383
398
|
# Tries: nested <a href="#_ftnN">, then text content digits.
|
|
384
399
|
#
|
|
385
|
-
# @param element [
|
|
400
|
+
# @param element [Moxml::Element]
|
|
386
401
|
# @return [String, nil]
|
|
387
402
|
def self.extract_note_id(element)
|
|
388
403
|
# Try nested <a href="#_ftnN"> or <a href="#_ednN">
|
|
389
|
-
anchor = element.
|
|
404
|
+
anchor = element.at_xpath('.//a[starts-with(@href, "#")]')
|
|
390
405
|
if anchor
|
|
391
|
-
href = anchor.
|
|
406
|
+
href = anchor.attribute("href")&.value&.sub(/^#/, "")
|
|
392
407
|
if href =~ /(\d+)\s*$/
|
|
393
408
|
return Regexp.last_match(1)
|
|
394
409
|
end
|
|
@@ -405,23 +420,22 @@ module Uniword
|
|
|
405
420
|
|
|
406
421
|
# Create a Hyperlink from an HTML <a href> element.
|
|
407
422
|
#
|
|
408
|
-
# @param element [
|
|
423
|
+
# @param element [Moxml::Element] <a> element
|
|
409
424
|
# @return [Uniword::Wordprocessingml::Hyperlink, nil]
|
|
410
425
|
def self.create_hyperlink(element)
|
|
411
|
-
href = element.
|
|
426
|
+
href = element.attribute("href")&.value
|
|
412
427
|
return nil unless href
|
|
413
428
|
|
|
414
429
|
hyperlink = Uniword::Wordprocessingml::Hyperlink.new
|
|
415
430
|
hyperlink.target = href
|
|
416
431
|
|
|
417
432
|
element.children.each do |child|
|
|
418
|
-
|
|
419
|
-
when Nokogiri::XML::Node::TEXT_NODE
|
|
433
|
+
if child.is_a?(Moxml::Text)
|
|
420
434
|
text = child.text.strip
|
|
421
435
|
next if text.empty?
|
|
422
436
|
|
|
423
437
|
hyperlink.runs << create_run(text)
|
|
424
|
-
|
|
438
|
+
elsif child.is_a?(Moxml::Element)
|
|
425
439
|
run = create_run_from_element(child)
|
|
426
440
|
hyperlink.runs << run if run
|
|
427
441
|
end
|
|
@@ -30,7 +30,7 @@ module Uniword
|
|
|
30
30
|
#
|
|
31
31
|
# Handles nested formatting like <u><em><strong>text</strong></em></u>.
|
|
32
32
|
#
|
|
33
|
-
# @param element [
|
|
33
|
+
# @param element [Moxml::Element] HTML element
|
|
34
34
|
# @return [Uniword::Wordprocessingml::RunProperties]
|
|
35
35
|
def self.collect_formatting(element)
|
|
36
36
|
props = Uniword::Wordprocessingml::RunProperties.new
|
|
@@ -118,10 +118,10 @@ module Uniword
|
|
|
118
118
|
|
|
119
119
|
# Apply formatting from span element's style attribute.
|
|
120
120
|
#
|
|
121
|
-
# @param element [
|
|
121
|
+
# @param element [Moxml::Element] span element
|
|
122
122
|
# @param props [Uniword::Wordprocessingml::RunProperties]
|
|
123
123
|
def self.apply_span_styles(element, props)
|
|
124
|
-
style = element.
|
|
124
|
+
style = element.attribute("style")&.value || ""
|
|
125
125
|
|
|
126
126
|
if style =~ /color:\s*#?([0-9a-fA-F]{6})/i
|
|
127
127
|
props.color =
|
|
@@ -18,15 +18,15 @@ module Uniword
|
|
|
18
18
|
return [] if html_content.nil? || html_content.empty?
|
|
19
19
|
|
|
20
20
|
body = HtmlFormattingMapper.extract_body(html_content)
|
|
21
|
-
doc =
|
|
21
|
+
doc = Moxml.new.parse_html(body)
|
|
22
22
|
paragraphs = []
|
|
23
23
|
|
|
24
|
-
doc.
|
|
25
|
-
next if element.ancestors("td
|
|
24
|
+
doc.xpath(".//p | .//h1 | .//h2 | .//h3 | .//h4 | .//h5 | .//h6 | .//li | .//div | .//tr").each do |element|
|
|
25
|
+
next if element.ancestors("td").any? || element.ancestors("th").any?
|
|
26
26
|
next if %w[tr td].include?(element.name)
|
|
27
27
|
|
|
28
28
|
if %w[div li].include?(element.name) &&
|
|
29
|
-
element.
|
|
29
|
+
element.xpath(".//p | .//h1 | .//h2 | .//h3 | .//h4 | .//h5 | .//h6 | .//li | .//div | .//tr").any?
|
|
30
30
|
next
|
|
31
31
|
end
|
|
32
32
|
|
|
@@ -45,10 +45,10 @@ module Uniword
|
|
|
45
45
|
return [] if html_content.nil? || html_content.empty?
|
|
46
46
|
|
|
47
47
|
body = HtmlFormattingMapper.extract_body(html_content)
|
|
48
|
-
doc =
|
|
48
|
+
doc = Moxml.new.parse_html(body)
|
|
49
49
|
tables = []
|
|
50
50
|
|
|
51
|
-
doc.
|
|
51
|
+
doc.xpath(".//table").each do |html_table|
|
|
52
52
|
table = HtmlElementBuilder.build_table(html_table)
|
|
53
53
|
tables << table if table
|
|
54
54
|
end
|
|
@@ -58,7 +58,7 @@ module Uniword
|
|
|
58
58
|
|
|
59
59
|
# Convert a single HTML table to OOXML table.
|
|
60
60
|
#
|
|
61
|
-
# @param html_table [
|
|
61
|
+
# @param html_table [Moxml::Element] HTML table element
|
|
62
62
|
# @return [Uniword::Wordprocessingml::Table, nil]
|
|
63
63
|
def self.html_table_to_table(html_table)
|
|
64
64
|
HtmlElementBuilder.build_table(html_table)
|
|
@@ -66,7 +66,7 @@ module Uniword
|
|
|
66
66
|
|
|
67
67
|
# Convert a single HTML row to OOXML table row.
|
|
68
68
|
#
|
|
69
|
-
# @param html_row [
|
|
69
|
+
# @param html_row [Moxml::Element] HTML tr element
|
|
70
70
|
# @return [Uniword::Wordprocessingml::TableRow, nil]
|
|
71
71
|
def self.html_row_to_row(html_row)
|
|
72
72
|
HtmlElementBuilder.build_row(html_row)
|
|
@@ -74,7 +74,7 @@ module Uniword
|
|
|
74
74
|
|
|
75
75
|
# Convert a single HTML cell to OOXML table cell.
|
|
76
76
|
#
|
|
77
|
-
# @param html_cell [
|
|
77
|
+
# @param html_cell [Moxml::Element] HTML td/th element
|
|
78
78
|
# @return [Uniword::Wordprocessingml::TableCell, nil]
|
|
79
79
|
def self.html_cell_to_cell(html_cell)
|
|
80
80
|
HtmlElementBuilder.build_cell(html_cell)
|
|
@@ -82,7 +82,7 @@ module Uniword
|
|
|
82
82
|
|
|
83
83
|
# Convert a single HTML element to OOXML paragraph.
|
|
84
84
|
#
|
|
85
|
-
# @param element [
|
|
85
|
+
# @param element [Moxml::Element] HTML element
|
|
86
86
|
# @return [Uniword::Wordprocessingml::Paragraph, nil]
|
|
87
87
|
def self.html_element_to_paragraph(element)
|
|
88
88
|
HtmlElementBuilder.build_paragraph(element)
|
|
@@ -98,7 +98,7 @@ module Uniword
|
|
|
98
98
|
|
|
99
99
|
# Create a run from HTML element with inline formatting.
|
|
100
100
|
#
|
|
101
|
-
# @param element [
|
|
101
|
+
# @param element [Moxml::Element] HTML element
|
|
102
102
|
# @return [Uniword::Wordprocessingml::Run, nil]
|
|
103
103
|
def self.create_run_from_element(element)
|
|
104
104
|
HtmlElementBuilder.create_run_from_element(element)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "nokogiri"
|
|
4
3
|
|
|
5
4
|
module Uniword
|
|
6
5
|
module Transformation
|
|
@@ -625,7 +624,7 @@ module Uniword
|
|
|
625
624
|
return "" unless content
|
|
626
625
|
|
|
627
626
|
xml = content.to_xml
|
|
628
|
-
doc =
|
|
627
|
+
doc = Moxml.new.parse_html(xml)
|
|
629
628
|
doc.text.gsub(/\s+/, " ").strip
|
|
630
629
|
end
|
|
631
630
|
|
|
@@ -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 = {
|
|
@@ -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
|