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.
Files changed (29) hide show
  1. checksums.yaml +7 -0
  2. data/README.adoc +164 -0
  3. data/lib/coradoc/docx/transform/context.rb +72 -0
  4. data/lib/coradoc/docx/transform/from_core_model.rb +577 -0
  5. data/lib/coradoc/docx/transform/numbering_resolver.rb +127 -0
  6. data/lib/coradoc/docx/transform/ordered_content.rb +95 -0
  7. data/lib/coradoc/docx/transform/rule.rb +57 -0
  8. data/lib/coradoc/docx/transform/rule_registry.rb +60 -0
  9. data/lib/coradoc/docx/transform/rules/bookmark_rule.rb +34 -0
  10. data/lib/coradoc/docx/transform/rules/break_rule.rb +30 -0
  11. data/lib/coradoc/docx/transform/rules/footnote_rule.rb +27 -0
  12. data/lib/coradoc/docx/transform/rules/heading_rule.rb +53 -0
  13. data/lib/coradoc/docx/transform/rules/hyperlink_rule.rb +58 -0
  14. data/lib/coradoc/docx/transform/rules/image_rule.rb +125 -0
  15. data/lib/coradoc/docx/transform/rules/list_item_rule.rb +47 -0
  16. data/lib/coradoc/docx/transform/rules/math_rule.rb +82 -0
  17. data/lib/coradoc/docx/transform/rules/paragraph_rule.rb +65 -0
  18. data/lib/coradoc/docx/transform/rules/proof_error_rule.rb +25 -0
  19. data/lib/coradoc/docx/transform/rules/run_rule.rb +189 -0
  20. data/lib/coradoc/docx/transform/rules/simple_field_rule.rb +87 -0
  21. data/lib/coradoc/docx/transform/rules/structured_document_tag_rule.rb +36 -0
  22. data/lib/coradoc/docx/transform/rules/table_rule.rb +85 -0
  23. data/lib/coradoc/docx/transform/rules/text_rule.rb +25 -0
  24. data/lib/coradoc/docx/transform/style_resolver.rb +249 -0
  25. data/lib/coradoc/docx/transform/to_core_model.rb +340 -0
  26. data/lib/coradoc/docx/transform.rb +38 -0
  27. data/lib/coradoc/docx/version.rb +7 -0
  28. data/lib/coradoc/docx.rb +99 -0
  29. metadata +155 -0
@@ -0,0 +1,249 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Docx
5
+ module Transform
6
+ # Resolves paragraph and run styles to semantic roles.
7
+ #
8
+ # OOXML paragraphs don't have explicit element types. Instead, their
9
+ # meaning is determined by style references (e.g., "Heading1" → section)
10
+ # or by formatting properties (e.g., numPr → list item).
11
+ #
12
+ # StyleResolver centralizes this detection so HeadingRule, ListItemRule,
13
+ # and ParagraphRule don't duplicate the logic.
14
+ #
15
+ # The style map is built from the Uniword StylesConfiguration by walking
16
+ # all style definitions and their basedOn chains.
17
+ class StyleResolver
18
+ HEADING_PATTERN = /^(heading|heading|h)\s*(\d+)$/i
19
+ QUOTE_PATTERN = /\bquote\b/i
20
+ CODE_PATTERN = /\b(code|source|listing)\b/i
21
+ LITERAL_PATTERN = /\bliteral\b/i
22
+ EXAMPLE_PATTERN = /\bexample\b/i
23
+
24
+ # @param styles_configuration [Object, nil] Uniword styles configuration
25
+ def initialize(styles_configuration)
26
+ @config = styles_configuration
27
+ @style_map = build_style_map(styles_configuration)
28
+ end
29
+
30
+ # Determine the semantic role of a paragraph
31
+ #
32
+ # @param paragraph [Uniword::Wordprocessingml::Paragraph]
33
+ # @return [Symbol] :heading, :list_item, :quote, :source, :literal,
34
+ # :example, or :paragraph
35
+ def semantic_role(paragraph)
36
+ return :heading if heading?(paragraph)
37
+ return :list_item if list_item?(paragraph)
38
+
39
+ style_role = role_from_style(paragraph)
40
+ return style_role if style_role
41
+
42
+ :paragraph
43
+ end
44
+
45
+ # Check if paragraph is a heading
46
+ # @param paragraph [Uniword::Wordprocessingml::Paragraph]
47
+ # @return [Boolean]
48
+ def heading?(paragraph)
49
+ return false unless paragraph.properties
50
+
51
+ style_name = resolve_style_name(paragraph)
52
+ return true if style_name && HEADING_PATTERN.match?(style_name)
53
+
54
+ ol = paragraph.properties.outline_level
55
+ if ol
56
+ ol_level = ol.is_a?(Uniword::Wordprocessingml::OutlineLevel) ? ol.value.to_i : ol.to_i
57
+ return true if ol_level.positive?
58
+ end
59
+
60
+ style = find_style_for_paragraph(paragraph)
61
+ if style&.outline_level
62
+ ol_val = style.outline_level
63
+ ol_val = ol_val.is_a?(Uniword::Wordprocessingml::OutlineLevel) ? ol_val.value.to_i : ol_val.to_i
64
+ return true if ol_val.positive?
65
+ end
66
+
67
+ false
68
+ end
69
+
70
+ # Get heading level (1-6) or nil
71
+ # @param paragraph [Uniword::Wordprocessingml::Paragraph]
72
+ # @return [Integer, nil]
73
+ def heading_level(paragraph)
74
+ style_name = resolve_style_name(paragraph)
75
+ if style_name
76
+ match = HEADING_PATTERN.match(style_name)
77
+ return match[2].to_i if match
78
+ end
79
+
80
+ # Check outline_level on paragraph properties
81
+ ol = paragraph.properties&.outline_level
82
+ if ol
83
+ level = ol.is_a?(Uniword::Wordprocessingml::OutlineLevel) ? ol.value.to_i : ol.to_i
84
+ return level if level.positive?
85
+ end
86
+
87
+ nil
88
+ end
89
+
90
+ # Check if paragraph is a list item
91
+ # @param paragraph [Uniword::Wordprocessingml::Paragraph]
92
+ # @return [Boolean]
93
+ def list_item?(paragraph)
94
+ return false unless paragraph.properties
95
+
96
+ num_id = paragraph.properties.num_id
97
+ num_id.to_i.positive?
98
+ end
99
+
100
+ # Check if paragraph has a specific role based on style name
101
+ # @param paragraph [Uniword::Wordprocessingml::Paragraph]
102
+ # @return [Symbol, nil]
103
+ def role_from_style(paragraph)
104
+ style_name = resolve_style_name(paragraph)
105
+ return nil unless style_name
106
+
107
+ case style_name
108
+ when QUOTE_PATTERN then :quote
109
+ when CODE_PATTERN then :source
110
+ when LITERAL_PATTERN then :literal
111
+ when EXAMPLE_PATTERN then :example
112
+ end
113
+ end
114
+
115
+ # Detect semantic role of a run based on its rStyle
116
+ # @param run [Uniword::Wordprocessingml::Run]
117
+ # @return [Symbol, nil]
118
+ def run_semantic_role(run)
119
+ return nil unless run.properties
120
+ return nil unless run.properties.style
121
+
122
+ style_name = resolve_run_style_name(run)
123
+ return nil unless style_name
124
+
125
+ case style_name
126
+ when /\b(code|verbatim|teletype|keyboard)\b/i then :monospace
127
+ when /\bstrong\b/i then :bold
128
+ when /\b(emphasis|em)\b/i then :italic
129
+ when /\bcitation\b/i then :italic
130
+ end
131
+ end
132
+
133
+ private
134
+
135
+ def resolve_style_name(paragraph)
136
+ style_ref = paragraph.properties&.style
137
+ return nil unless style_ref
138
+
139
+ value = style_ref.is_a?(Uniword::Wordprocessingml::PStyle) ? style_ref.val : style_ref.to_s
140
+ return nil unless value
141
+
142
+ mapped = @style_map[value]
143
+ return mapped if mapped
144
+
145
+ value
146
+ end
147
+
148
+ def resolve_run_style_name(run)
149
+ style_ref = run.properties.style
150
+ return nil unless style_ref
151
+
152
+ value = style_ref.is_a?(Uniword::Wordprocessingml::PStyle) ? style_ref.val : style_ref.to_s
153
+ return nil unless value
154
+
155
+ mapped = @style_map[value]
156
+ mapped || value
157
+ end
158
+
159
+ def find_style_for_paragraph(paragraph)
160
+ return nil unless @config
161
+
162
+ style_id = style_id_from_paragraph(paragraph)
163
+ return nil unless style_id
164
+
165
+ return unless @config.is_a?(Uniword::Wordprocessingml::StylesConfiguration)
166
+
167
+ @config.style_by_id(style_id)
168
+ end
169
+
170
+ def style_id_from_paragraph(paragraph)
171
+ style_ref = paragraph.properties&.style
172
+ return nil unless style_ref
173
+
174
+ style_ref.is_a?(Uniword::Wordprocessingml::PStyle) ? style_ref.val : style_ref.to_s
175
+ end
176
+
177
+ def build_style_map(config)
178
+ return {} unless config
179
+ return {} unless config.is_a?(Uniword::Wordprocessingml::StylesConfiguration)
180
+
181
+ map = {}
182
+ config.styles.each do |style|
183
+ id = style.styleId
184
+ name = extract_style_name(style)
185
+ next unless id && name
186
+
187
+ map[id] = name
188
+
189
+ if heading_by_based_on?(config, style)
190
+ level = heading_level_from_chain(config, style)
191
+ map[id] = "Heading#{level}"
192
+ end
193
+ end
194
+
195
+ map
196
+ end
197
+
198
+ def extract_style_name(style)
199
+ sn = style.style_name
200
+ return sn if sn
201
+
202
+ name = style.name
203
+ return nil unless name
204
+
205
+ name.is_a?(Uniword::Wordprocessingml::StyleName) ? name.val.to_s : name.to_s
206
+ end
207
+
208
+ def heading_by_based_on?(config, style)
209
+ based_on = style.based_on
210
+ return false unless based_on
211
+
212
+ visited = Set.new
213
+ current = style
214
+ while current && !visited.include?(current.styleId)
215
+ visited << current.styleId
216
+ parent_id = current.based_on
217
+ return true if parent_id && HEADING_PATTERN.match?(parent_id)
218
+
219
+ break unless parent_id
220
+
221
+ current = config.styles.find { |s| s.styleId == parent_id }
222
+ end
223
+
224
+ false
225
+ end
226
+
227
+ def heading_level_from_chain(config, style)
228
+ visited = Set.new
229
+ current = style
230
+ while current && !visited.include?(current.styleId)
231
+ visited << current.styleId
232
+ name = extract_style_name(current)
233
+ if name
234
+ match = HEADING_PATTERN.match(name)
235
+ return match[2].to_i if match
236
+ end
237
+
238
+ parent_id = current.based_on
239
+ break unless parent_id
240
+
241
+ current = config.styles.find { |s| s.styleId == parent_id }
242
+ end
243
+
244
+ 1
245
+ end
246
+ end
247
+ end
248
+ end
249
+ end
@@ -0,0 +1,340 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Docx
5
+ module Transform
6
+ # Orchestrator for OOXML → CoreModel transformation.
7
+ #
8
+ # Walks a Uniword::Wordprocessingml::DocumentRoot tree and dispatches
9
+ # to registered transform rules. Handles:
10
+ #
11
+ # - Style-based heading detection (via StyleResolver)
12
+ # - List grouping (consecutive numPr paragraphs → single ListBlock)
13
+ # - Footnote content collection
14
+ # - Image reference tracking
15
+ # - Bookmark ID propagation
16
+ #
17
+ # Dispatch strategy:
18
+ # - HeadingRule and ListItemRule are dispatched directly by the
19
+ # orchestrator (they need context for style resolution).
20
+ # - All other element types are dispatched via RuleRegistry.
21
+ #
22
+ # @example Transform a DOCX document
23
+ # doc = Uniword::DocumentFactory.from_file("input.docx")
24
+ # core = ToCoreModel.transform(doc)
25
+ # # => Coradoc::CoreModel::StructuralElement
26
+ class ToCoreModel
27
+ class << self
28
+ def transform(document)
29
+ new.transform(document)
30
+ end
31
+ end
32
+
33
+ def transform(document)
34
+ registry = build_registry
35
+
36
+ context = Context.new(
37
+ styles_configuration: document.styles_configuration,
38
+ numbering_configuration: document.numbering_configuration,
39
+ footnotes: collect_footnotes(document),
40
+ registry: registry
41
+ )
42
+
43
+ @heading_rule = Rules::HeadingRule.new
44
+ @list_item_rule = Rules::ListItemRule.new
45
+
46
+ body = document.body
47
+ doc_title = extract_document_title(document, context)
48
+ children = transform_elements(body, context)
49
+
50
+ # If the first child is an H1 matching the doc title, skip the
51
+ # duplicate — the document title already captures it
52
+ if doc_title && children.first.is_a?(Coradoc::CoreModel::StructuralElement) &&
53
+ children.first.section? &&
54
+ children.first.title == doc_title &&
55
+ children.first.level == 1
56
+ children.shift
57
+ end
58
+
59
+ doc = Coradoc::CoreModel::StructuralElement.new(
60
+ element_type: 'document',
61
+ title: doc_title,
62
+ children: children
63
+ )
64
+
65
+ # Extract semantic content from headers/footers
66
+ extract_header_footer_metadata(document, doc)
67
+
68
+ doc
69
+ end
70
+
71
+ private
72
+
73
+ # Walk body elements with list grouping look-ahead
74
+ def transform_elements(body, context)
75
+ return [] unless body
76
+
77
+ elements = body_ordered_elements(body)
78
+
79
+ result = []
80
+ i = 0
81
+
82
+ while i < elements.length
83
+ element = elements[i]
84
+
85
+ transformed = dispatch_element(element, i, elements, context)
86
+
87
+ case transformed
88
+ when Array
89
+ consumed = transformed.length
90
+ result.concat(transformed.compact)
91
+ i += consumed
92
+ when nil
93
+ i += 1
94
+ else
95
+ result << transformed
96
+ i += 1
97
+ end
98
+ end
99
+
100
+ result
101
+ end
102
+
103
+ # Dispatch a single body element, handling paragraphs specially
104
+ def dispatch_element(element, index, elements, context)
105
+ # Paragraphs need style-based dispatch (heading, list, or plain)
106
+ return dispatch_paragraph(element, index, elements, context) if paragraph?(element)
107
+
108
+ # Tables go through registry directly
109
+ context.transform(element)
110
+ end
111
+
112
+ def dispatch_paragraph(paragraph, index, elements, context)
113
+ resolver = context.style_resolver
114
+
115
+ # Check for section break in paragraph properties
116
+ if section_break?(paragraph)
117
+ # Section break without heading → thematic break
118
+ return section_break_element(paragraph, context)
119
+ end
120
+
121
+ # Heading
122
+ return @heading_rule.apply(paragraph, context) if resolver.heading?(paragraph)
123
+
124
+ # List item — group consecutive items with same numId
125
+ return group_list(elements, index, context) if resolver.list_item?(paragraph)
126
+
127
+ # Regular paragraph (via registry)
128
+ context.transform(paragraph)
129
+ end
130
+
131
+ def section_break?(paragraph)
132
+ return false unless paragraph.is_a?(Uniword::Wordprocessingml::Paragraph)
133
+ return false unless paragraph.properties
134
+
135
+ sect_pr = paragraph.properties.section_properties
136
+ return false unless sect_pr
137
+
138
+ sect_pr.type ? true : false
139
+ end
140
+
141
+ def section_break_element(paragraph, context)
142
+ # First, transform the paragraph content if it has text
143
+ content = paragraph.runs&.map { |r| r.text&.content.to_s }&.join
144
+ if content && !content.strip.empty?
145
+ # Has content — transform normally (content comes before the break)
146
+ context.transform(paragraph)
147
+ else
148
+ # Standalone section break → thematic break
149
+ Coradoc::CoreModel::Block.new(
150
+ element_type: 'thematic_break'
151
+ )
152
+ end
153
+ end
154
+
155
+ # Collect consecutive list items with the same numId into a ListBlock
156
+ def group_list(elements, start_index, context)
157
+ first = elements[start_index]
158
+ num_id = first.properties&.num_id.to_i
159
+ items = []
160
+ consumed = 0
161
+
162
+ idx = start_index
163
+ while idx < elements.length
164
+ para = elements[idx]
165
+ break unless paragraph?(para)
166
+ break unless context.style_resolver.list_item?(para)
167
+ break unless para.properties&.num_id.to_i == num_id
168
+
169
+ items << @list_item_rule.apply(para, context)
170
+ consumed += 1
171
+ idx += 1
172
+ end
173
+
174
+ list_block = Coradoc::CoreModel::ListBlock.new(
175
+ marker_type: context.numbering_resolver.marker_type(num_id),
176
+ items: items
177
+ )
178
+
179
+ # Return array so caller knows how many elements were consumed
180
+ consumed > 1 ? [list_block] + Array.new(consumed - 1, nil) : list_block
181
+ end
182
+
183
+ def body_ordered_elements(body)
184
+ order = body.is_a?(Uniword::Wordprocessingml::Body) ? body.element_order : nil
185
+ return body.elements if order.nil? || order.empty?
186
+
187
+ p_idx = tbl_idx = sdt_idx = 0
188
+ order.filter_map do |entry|
189
+ name = entry.is_a?(String) ? entry : entry.name
190
+ case name
191
+ when 'p'
192
+ para = body.paragraphs[p_idx]
193
+ p_idx += 1
194
+ para
195
+ when 'tbl'
196
+ tbl = body.tables[tbl_idx]
197
+ tbl_idx += 1
198
+ tbl
199
+ when 'sdt'
200
+ sdt = body.structured_document_tags&.[](sdt_idx)
201
+ sdt_idx += 1
202
+ sdt
203
+ end
204
+ end
205
+ end
206
+
207
+ def paragraph?(element)
208
+ defined?(Uniword::Wordprocessingml::Paragraph) &&
209
+ element.is_a?(Uniword::Wordprocessingml::Paragraph)
210
+ end
211
+
212
+ def collect_footnotes(document)
213
+ footnotes = {}
214
+
215
+ doc_footnotes = document.footnotes
216
+ if doc_footnotes.is_a?(Hash)
217
+ doc_footnotes.each do |id, fn|
218
+ paragraphs = fn.is_a?(Uniword::Wordprocessingml::Footnote) ? fn.paragraphs : fn[:content]
219
+ footnotes[id.to_s] = Array(paragraphs)
220
+ end
221
+ end
222
+
223
+ if defined?(Uniword::Wordprocessingml::Footnotes) &&
224
+ document.footnotes.is_a?(Uniword::Wordprocessingml::Footnotes)
225
+ document.footnotes.footnotes.each do |fn|
226
+ id = fn.id&.to_s
227
+ next unless id
228
+
229
+ footnotes[id] = Array(fn.paragraphs || [])
230
+ end
231
+ end
232
+
233
+ footnotes
234
+ end
235
+
236
+ def extract_document_title(document, context)
237
+ body = document.body
238
+ return nil unless body
239
+
240
+ paragraphs = body.paragraphs || []
241
+ paragraphs.each do |para|
242
+ next unless context.style_resolver.heading?(para)
243
+ next unless context.style_resolver.heading_level(para) == 1
244
+
245
+ runs = para.runs || []
246
+ return runs.map { |r| r.text&.content.to_s }.join
247
+ end
248
+
249
+ nil
250
+ end
251
+
252
+ def build_registry
253
+ registry = RuleRegistry.new
254
+
255
+ # Only register rules that don't need context for dispatch
256
+ registry.register(Rules::ParagraphRule.new)
257
+ registry.register(Rules::RunRule.new)
258
+ registry.register(Rules::TextRule.new)
259
+ registry.register(Rules::BreakRule.new)
260
+ registry.register(Rules::HyperlinkRule.new)
261
+ registry.register(Rules::ImageRule.new)
262
+ registry.register(Rules::FootnoteRule.new)
263
+ registry.register(Rules::BookmarkRule.new)
264
+ registry.register(Rules::TableRule.new)
265
+ registry.register(Rules::MathRule.new)
266
+ registry.register(Rules::StructuredDocumentTagRule.new)
267
+ registry.register(Rules::SimpleFieldRule.new)
268
+ registry.register(Rules::ProofErrorRule.new)
269
+
270
+ registry
271
+ end
272
+
273
+ # Extract semantic text from headers and footers.
274
+ # Discards purely layout text ("Page X of Y", page numbers, dates).
275
+ # Preserves meaningful text (title, version, confidentiality notices).
276
+ def extract_header_footer_metadata(document, core_doc)
277
+ extract_from_parts(document, :headers, 'header', core_doc)
278
+ extract_from_parts(document, :footers, 'footer', core_doc)
279
+ end
280
+
281
+ def extract_from_parts(document, method, prefix, core_doc)
282
+ parts = case method
283
+ when :headers then document.headers
284
+ when :footers then document.footers
285
+ end
286
+ return unless parts
287
+
288
+ Array(parts).each_with_index do |part, idx|
289
+ text = extract_part_text(part)
290
+ next if text.nil? || text.strip.empty?
291
+ next if layout_only_text?(text)
292
+
293
+ part_type = if part.is_a?(Uniword::Wordprocessingml::Header) ||
294
+ part.is_a?(Uniword::Wordprocessingml::Footer)
295
+ part.type
296
+ else
297
+ idx
298
+ end
299
+ core_doc.set_metadata("docx.#{prefix}.#{part_type}", text.strip)
300
+ end
301
+ end
302
+
303
+ def extract_part_text(part)
304
+ paragraphs = part.paragraphs || []
305
+ return nil unless paragraphs
306
+
307
+ paragraphs.map do |para|
308
+ extract_paragraph_text_content(para)
309
+ end.compact.join(' ').strip
310
+ end
311
+
312
+ def extract_paragraph_text_content(para)
313
+ runs = para.runs || []
314
+ return nil unless runs
315
+
316
+ runs.map { |r| r.text&.content.to_s }.join
317
+ end
318
+
319
+ # Check if header/footer text is purely layout content
320
+ # (page numbers, "Page X of Y", dates, etc.)
321
+ def layout_only_text?(text)
322
+ stripped = text.strip
323
+ return true if stripped.empty?
324
+
325
+ # Pure numbers (page numbers)
326
+ return true if stripped.match?(/\A\d+\z/)
327
+
328
+ # Common page number patterns
329
+ return true if stripped.match?(/\APage\s+\d+(\s+of\s+\d+)?\z/i)
330
+
331
+ # Pure date patterns
332
+ return true if stripped.match?(%r{\A\d{1,2}[/-]\d{1,2}[/-]\d{2,4}\z})
333
+ return true if stripped.match?(%r{\A\d{4}[/-]\d{1,2}[/-]\d{1,2}\z})
334
+
335
+ false
336
+ end
337
+ end
338
+ end
339
+ end
340
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Docx
5
+ module Transform
6
+ autoload :Rule, 'coradoc/docx/transform/rule'
7
+ autoload :RuleRegistry, 'coradoc/docx/transform/rule_registry'
8
+ autoload :Context, 'coradoc/docx/transform/context'
9
+ autoload :ToCoreModel, 'coradoc/docx/transform/to_core_model'
10
+ autoload :FromCoreModel, 'coradoc/docx/transform/from_core_model'
11
+ autoload :StyleResolver, 'coradoc/docx/transform/style_resolver'
12
+ autoload :NumberingResolver, 'coradoc/docx/transform/numbering_resolver'
13
+ autoload :OrderedContent, 'coradoc/docx/transform/ordered_content'
14
+
15
+ # Element transform rules
16
+ module Rules
17
+ autoload :TextRule, 'coradoc/docx/transform/rules/text_rule'
18
+ autoload :BreakRule, 'coradoc/docx/transform/rules/break_rule'
19
+ autoload :RunRule, 'coradoc/docx/transform/rules/run_rule'
20
+ autoload :HyperlinkRule, 'coradoc/docx/transform/rules/hyperlink_rule'
21
+ autoload :ImageRule, 'coradoc/docx/transform/rules/image_rule'
22
+ autoload :FootnoteRule, 'coradoc/docx/transform/rules/footnote_rule'
23
+ autoload :HeadingRule, 'coradoc/docx/transform/rules/heading_rule'
24
+ autoload :ListItemRule, 'coradoc/docx/transform/rules/list_item_rule'
25
+ autoload :ParagraphRule, 'coradoc/docx/transform/rules/paragraph_rule'
26
+ autoload :TableRule, 'coradoc/docx/transform/rules/table_rule'
27
+ autoload :MathRule, 'coradoc/docx/transform/rules/math_rule'
28
+ autoload :BookmarkRule, 'coradoc/docx/transform/rules/bookmark_rule'
29
+ autoload :StructuredDocumentTagRule,
30
+ 'coradoc/docx/transform/rules/structured_document_tag_rule'
31
+ autoload :SimpleFieldRule,
32
+ 'coradoc/docx/transform/rules/simple_field_rule'
33
+ autoload :ProofErrorRule,
34
+ 'coradoc/docx/transform/rules/proof_error_rule'
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Docx
5
+ VERSION = '0.1.0'
6
+ end
7
+ end