coradoc-docx 0.1.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 +7 -0
- data/README.adoc +164 -0
- data/lib/coradoc/docx/transform/context.rb +72 -0
- data/lib/coradoc/docx/transform/from_core_model.rb +577 -0
- data/lib/coradoc/docx/transform/numbering_resolver.rb +127 -0
- data/lib/coradoc/docx/transform/ordered_content.rb +95 -0
- data/lib/coradoc/docx/transform/rule.rb +57 -0
- data/lib/coradoc/docx/transform/rule_registry.rb +60 -0
- data/lib/coradoc/docx/transform/rules/bookmark_rule.rb +34 -0
- data/lib/coradoc/docx/transform/rules/break_rule.rb +30 -0
- data/lib/coradoc/docx/transform/rules/footnote_rule.rb +27 -0
- data/lib/coradoc/docx/transform/rules/heading_rule.rb +53 -0
- data/lib/coradoc/docx/transform/rules/hyperlink_rule.rb +58 -0
- data/lib/coradoc/docx/transform/rules/image_rule.rb +125 -0
- data/lib/coradoc/docx/transform/rules/list_item_rule.rb +47 -0
- data/lib/coradoc/docx/transform/rules/math_rule.rb +82 -0
- data/lib/coradoc/docx/transform/rules/paragraph_rule.rb +65 -0
- data/lib/coradoc/docx/transform/rules/proof_error_rule.rb +25 -0
- data/lib/coradoc/docx/transform/rules/run_rule.rb +189 -0
- data/lib/coradoc/docx/transform/rules/simple_field_rule.rb +87 -0
- data/lib/coradoc/docx/transform/rules/structured_document_tag_rule.rb +36 -0
- data/lib/coradoc/docx/transform/rules/table_rule.rb +85 -0
- data/lib/coradoc/docx/transform/rules/text_rule.rb +25 -0
- data/lib/coradoc/docx/transform/style_resolver.rb +249 -0
- data/lib/coradoc/docx/transform/to_core_model.rb +340 -0
- data/lib/coradoc/docx/transform.rb +38 -0
- data/lib/coradoc/docx/version.rb +7 -0
- data/lib/coradoc/docx.rb +99 -0
- metadata +155 -0
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'uniword'
|
|
4
|
+
|
|
5
|
+
module Coradoc
|
|
6
|
+
module Docx
|
|
7
|
+
module Transform
|
|
8
|
+
# Transforms CoreModel to OOXML document via Uniword Builder.
|
|
9
|
+
#
|
|
10
|
+
# Follows the hub-and-spoke architecture: CoreModel elements are
|
|
11
|
+
# dispatched to handler methods that produce Uniword OOXML objects.
|
|
12
|
+
# The resulting DocumentRoot is serialized to .docx format.
|
|
13
|
+
#
|
|
14
|
+
# @example Convert CoreModel to DOCX file
|
|
15
|
+
# Coradoc::Docx::Transform::FromCoreModel.transform_to_file(core, "output.docx")
|
|
16
|
+
#
|
|
17
|
+
# @example Get Uniword DocumentRoot
|
|
18
|
+
# doc = Coradoc::Docx::Transform::FromCoreModel.transform(core)
|
|
19
|
+
# doc.save("output.docx")
|
|
20
|
+
class FromCoreModel
|
|
21
|
+
class << self
|
|
22
|
+
# Transform a CoreModel document to a Uniword DocumentRoot
|
|
23
|
+
#
|
|
24
|
+
# @param core [Coradoc::CoreModel::Base] CoreModel document
|
|
25
|
+
# @return [Uniword::Wordprocessingml::DocumentRoot] OOXML document
|
|
26
|
+
def transform(core)
|
|
27
|
+
new.transform(core)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Transform a CoreModel document and save to .docx file
|
|
31
|
+
#
|
|
32
|
+
# @param core [Coradoc::CoreModel::Base] CoreModel document
|
|
33
|
+
# @param path [String] output file path
|
|
34
|
+
# @return [void]
|
|
35
|
+
def transform_to_file(core, path)
|
|
36
|
+
doc = transform(core)
|
|
37
|
+
doc.save(path)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def transform(core)
|
|
42
|
+
case core
|
|
43
|
+
when Coradoc::CoreModel::StructuralElement
|
|
44
|
+
transform_structural_element(core)
|
|
45
|
+
when Coradoc::CoreModel::AnnotationBlock
|
|
46
|
+
transform_annotation_block(core)
|
|
47
|
+
when Coradoc::CoreModel::Block
|
|
48
|
+
transform_block(core)
|
|
49
|
+
when Coradoc::CoreModel::ListBlock
|
|
50
|
+
transform_list(core)
|
|
51
|
+
when Coradoc::CoreModel::Table
|
|
52
|
+
transform_table(core)
|
|
53
|
+
when Coradoc::CoreModel::Image
|
|
54
|
+
transform_image(core)
|
|
55
|
+
when Coradoc::CoreModel::InlineElement
|
|
56
|
+
transform_inline(core)
|
|
57
|
+
when Coradoc::CoreModel::FootnoteReference
|
|
58
|
+
build_ooxml_footnote_reference(core)
|
|
59
|
+
when Coradoc::CoreModel::Footnote
|
|
60
|
+
build_ooxml_footnote(core)
|
|
61
|
+
when Coradoc::CoreModel::DefinitionList
|
|
62
|
+
build_ooxml_definition_list(core)
|
|
63
|
+
when Coradoc::CoreModel::Toc
|
|
64
|
+
build_ooxml_toc(core)
|
|
65
|
+
when Coradoc::CoreModel::Term
|
|
66
|
+
build_ooxml_term(core)
|
|
67
|
+
when Coradoc::CoreModel::Abbreviation
|
|
68
|
+
build_ooxml_abbreviation(core)
|
|
69
|
+
when Coradoc::CoreModel::Bibliography
|
|
70
|
+
build_ooxml_bibliography(core)
|
|
71
|
+
when Coradoc::CoreModel::BibliographyEntry
|
|
72
|
+
build_ooxml_bibliography_entry(core)
|
|
73
|
+
when Coradoc::CoreModel::TocEntry
|
|
74
|
+
build_ooxml_toc_entry(core)
|
|
75
|
+
when Array
|
|
76
|
+
transform_array(core)
|
|
77
|
+
else
|
|
78
|
+
core
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def transform_structural_element(element)
|
|
85
|
+
case element.element_type
|
|
86
|
+
when 'document'
|
|
87
|
+
transform_document(element)
|
|
88
|
+
when 'section'
|
|
89
|
+
transform_section(element)
|
|
90
|
+
else
|
|
91
|
+
transform_section(element)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def transform_document(element)
|
|
96
|
+
doc = Uniword::Builder::DocumentBuilder.new
|
|
97
|
+
doc.title(element.title) if element.title
|
|
98
|
+
|
|
99
|
+
transform_children(element.children, doc)
|
|
100
|
+
|
|
101
|
+
doc.build
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def transform_section(element)
|
|
105
|
+
paragraphs = []
|
|
106
|
+
|
|
107
|
+
paragraphs << build_heading(element.title, level: element.heading_level) if element.title
|
|
108
|
+
|
|
109
|
+
element.children&.each do |child|
|
|
110
|
+
case child
|
|
111
|
+
when Coradoc::CoreModel::StructuralElement
|
|
112
|
+
paragraphs << transform_section(child)
|
|
113
|
+
when Coradoc::CoreModel::AnnotationBlock
|
|
114
|
+
paragraphs << transform_annotation_block(child)
|
|
115
|
+
when Coradoc::CoreModel::Block
|
|
116
|
+
paragraphs << build_ooxml_paragraph(child)
|
|
117
|
+
when Coradoc::CoreModel::ListBlock
|
|
118
|
+
paragraphs.concat(build_ooxml_list(child))
|
|
119
|
+
when Coradoc::CoreModel::Table
|
|
120
|
+
paragraphs << build_ooxml_table(child)
|
|
121
|
+
when Coradoc::CoreModel::Image
|
|
122
|
+
paragraphs << build_ooxml_image(child)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
paragraphs
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def transform_block(block)
|
|
130
|
+
case block.element_type
|
|
131
|
+
when 'page_break'
|
|
132
|
+
build_page_break
|
|
133
|
+
when 'paragraph', nil
|
|
134
|
+
build_ooxml_paragraph(block)
|
|
135
|
+
when 'comment'
|
|
136
|
+
nil
|
|
137
|
+
else
|
|
138
|
+
build_ooxml_paragraph(block)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def transform_annotation_block(annotation)
|
|
143
|
+
para = Uniword::Wordprocessingml::Paragraph.new
|
|
144
|
+
type_run = Uniword::Wordprocessingml::Run.new
|
|
145
|
+
type_run.text = Uniword::Wordprocessingml::Text.new(
|
|
146
|
+
content: "#{annotation.annotation_type}: "
|
|
147
|
+
)
|
|
148
|
+
type_run.properties = Uniword::Wordprocessingml::RunProperties.new
|
|
149
|
+
type_run.properties.bold = Uniword::Properties::Bold.new
|
|
150
|
+
|
|
151
|
+
text_run = Uniword::Wordprocessingml::Run.new
|
|
152
|
+
text_run.text = Uniword::Wordprocessingml::Text.new(content: annotation.flat_text)
|
|
153
|
+
|
|
154
|
+
para.runs << type_run
|
|
155
|
+
para.runs << text_run
|
|
156
|
+
para
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def transform_list(list_block)
|
|
160
|
+
build_ooxml_list(list_block)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def transform_table(table)
|
|
164
|
+
build_ooxml_table(table)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def transform_image(image)
|
|
168
|
+
build_ooxml_image(image)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def transform_inline(inline)
|
|
172
|
+
build_ooxml_run(inline)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def transform_array(elements)
|
|
176
|
+
elements.map { |e| transform(e) }
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Helper to transform children into builder calls
|
|
180
|
+
def transform_children(children, builder)
|
|
181
|
+
return unless children
|
|
182
|
+
|
|
183
|
+
children.each do |child|
|
|
184
|
+
case child
|
|
185
|
+
when Coradoc::CoreModel::StructuralElement
|
|
186
|
+
add_section_to_builder(child, builder)
|
|
187
|
+
when Coradoc::CoreModel::AnnotationBlock
|
|
188
|
+
add_annotation_to_builder(child, builder)
|
|
189
|
+
when Coradoc::CoreModel::Block
|
|
190
|
+
add_block_to_builder(child, builder)
|
|
191
|
+
when Coradoc::CoreModel::ListBlock
|
|
192
|
+
add_list_to_builder(child, builder)
|
|
193
|
+
when Coradoc::CoreModel::Table
|
|
194
|
+
add_table_to_builder(child, builder)
|
|
195
|
+
when Coradoc::CoreModel::Image
|
|
196
|
+
add_image_to_builder(child, builder)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def add_section_to_builder(element, builder)
|
|
202
|
+
level = element.heading_level
|
|
203
|
+
builder.heading(element.title, level: level) if element.title
|
|
204
|
+
|
|
205
|
+
transform_children(element.children, builder)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def add_block_to_builder(block, builder)
|
|
209
|
+
case block.element_type
|
|
210
|
+
when 'page_break'
|
|
211
|
+
builder.page_break
|
|
212
|
+
else
|
|
213
|
+
add_paragraph_to_builder(block, builder)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def add_annotation_to_builder(annotation, builder)
|
|
218
|
+
text = "#{annotation.annotation_type}: #{annotation.flat_text}"
|
|
219
|
+
builder.paragraph do |p|
|
|
220
|
+
p << Uniword::Builder.text(text, bold: true)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def add_paragraph_to_builder(block, builder)
|
|
225
|
+
content = block.renderable_content
|
|
226
|
+
|
|
227
|
+
if content.is_a?(Array) && content.any? { |c| !c.is_a?(String) }
|
|
228
|
+
builder.paragraph do |p|
|
|
229
|
+
content.each do |child|
|
|
230
|
+
case child
|
|
231
|
+
when String
|
|
232
|
+
p << child
|
|
233
|
+
when Coradoc::CoreModel::InlineElement
|
|
234
|
+
p << build_inline_text(child)
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
else
|
|
239
|
+
text = content.is_a?(Array) ? content.join : content.to_s
|
|
240
|
+
builder.paragraph(text)
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def add_list_to_builder(list_block, builder)
|
|
245
|
+
list_method = list_block.marker_type == 'numbered' ? :numbered_list : :bullet_list
|
|
246
|
+
|
|
247
|
+
if list_method == :numbered_list
|
|
248
|
+
builder.numbered_list do |list|
|
|
249
|
+
list_block.items.each do |item|
|
|
250
|
+
list.item(item.flat_text)
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
else
|
|
254
|
+
builder.bullet_list do |list|
|
|
255
|
+
list_block.items.each do |item|
|
|
256
|
+
list.item(item.flat_text)
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def add_table_to_builder(table, builder)
|
|
263
|
+
builder.table do |t|
|
|
264
|
+
table.rows.each do |row|
|
|
265
|
+
t.row do |r|
|
|
266
|
+
row.cells.each do |cell|
|
|
267
|
+
r.cell(text: cell.flat_text)
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def add_image_to_builder(image, builder)
|
|
275
|
+
if image.src && File.exist?(image.src)
|
|
276
|
+
builder.image(image.src, alt_text: image.alt || '')
|
|
277
|
+
else
|
|
278
|
+
builder.paragraph("[Image: #{image.alt || image.src}]")
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Build OOXML objects directly (low-level)
|
|
283
|
+
|
|
284
|
+
def build_heading(text, level:)
|
|
285
|
+
para = Uniword::Wordprocessingml::Paragraph.new
|
|
286
|
+
para.properties = Uniword::Wordprocessingml::ParagraphProperties.new
|
|
287
|
+
para.properties.style = Uniword::Properties::StyleReference.new(
|
|
288
|
+
value: "Heading#{level}"
|
|
289
|
+
)
|
|
290
|
+
run = Uniword::Wordprocessingml::Run.new
|
|
291
|
+
run.text = Uniword::Wordprocessingml::Text.new(content: text)
|
|
292
|
+
para.runs << run
|
|
293
|
+
para
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def build_ooxml_paragraph(block)
|
|
297
|
+
para = Uniword::Wordprocessingml::Paragraph.new
|
|
298
|
+
|
|
299
|
+
content = block.renderable_content
|
|
300
|
+
if content.is_a?(Array)
|
|
301
|
+
content.each do |child|
|
|
302
|
+
case child
|
|
303
|
+
when String
|
|
304
|
+
run = Uniword::Wordprocessingml::Run.new
|
|
305
|
+
run.text = Uniword::Wordprocessingml::Text.new(content: child)
|
|
306
|
+
para.runs << run
|
|
307
|
+
when Coradoc::CoreModel::InlineElement
|
|
308
|
+
para.runs << build_ooxml_run(child)
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
else
|
|
312
|
+
run = Uniword::Wordprocessingml::Run.new
|
|
313
|
+
run.text = Uniword::Wordprocessingml::Text.new(content: content.to_s)
|
|
314
|
+
para.runs << run
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
para.id = block.id if block.id
|
|
318
|
+
para
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def build_ooxml_run(inline)
|
|
322
|
+
run = Uniword::Wordprocessingml::Run.new
|
|
323
|
+
run.text = Uniword::Wordprocessingml::Text.new(content: inline.content.to_s)
|
|
324
|
+
|
|
325
|
+
props = Uniword::Wordprocessingml::RunProperties.new
|
|
326
|
+
|
|
327
|
+
case inline.format_type
|
|
328
|
+
when 'bold'
|
|
329
|
+
props.bold = Uniword::Properties::Bold.new
|
|
330
|
+
when 'italic'
|
|
331
|
+
props.italic = Uniword::Properties::Italic.new
|
|
332
|
+
when 'underline'
|
|
333
|
+
props.underline = Uniword::Properties::Underline.new
|
|
334
|
+
when 'strikethrough'
|
|
335
|
+
props.strike = Uniword::Properties::Strike.new
|
|
336
|
+
when 'subscript'
|
|
337
|
+
va = Uniword::Properties::VerticalAlign.new
|
|
338
|
+
va.value = 'subscript'
|
|
339
|
+
props.vertical_align = va
|
|
340
|
+
when 'superscript'
|
|
341
|
+
va = Uniword::Properties::VerticalAlign.new
|
|
342
|
+
va.value = 'superscript'
|
|
343
|
+
props.vertical_align = va
|
|
344
|
+
when 'monospace'
|
|
345
|
+
props.fonts = Uniword::Wordprocessingml::RFonts.new(ascii: 'Courier New')
|
|
346
|
+
when 'link'
|
|
347
|
+
# Links need to be at the paragraph level (w:hyperlink)
|
|
348
|
+
# Return the run; caller should wrap in Hyperlink
|
|
349
|
+
return run
|
|
350
|
+
when 'highlight'
|
|
351
|
+
hl = Uniword::Properties::Highlight.new
|
|
352
|
+
hl.value = 'yellow'
|
|
353
|
+
props.highlight = hl
|
|
354
|
+
when 'xref'
|
|
355
|
+
return run
|
|
356
|
+
when 'stem'
|
|
357
|
+
return run
|
|
358
|
+
else
|
|
359
|
+
return run
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
run.properties = props unless plain_properties?(props)
|
|
363
|
+
run
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def build_ooxml_list(list_block)
|
|
367
|
+
items = list_block.items || []
|
|
368
|
+
items.map do |item|
|
|
369
|
+
para = Uniword::Wordprocessingml::Paragraph.new
|
|
370
|
+
para.properties = Uniword::Wordprocessingml::ParagraphProperties.new
|
|
371
|
+
para.properties.num_id = 1
|
|
372
|
+
para.properties.ilvl = (list_block.marker_level || 1) - 1
|
|
373
|
+
|
|
374
|
+
content = item.flat_text
|
|
375
|
+
run = Uniword::Wordprocessingml::Run.new
|
|
376
|
+
run.text = Uniword::Wordprocessingml::Text.new(content: content)
|
|
377
|
+
para.runs << run
|
|
378
|
+
para
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def build_ooxml_table(table)
|
|
383
|
+
tbl = Uniword::Wordprocessingml::Table.new
|
|
384
|
+
|
|
385
|
+
table.rows.each do |row|
|
|
386
|
+
tr = Uniword::Wordprocessingml::TableRow.new
|
|
387
|
+
row.cells.each do |cell|
|
|
388
|
+
tc = Uniword::Wordprocessingml::TableCell.new
|
|
389
|
+
|
|
390
|
+
para = Uniword::Wordprocessingml::Paragraph.new
|
|
391
|
+
run = Uniword::Wordprocessingml::Run.new
|
|
392
|
+
run.text = Uniword::Wordprocessingml::Text.new(content: cell.flat_text)
|
|
393
|
+
para.runs << run
|
|
394
|
+
tc.paragraphs << para
|
|
395
|
+
|
|
396
|
+
tc.column_span = cell.colspan if cell.colspan && cell.colspan > 1
|
|
397
|
+
tc.row_span = cell.rowspan if cell.rowspan && cell.rowspan > 1
|
|
398
|
+
|
|
399
|
+
tr.cells << tc
|
|
400
|
+
end
|
|
401
|
+
tbl.rows << tr
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
tbl
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def build_ooxml_image(image)
|
|
408
|
+
if image.src && File.exist?(image.src)
|
|
409
|
+
run = Uniword::Builder::ImageBuilder.create_run(
|
|
410
|
+
nil, image.src, alt_text: image.alt
|
|
411
|
+
)
|
|
412
|
+
para = Uniword::Wordprocessingml::Paragraph.new
|
|
413
|
+
else
|
|
414
|
+
para = Uniword::Wordprocessingml::Paragraph.new
|
|
415
|
+
run = Uniword::Wordprocessingml::Run.new
|
|
416
|
+
run.text = Uniword::Wordprocessingml::Text.new(
|
|
417
|
+
content: "[Image: #{image.alt || image.src}]"
|
|
418
|
+
)
|
|
419
|
+
end
|
|
420
|
+
para.runs << run
|
|
421
|
+
para
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def build_page_break
|
|
425
|
+
para = Uniword::Wordprocessingml::Paragraph.new
|
|
426
|
+
run = Uniword::Wordprocessingml::Run.new
|
|
427
|
+
run.break = Uniword::Wordprocessingml::Break.new
|
|
428
|
+
run.break.type = 'page'
|
|
429
|
+
para.runs << run
|
|
430
|
+
para
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def build_inline_text(inline)
|
|
434
|
+
text = inline.content.to_s
|
|
435
|
+
|
|
436
|
+
case inline.format_type
|
|
437
|
+
when 'bold' then Uniword::Builder.text(text, bold: true)
|
|
438
|
+
when 'italic' then Uniword::Builder.text(text, italic: true)
|
|
439
|
+
when 'underline' then Uniword::Builder.text(text, underline: true)
|
|
440
|
+
when 'strikethrough' then Uniword::Builder.text(text, strike: true)
|
|
441
|
+
when 'highlight' then Uniword::Builder.text(text, highlight: 'yellow')
|
|
442
|
+
when 'monospace' then Uniword::Builder.text(text, font: 'Courier New')
|
|
443
|
+
when 'subscript', 'superscript'
|
|
444
|
+
run = Uniword::Wordprocessingml::Run.new(text: text)
|
|
445
|
+
props = Uniword::Wordprocessingml::RunProperties.new
|
|
446
|
+
va = Uniword::Properties::VerticalAlign.new
|
|
447
|
+
va.value = inline.format_type
|
|
448
|
+
props.vertical_align = va
|
|
449
|
+
run.properties = props
|
|
450
|
+
run
|
|
451
|
+
when 'link'
|
|
452
|
+
Uniword::Builder.hyperlink(inline.target.to_s, text)
|
|
453
|
+
else
|
|
454
|
+
text
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def plain_properties?(props)
|
|
459
|
+
props.bold.nil? &&
|
|
460
|
+
props.italic.nil? &&
|
|
461
|
+
props.underline.nil? &&
|
|
462
|
+
props.strike.nil? &&
|
|
463
|
+
props.vertical_align.nil? &&
|
|
464
|
+
props.highlight.nil?
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
# Build OOXML footnote reference (w:footnoteReference)
|
|
468
|
+
def build_ooxml_footnote_reference(footnote_ref)
|
|
469
|
+
ref = Uniword::Wordprocessingml::FootnoteReference.new
|
|
470
|
+
ref.id = footnote_ref.id.to_s
|
|
471
|
+
|
|
472
|
+
run = Uniword::Wordprocessingml::Run.new
|
|
473
|
+
run.footnote_reference = ref
|
|
474
|
+
run
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
# Build OOXML footnote content as a paragraph placeholder
|
|
478
|
+
def build_ooxml_footnote(footnote)
|
|
479
|
+
para = Uniword::Wordprocessingml::Paragraph.new
|
|
480
|
+
run = Uniword::Wordprocessingml::Run.new
|
|
481
|
+
run.text = Uniword::Wordprocessingml::Text.new(content: footnote.content.to_s)
|
|
482
|
+
para.runs << run
|
|
483
|
+
para
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
# Build OOXML definition list as a two-column table
|
|
487
|
+
def build_ooxml_definition_list(dl)
|
|
488
|
+
tbl = Uniword::Wordprocessingml::Table.new
|
|
489
|
+
|
|
490
|
+
Array(dl.items).each do |item|
|
|
491
|
+
row = Uniword::Wordprocessingml::TableRow.new
|
|
492
|
+
|
|
493
|
+
term_cell = Uniword::Wordprocessingml::TableCell.new
|
|
494
|
+
term_para = Uniword::Wordprocessingml::Paragraph.new
|
|
495
|
+
term_run = Uniword::Wordprocessingml::Run.new
|
|
496
|
+
term_run.text = Uniword::Wordprocessingml::Text.new(content: item.term.to_s)
|
|
497
|
+
term_props = Uniword::Wordprocessingml::RunProperties.new
|
|
498
|
+
term_props.bold = Uniword::Properties::Bold.new
|
|
499
|
+
term_run.properties = term_props
|
|
500
|
+
term_para.runs << term_run
|
|
501
|
+
term_cell.paragraphs << term_para
|
|
502
|
+
|
|
503
|
+
def_cell = Uniword::Wordprocessingml::TableCell.new
|
|
504
|
+
def_text = Array(item.definitions).map(&:to_s).join('; ')
|
|
505
|
+
def_para = Uniword::Wordprocessingml::Paragraph.new
|
|
506
|
+
def_run = Uniword::Wordprocessingml::Run.new
|
|
507
|
+
def_run.text = Uniword::Wordprocessingml::Text.new(content: def_text)
|
|
508
|
+
def_para.runs << def_run
|
|
509
|
+
def_cell.paragraphs << def_para
|
|
510
|
+
|
|
511
|
+
row.cells << term_cell
|
|
512
|
+
row.cells << def_cell
|
|
513
|
+
tbl.rows << row
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
tbl
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
# Build OOXML TOC as a text placeholder
|
|
520
|
+
def build_ooxml_toc(_toc)
|
|
521
|
+
para = Uniword::Wordprocessingml::Paragraph.new
|
|
522
|
+
run = Uniword::Wordprocessingml::Run.new
|
|
523
|
+
run.text = Uniword::Wordprocessingml::Text.new(content: '[Table of Contents]')
|
|
524
|
+
para.runs << run
|
|
525
|
+
para
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
# Build OOXML term as a bold paragraph
|
|
529
|
+
def build_ooxml_term(term)
|
|
530
|
+
para = Uniword::Wordprocessingml::Paragraph.new
|
|
531
|
+
run = Uniword::Wordprocessingml::Run.new
|
|
532
|
+
run.text = Uniword::Wordprocessingml::Text.new(content: term.text.to_s)
|
|
533
|
+
run_props = Uniword::Wordprocessingml::RunProperties.new
|
|
534
|
+
run_props.bold = Uniword::Properties::Bold.new
|
|
535
|
+
run.properties = run_props
|
|
536
|
+
para.runs << run
|
|
537
|
+
para
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
def build_ooxml_abbreviation(abbr)
|
|
541
|
+
para = Uniword::Wordprocessingml::Paragraph.new
|
|
542
|
+
run = Uniword::Wordprocessingml::Run.new
|
|
543
|
+
text = abbr.term.to_s
|
|
544
|
+
text += " (#{abbr.definition})" if abbr.definition
|
|
545
|
+
run.text = Uniword::Wordprocessingml::Text.new(content: text)
|
|
546
|
+
para.runs << run
|
|
547
|
+
para
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def build_ooxml_bibliography(bib)
|
|
551
|
+
entries = Array(bib.entries).map { |e| build_ooxml_bibliography_entry(e) }
|
|
552
|
+
|
|
553
|
+
result = []
|
|
554
|
+
result << build_heading(bib.title, level: bib.level || 2) if bib.title
|
|
555
|
+
result.concat(entries)
|
|
556
|
+
result
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
def build_ooxml_bibliography_entry(entry)
|
|
560
|
+
para = Uniword::Wordprocessingml::Paragraph.new
|
|
561
|
+
run = Uniword::Wordprocessingml::Run.new
|
|
562
|
+
run.text = Uniword::Wordprocessingml::Text.new(content: entry.display_text)
|
|
563
|
+
para.runs << run
|
|
564
|
+
para
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
def build_ooxml_toc_entry(entry)
|
|
568
|
+
para = Uniword::Wordprocessingml::Paragraph.new
|
|
569
|
+
run = Uniword::Wordprocessingml::Run.new
|
|
570
|
+
run.text = Uniword::Wordprocessingml::Text.new(content: entry.title.to_s)
|
|
571
|
+
para.runs << run
|
|
572
|
+
para
|
|
573
|
+
end
|
|
574
|
+
end
|
|
575
|
+
end
|
|
576
|
+
end
|
|
577
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Coradoc
|
|
4
|
+
module Docx
|
|
5
|
+
module Transform
|
|
6
|
+
# Resolves OOXML numbering definitions to list style information.
|
|
7
|
+
#
|
|
8
|
+
# OOXML stores list formatting in numbering definitions (abstractNum).
|
|
9
|
+
# Each numPr reference (numId + ilvl) points to a specific numbering
|
|
10
|
+
# format (bullet, decimal, lowerLetter, etc.).
|
|
11
|
+
#
|
|
12
|
+
# The resolver walks Uniword's NumberingConfiguration to build a map
|
|
13
|
+
# of numId → { ordered, marker_type, format }.
|
|
14
|
+
class NumberingResolver
|
|
15
|
+
ORDERED_FORMATS = %w[decimal lowerLetter upperLetter lowerRoman upperRoman
|
|
16
|
+
russianLower russianUpper hebrew1 hebrew2 thaiLetters
|
|
17
|
+
japaneseDigitalTenji japaneseKorean chineseCounting].freeze
|
|
18
|
+
|
|
19
|
+
# @param numbering_configuration [Object, nil] Uniword numbering config
|
|
20
|
+
def initialize(numbering_configuration)
|
|
21
|
+
@config = numbering_configuration
|
|
22
|
+
@num_map = build_num_map(numbering_configuration)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Determine if a numId represents an ordered list
|
|
26
|
+
#
|
|
27
|
+
# @param num_id [Integer, String, nil] numbering definition ID
|
|
28
|
+
# @return [Boolean] true if ordered, false if unordered
|
|
29
|
+
def ordered?(num_id)
|
|
30
|
+
return false unless num_id
|
|
31
|
+
|
|
32
|
+
info = @num_map[num_id.to_i]
|
|
33
|
+
return false unless info
|
|
34
|
+
|
|
35
|
+
info[:ordered]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Get the list style for a numId
|
|
39
|
+
#
|
|
40
|
+
# @param num_id [Integer, String, nil]
|
|
41
|
+
# @return [Symbol] :ordered, :unordered
|
|
42
|
+
def list_style(num_id)
|
|
43
|
+
ordered?(num_id) ? :ordered : :unordered
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Get marker type string for a numId
|
|
47
|
+
#
|
|
48
|
+
# @param num_id [Integer, String, nil]
|
|
49
|
+
# @return [String] "numbered", "asterisk", "lower_alpha", etc.
|
|
50
|
+
def marker_type(num_id)
|
|
51
|
+
return 'asterisk' unless num_id
|
|
52
|
+
|
|
53
|
+
info = @num_map[num_id.to_i]
|
|
54
|
+
return 'asterisk' unless info
|
|
55
|
+
|
|
56
|
+
info[:marker_type]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def build_num_map(config)
|
|
62
|
+
return {} unless config
|
|
63
|
+
|
|
64
|
+
instances = config.instances
|
|
65
|
+
return {} if instances.nil? || instances.empty?
|
|
66
|
+
|
|
67
|
+
map = {}
|
|
68
|
+
instances.each do |inst|
|
|
69
|
+
num_id = inst.num_id
|
|
70
|
+
abstract_num_id = extract_abstract_num_id(inst)
|
|
71
|
+
next unless num_id && abstract_num_id
|
|
72
|
+
|
|
73
|
+
definition = find_definition(config, abstract_num_id)
|
|
74
|
+
next unless definition
|
|
75
|
+
|
|
76
|
+
levels = definition.levels
|
|
77
|
+
level = levels&.first
|
|
78
|
+
next unless level
|
|
79
|
+
|
|
80
|
+
fmt = extract_num_fmt(level)
|
|
81
|
+
ordered = ORDERED_FORMATS.include?(fmt)
|
|
82
|
+
map[num_id] = {
|
|
83
|
+
ordered: ordered,
|
|
84
|
+
marker_type: marker_type_for(fmt),
|
|
85
|
+
format: fmt
|
|
86
|
+
}
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
map
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def extract_abstract_num_id(instance)
|
|
93
|
+
aid = instance.abstract_num_id
|
|
94
|
+
return nil unless aid
|
|
95
|
+
|
|
96
|
+
aid.is_a?(Uniword::Wordprocessingml::AbstractNumId) ? aid.val : aid
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def find_definition(config, abstract_num_id)
|
|
100
|
+
defs = config.definitions
|
|
101
|
+
return [] unless defs
|
|
102
|
+
|
|
103
|
+
defs.find { |d| d.abstract_num_id == abstract_num_id }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def extract_num_fmt(level)
|
|
107
|
+
nf = level.numFmt
|
|
108
|
+
return 'bullet' unless nf
|
|
109
|
+
|
|
110
|
+
nf.is_a?(Uniword::Wordprocessingml::NumFmt) ? nf.val.to_s : nf.to_s
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def marker_type_for(fmt)
|
|
114
|
+
case fmt
|
|
115
|
+
when 'decimal' then 'numbered'
|
|
116
|
+
when 'lowerLetter' then 'lower_alpha'
|
|
117
|
+
when 'upperLetter' then 'upper_alpha'
|
|
118
|
+
when 'lowerRoman' then 'lower_roman'
|
|
119
|
+
when 'upperRoman' then 'upper_roman'
|
|
120
|
+
when 'bullet' then 'asterisk'
|
|
121
|
+
else 'numbered'
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|