coradoc-docx 0.1.3 → 0.1.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b2d3e5bd2f76d67000623cf3a2cb3889d5b49105a7420494f6f09622bac8a61b
4
- data.tar.gz: 2c611f389f427ac79c73b5ce6e727e602b1d392f97f6090af837d2c6a5d05d9a
3
+ metadata.gz: 18f25e708f14692cd5a7b48a8636e34df3eee0b5ac3fa8a7f0904a5f56acbeaa
4
+ data.tar.gz: b15177104fe5b8b50635fb6856fa059856c0fb4abfea48efaf15a7bb0cad0a20
5
5
  SHA512:
6
- metadata.gz: faff02134845b773db02e2e8e8f4a0f99e58548b013db9ad99212fe349f1e8be7088fc153465bb32b54c584c05030d24c8cc5a07dbf0e8365ab2cd3368ed5bc6
7
- data.tar.gz: 308659e8aaee319757fb35fd19abf69296aebe00e8d31d4405b9d262454cd1e8e020aed702a6ab043664dd13e64670bf2758bcfda41ef7c80115d6c02d4f513b
6
+ metadata.gz: 9889857c0c723281bc2c45903c31898b8881d49ff431a7aef699882a74dd1243009f9d1cbd548bc9970f09131b34316743a90d00ee99e6ce9f7509dec7f3276a
7
+ data.tar.gz: b1a47163cdb94d6ecc07d0d0fb4ca1a36eb9539867620f5b7ad861aa7d789fedaaa6018d5731855938ba164b47ae35874d6fb29d028f67e6567221619a2cfdbd
@@ -234,6 +234,14 @@ module Coradoc
234
234
  builder.table do |t|
235
235
  table.rows.each do |row|
236
236
  t.row do |r|
237
+ # TableRowBuilder#header_row is broken upstream (calls a
238
+ # nonexistent table_header= setter), so set the persisted
239
+ # w:tblHeader property on the row model directly
240
+ if row.header
241
+ r.model.properties = Uniword::Wordprocessingml::TableRowProperties.new(
242
+ tbl_header: Uniword::Wordprocessingml::ValInt.new(value: 1)
243
+ )
244
+ end
237
245
  row.cells.each do |cell|
238
246
  r.cell(text: cell.flat_text)
239
247
  end
@@ -295,7 +303,9 @@ module Coradoc
295
303
 
296
304
  props = Uniword::Wordprocessingml::RunProperties.new
297
305
 
298
- case inline.format_type
306
+ # resolve_format_type covers typed subclasses (BoldElement etc.)
307
+ # that carry no explicit format_type attribute
308
+ case inline.resolve_format_type
299
309
  when 'bold'
300
310
  props.bold = Uniword::Properties::Bold.new
301
311
  when 'italic'
@@ -355,6 +365,11 @@ module Coradoc
355
365
 
356
366
  table.rows.each do |row|
357
367
  tr = Uniword::Wordprocessingml::TableRow.new
368
+ if row.header
369
+ tr.properties = Uniword::Wordprocessingml::TableRowProperties.new(
370
+ tbl_header: Uniword::Wordprocessingml::ValInt.new(value: 1)
371
+ )
372
+ end
358
373
  row.cells.each do |cell|
359
374
  tc = Uniword::Wordprocessingml::TableCell.new
360
375
 
@@ -395,8 +410,7 @@ module Coradoc
395
410
  def build_page_break
396
411
  para = Uniword::Wordprocessingml::Paragraph.new
397
412
  run = Uniword::Wordprocessingml::Run.new
398
- run.break = Uniword::Wordprocessingml::Break.new
399
- run.break.type = 'page'
413
+ run.break = Uniword::Wordprocessingml::Break.new(type: 'page')
400
414
  para.runs << run
401
415
  para
402
416
  end
@@ -404,7 +418,9 @@ module Coradoc
404
418
  def build_inline_text(inline)
405
419
  text = inline.content.to_s
406
420
 
407
- case inline.format_type
421
+ # resolve_format_type covers typed subclasses (BoldElement etc.)
422
+ # that carry no explicit format_type attribute
423
+ case inline.resolve_format_type
408
424
  when 'bold' then Uniword::Builder.text(text, bold: true)
409
425
  when 'italic' then Uniword::Builder.text(text, italic: true)
410
426
  when 'underline' then Uniword::Builder.text(text, underline: true)
@@ -415,7 +431,7 @@ module Coradoc
415
431
  run = Uniword::Wordprocessingml::Run.new(text: text)
416
432
  props = Uniword::Wordprocessingml::RunProperties.new
417
433
  va = Uniword::Properties::VerticalAlign.new
418
- va.value = inline.format_type
434
+ va.value = inline.resolve_format_type
419
435
  props.vertical_align = va
420
436
  run.properties = props
421
437
  run
@@ -39,7 +39,7 @@ module Coradoc
39
39
  def apply(builder, block)
40
40
  return unless block.is_a?(Coradoc::CoreModel::FrontmatterBlock)
41
41
 
42
- data = block.data || {}
42
+ data = Coradoc::CoreModel::FrontmatterBlock::Codec.to_hash(block)
43
43
 
44
44
  if (v = find_first_scalar(data, KEYWORDS_KEYS))
45
45
  builder.keywords(v)
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Coradoc
4
+ module Docx
5
+ module Transform
6
+ # Shared access to a paragraph's numbering (list) attributes.
7
+ #
8
+ # uniword persists w:numPr as NESTED properties:
9
+ # paragraph.properties.numbering_properties.num_id.value
10
+ # paragraph.properties.numbering_properties.ilvl.value
11
+ # while the flat integers (properties.num_id / properties.ilvl) are
12
+ # only populated programmatically (builder/YAML API). Readers must
13
+ # consult the nested shape first and fall back to the flat one —
14
+ # documents deserialized from DOCX only ever carry the nested shape.
15
+ #
16
+ # lutaml-model also returns an UninitializedClass sentinel for
17
+ # attributes that were not deserialized from source, so every read
18
+ # is guarded by an Integer type check instead of truthiness.
19
+ module NumberingAccess
20
+ # Numbering definition ID (w:numId) for a paragraph.
21
+ #
22
+ # @param paragraph [Uniword::Wordprocessingml::Paragraph]
23
+ # @return [Integer, nil]
24
+ def paragraph_num_id(paragraph)
25
+ nested_numbering_value(paragraph, :num_id)
26
+ end
27
+
28
+ # Numbering level (w:ilvl) for a paragraph.
29
+ #
30
+ # @param paragraph [Uniword::Wordprocessingml::Paragraph]
31
+ # @return [Integer, nil]
32
+ def paragraph_ilvl(paragraph)
33
+ nested_numbering_value(paragraph, :ilvl)
34
+ end
35
+
36
+ private
37
+
38
+ def nested_numbering_value(paragraph, attribute)
39
+ numbering = paragraph_properties(paragraph)&.numbering_properties
40
+ nested = numbering&.public_send(attribute)
41
+ value = wrapper_value(nested)
42
+ return value if value.is_a?(Integer)
43
+
44
+ # Fall back to the flat builder/YAML attribute
45
+ flat = paragraph_properties(paragraph)&.public_send(attribute)
46
+ flat.is_a?(Integer) ? flat : nil
47
+ end
48
+
49
+ def paragraph_properties(paragraph)
50
+ paragraph.properties if paragraph.respond_to?(:properties)
51
+ end
52
+
53
+ # Unwrap w:numId / w:ilvl element models to their value; sentinel
54
+ # objects (UninitializedClass) and nils fail the Integer check.
55
+ def wrapper_value(node)
56
+ value = node.respond_to?(:value) ? node.value : node
57
+ value.is_a?(Integer) ? value : nil
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -17,13 +17,14 @@ module Coradoc
17
17
  # dispatches directly after checking style_resolver.list_item?.
18
18
  class ListItemRule < Rule
19
19
  include OrderedContent
20
+ include NumberingAccess
20
21
 
21
22
  def matches?(_element)
22
23
  false # Never auto-matched; orchestrator dispatches directly
23
24
  end
24
25
 
25
26
  def apply(paragraph, context)
26
- ilvl = paragraph.properties&.ilvl.to_i
27
+ ilvl = paragraph_ilvl(paragraph).to_i
27
28
 
28
29
  children = transform_paragraph_content(paragraph, context)
29
30
 
@@ -6,23 +6,20 @@ module Coradoc
6
6
  module Rules
7
7
  # Transforms OMML math elements to CoreModel.
8
8
  #
9
- # Display math (m:oMathPara) → CoreModel::Block (stem)
10
- # Inline math (m:oMath) → CoreModel::InlineElement (stem)
9
+ # Display math (MathEquation#block?) → CoreModel::Block (stem)
10
+ # Inline math (MathEquation#inline?) → CoreModel::InlineElement (stem)
11
11
  #
12
- # Uses Plurimath for OMML → LaTeX conversion when available.
13
- # Falls back to raw XML string when Plurimath is not loaded.
12
+ # LaTeX conversion is provided natively by Uniword::MathEquation.
14
13
  class MathRule < Rule
15
14
  def matches?(element)
16
- return false unless defined?(Uniword::Math)
17
-
18
- element.is_a?(Uniword::Math::OMathPara) ||
19
- element.is_a?(Uniword::Math::OMath)
15
+ defined?(Uniword::MathEquation) &&
16
+ element.is_a?(Uniword::MathEquation)
20
17
  end
21
18
 
22
19
  def apply(element, _context)
23
- latex = omml_to_latex(element)
20
+ latex = element.to_latex.to_s
24
21
 
25
- if display_math?(element)
22
+ if element.block?
26
23
  CoreModel::PassBlock.new(
27
24
  delimiter_type: '++++',
28
25
  language: 'latexmath',
@@ -35,45 +32,6 @@ module Coradoc
35
32
  )
36
33
  end
37
34
  end
38
-
39
- private
40
-
41
- def display_math?(element)
42
- defined?(Uniword::Math::OMathPara) &&
43
- element.is_a?(Uniword::Math::OMathPara)
44
- end
45
-
46
- def omml_to_latex(element)
47
- if defined?(Plurimath)
48
- plurimath_to_latex(element)
49
- else
50
- # Fallback: serialize to XML string
51
- element_respond_to_xml(element) || ''
52
- end
53
- end
54
-
55
- def plurimath_to_latex(element)
56
- xml = element_to_xml(element)
57
- return '' if xml.nil? || xml.empty?
58
-
59
- begin
60
- formula = Plurimath::OMML.parse(xml)
61
- formula.to_latex
62
- rescue StandardError
63
- ''
64
- end
65
- end
66
-
67
- def element_to_xml(element)
68
- return '' unless element.is_a?(Uniword::Wordprocessingml::AlternateContent) ||
69
- element.is_a?(Uniword::Wordprocessingml::OMath)
70
-
71
- element.to_xml
72
- end
73
-
74
- def element_respond_to_xml(element)
75
- element_to_xml(element)
76
- end
77
35
  end
78
36
  end
79
37
  end
@@ -25,9 +25,11 @@ module Coradoc
25
25
  def apply(run, context)
26
26
  # Delegate non-text children (breaks, drawings, footnotes, etc.)
27
27
  non_text = extract_non_text_children(run, context)
28
- return non_text.first if non_text.any? && run.text.nil?
28
+ return non_text.first if non_text.any? && Array(run.text).empty?
29
29
 
30
- text = run.text&.content.to_s
30
+ # w:r may carry several w:t children (uniword models text as
31
+ # a collection) — join them in document order
32
+ text = Array(run.text).map { |node| node&.content.to_s }.join
31
33
  return '' if text.empty? && non_text.empty?
32
34
 
33
35
  props = effective_props(run)
@@ -54,7 +56,7 @@ module Coradoc
54
56
  def extract_non_text_children(run, context)
55
57
  result = []
56
58
 
57
- result << context.transform(run.break) if run.break
59
+ result.concat(Array(run.break).map { |brk| context.transform(brk) })
58
60
 
59
61
  result << context.transform(run.footnote_reference) if run.footnote_reference
60
62
 
@@ -64,7 +64,7 @@ module Coradoc
64
64
  def field_text(field)
65
65
  if field.runs && !field.runs.empty?
66
66
  return field.runs.map do |r|
67
- r.text&.content.to_s
67
+ Array(r.text).map(&:to_s).join
68
68
  end.join
69
69
  end
70
70
 
@@ -32,10 +32,23 @@ module Coradoc
32
32
  def transform_row(row, context)
33
33
  CoreModel::TableRow.new(
34
34
  cells: row.cells.map { |c| transform_cell(c, context) },
35
- header: row.header?
35
+ header: row_header?(row)
36
36
  )
37
37
  end
38
38
 
39
+ # The persisted header flag lives in w:trPr/w:tblHeader
40
+ # (row.properties.tbl_header); TableRow#header? is a runtime-only
41
+ # accessor that deserialized documents never populate.
42
+ def row_header?(row)
43
+ tbl_header = row.properties&.tbl_header
44
+ return row.header? unless tbl_header.is_a?(Uniword::Wordprocessingml::ValInt)
45
+
46
+ # <w:tblHeader/> without w:val means "on" (ST_OnOff semantics)
47
+ return true if tbl_header.value.nil?
48
+
49
+ tbl_header.value.to_i.positive?
50
+ end
51
+
39
52
  def transform_cell(cell, context)
40
53
  inline_children = cell_paragraphs(cell).flat_map do |para|
41
54
  extract_inline_from_paragraph(para, context)
@@ -15,6 +15,8 @@ module Coradoc
15
15
  # The style map is built from the Uniword StylesConfiguration by walking
16
16
  # all style definitions and their basedOn chains.
17
17
  class StyleResolver
18
+ include NumberingAccess
19
+
18
20
  HEADING_PATTERN = /^(heading|heading|h)\s*(\d+)$/i
19
21
  QUOTE_PATTERN = /\bquote\b/i
20
22
  CODE_PATTERN = /\b(code|source|listing)\b/i
@@ -53,14 +55,14 @@ module Coradoc
53
55
 
54
56
  ol = paragraph.properties.outline_level
55
57
  if ol
56
- ol_level = ol.is_a?(Uniword::Wordprocessingml::OutlineLevel) ? ol.value.to_i : ol.to_i
58
+ ol_level = ol.is_a?(Uniword::Properties::OutlineLevel) ? ol.value.to_i : ol.to_i
57
59
  return true if ol_level.positive?
58
60
  end
59
61
 
60
62
  style = find_style_for_paragraph(paragraph)
61
63
  if style&.outline_level
62
64
  ol_val = style.outline_level
63
- ol_val = ol_val.is_a?(Uniword::Wordprocessingml::OutlineLevel) ? ol_val.value.to_i : ol_val.to_i
65
+ ol_val = ol_val.is_a?(Uniword::Properties::OutlineLevel) ? ol_val.value.to_i : ol_val.to_i
64
66
  return true if ol_val.positive?
65
67
  end
66
68
 
@@ -80,7 +82,7 @@ module Coradoc
80
82
  # Check outline_level on paragraph properties
81
83
  ol = paragraph.properties&.outline_level
82
84
  if ol
83
- level = ol.is_a?(Uniword::Wordprocessingml::OutlineLevel) ? ol.value.to_i : ol.to_i
85
+ level = ol.is_a?(Uniword::Properties::OutlineLevel) ? ol.value.to_i : ol.to_i
84
86
  return level if level.positive?
85
87
  end
86
88
 
@@ -93,13 +95,10 @@ module Coradoc
93
95
  def list_item?(paragraph)
94
96
  return false unless paragraph.properties
95
97
 
96
- num_id = paragraph.properties.num_id
97
- # Uniword (via lutaml-model) returns an UninitializedClass
98
- # sentinel for attributes that weren't deserialized from source.
99
- # Treat anything that isn't a real Integer as "no list id".
100
- return false unless num_id.is_a?(Integer)
101
-
102
- num_id.positive?
98
+ # w:numPr is persisted as nested numbering_properties; the flat
99
+ # num_id is only set by the builder/YAML API (see NumberingAccess)
100
+ num_id = paragraph_num_id(paragraph)
101
+ !num_id.nil? && num_id.positive?
103
102
  end
104
103
 
105
104
  # Check if paragraph has a specific role based on style name
@@ -138,10 +137,7 @@ module Coradoc
138
137
  private
139
138
 
140
139
  def resolve_style_name(paragraph)
141
- style_ref = paragraph.properties&.style
142
- return nil unless style_ref
143
-
144
- value = style_ref.is_a?(Uniword::Wordprocessingml::PStyle) ? style_ref.val : style_ref.to_s
140
+ value = paragraph_style_value(paragraph)
145
141
  return nil unless value
146
142
 
147
143
  mapped = @style_map[value]
@@ -154,7 +150,7 @@ module Coradoc
154
150
  style_ref = run.properties.style
155
151
  return nil unless style_ref
156
152
 
157
- value = style_ref.is_a?(Uniword::Wordprocessingml::PStyle) ? style_ref.val : style_ref.to_s
153
+ value = style_ref.value.to_s
158
154
  return nil unless value
159
155
 
160
156
  mapped = @style_map[value]
@@ -173,10 +169,12 @@ module Coradoc
173
169
  end
174
170
 
175
171
  def style_id_from_paragraph(paragraph)
176
- style_ref = paragraph.properties&.style
177
- return nil unless style_ref
172
+ paragraph_style_value(paragraph)
173
+ end
178
174
 
179
- style_ref.is_a?(Uniword::Wordprocessingml::PStyle) ? style_ref.val : style_ref.to_s
175
+ # uniword models w:pStyle as a collection of StyleReference
176
+ def paragraph_style_value(paragraph)
177
+ Array(paragraph.properties&.style).filter_map(&:value).first.to_s
180
178
  end
181
179
 
182
180
  def build_style_map(config)
@@ -24,6 +24,8 @@ module Coradoc
24
24
  # core = ToCoreModel.transform(doc)
25
25
  # # => Coradoc::CoreModel::StructuralElement
26
26
  class ToCoreModel
27
+ include NumberingAccess
28
+
27
29
  class << self
28
30
  def transform(document)
29
31
  new.transform(document)
@@ -139,7 +141,7 @@ module Coradoc
139
141
 
140
142
  def section_break_element(paragraph, context)
141
143
  # First, transform the paragraph content if it has text
142
- content = paragraph.runs&.map { |r| r.text&.content.to_s }&.join
144
+ content = paragraph.runs&.map { |r| Array(r.text).map(&:to_s).join }&.join
143
145
  if content && !content.strip.empty?
144
146
  # Has content — transform normally (content comes before the break)
145
147
  context.transform(paragraph)
@@ -154,7 +156,7 @@ module Coradoc
154
156
  # Collect consecutive list items with the same numId into a ListBlock
155
157
  def group_list(elements, start_index, context)
156
158
  first = elements[start_index]
157
- num_id = first.properties&.num_id.to_i
159
+ num_id = paragraph_num_id(first).to_i
158
160
  items = []
159
161
  consumed = 0
160
162
 
@@ -163,7 +165,7 @@ module Coradoc
163
165
  para = elements[idx]
164
166
  break unless paragraph?(para)
165
167
  break unless context.style_resolver.list_item?(para)
166
- break unless para.properties&.num_id.to_i == num_id
168
+ break unless paragraph_num_id(para).to_i == num_id
167
169
 
168
170
  items << @list_item_rule.apply(para, context)
169
171
  consumed += 1
@@ -242,7 +244,7 @@ module Coradoc
242
244
  next unless context.style_resolver.heading_level(para) == 1
243
245
 
244
246
  runs = para.runs || []
245
- return runs.map { |r| r.text&.content.to_s }.join
247
+ return runs.map { |r| Array(r.text).map(&:to_s).join }.join
246
248
  end
247
249
 
248
250
  nil
@@ -312,7 +314,7 @@ module Coradoc
312
314
  runs = para.runs || []
313
315
  return nil unless runs
314
316
 
315
- runs.map { |r| r.text&.content.to_s }.join
317
+ runs.map { |r| Array(r.text).map(&:to_s).join }.join
316
318
  end
317
319
 
318
320
  # Check if header/footer text is purely layout content
@@ -10,6 +10,7 @@ module Coradoc
10
10
  autoload :FromCoreModel, 'coradoc/docx/transform/from_core_model'
11
11
  autoload :StyleResolver, 'coradoc/docx/transform/style_resolver'
12
12
  autoload :NumberingResolver, 'coradoc/docx/transform/numbering_resolver'
13
+ autoload :NumberingAccess, 'coradoc/docx/transform/numbering_access'
13
14
  autoload :OrderedContent, 'coradoc/docx/transform/ordered_content'
14
15
  autoload :FrontmatterCoreProperties, 'coradoc/docx/transform/frontmatter_core_properties'
15
16
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Coradoc
4
4
  module Docx
5
- VERSION = '0.1.3'
5
+ VERSION = '0.1.4'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: coradoc-docx
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -41,16 +41,16 @@ dependencies:
41
41
  name: uniword
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
- - - ">="
44
+ - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '0'
46
+ version: '1.6'
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
- - - ">="
51
+ - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: '0'
53
+ version: '1.6'
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: rake
56
56
  requirement: !ruby/object:Gem::Requirement
@@ -109,6 +109,7 @@ files:
109
109
  - lib/coradoc/docx/transform/context.rb
110
110
  - lib/coradoc/docx/transform/from_core_model.rb
111
111
  - lib/coradoc/docx/transform/frontmatter_core_properties.rb
112
+ - lib/coradoc/docx/transform/numbering_access.rb
112
113
  - lib/coradoc/docx/transform/numbering_resolver.rb
113
114
  - lib/coradoc/docx/transform/ordered_content.rb
114
115
  - lib/coradoc/docx/transform/rule.rb