uniword 1.0.7 → 1.0.9

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: ea1c6924b201809b0adcd798ea5453ceb06a6628e9ea54ec30221d4d4b325ca4
4
- data.tar.gz: 45d19527778e0c36b9acb7a2a7ce98e83ea9f841df619c2bf1eb3ae4f77fbc46
3
+ metadata.gz: 8b3b37b9a1642f94a634c42b754630ca35cd7167ddc1415223dd65848a379c48
4
+ data.tar.gz: 011ddc8e381521e2d161cd0cf9fd0dd9eee791e9f28026fd63e89cf9386bfa03
5
5
  SHA512:
6
- metadata.gz: 9ff4355adebd0f8eb10843ca4ae83b43df148340b3ab562d1bd43b1317d6384fd0dd3fe713e741df9d57dbad2eca70684e946565dc9823bb923c75cc2f302226
7
- data.tar.gz: 92b190e3421fce1634462ce0b6bdff183138c6eaefa41ef96cce9b441eba72661988a36ae652fa5df8afdd1ef3eeb3d62bdd9e7eb1cd09b10a7ae7863023b331
6
+ metadata.gz: 1c6c8785a7fe56480f0d7bdda19ec56e50704653016d4146c561e401c8e98b856cf99a1205411521b78cd19b701e794dfbc75c4d4c844b87e2b8fe3d44fb514e
7
+ data.tar.gz: 9275ea155ee7af2ee9cd0e24d6534a0124f0f8501583206fb98eb3c63e7736c280552336a47bad97a31e50b235805c5fba904d1a80c0cee9bbe6f216f7e49f02
@@ -11,8 +11,9 @@ module Uniword
11
11
  class CommentBuilder
12
12
  attr_reader :model
13
13
 
14
- def initialize(author:, comment_id: nil, date: nil, initials: nil)
15
- @model = Comment.new(
14
+ def initialize(model: nil, author: nil, comment_id: nil, date: nil,
15
+ initials: nil)
16
+ @model = model || Comment.new(
16
17
  author: author,
17
18
  comment_id: comment_id,
18
19
  date: date,
@@ -22,9 +23,7 @@ module Uniword
22
23
 
23
24
  # Wrap an existing Comment model
24
25
  def self.from_model(model)
25
- builder = allocate
26
- builder.instance_variable_set(:@model, model)
27
- builder
26
+ new(model: model)
28
27
  end
29
28
 
30
29
  # Append text to the comment (creates a paragraph)
@@ -148,15 +148,25 @@ module Uniword
148
148
  run
149
149
  end
150
150
 
151
- # Factory: creates a Run with a tab character
151
+ # Factory: creates a Run containing a tab character.
152
+ # Use this with ParagraphBuilder#<< to append a tab to a paragraph.
152
153
  #
153
154
  # @return [Wordprocessingml::Run]
155
+ # @see .tab_element for the bare Tab element
154
156
  def self.tab
155
157
  run = Wordprocessingml::Run.new
156
- run.tab = Wordprocessingml::Tab.new
158
+ run.tab = tab_element
157
159
  run
158
160
  end
159
161
 
162
+ # Factory: creates a bare Tab element for direct assignment to Run#tab.
163
+ # Most callers should use .tab instead, which wraps the Tab in a Run.
164
+ #
165
+ # @return [Wordprocessingml::Tab]
166
+ def self.tab_element
167
+ Wordprocessingml::Tab.new
168
+ end
169
+
160
170
  # Factory: creates a Run containing a line break
161
171
  #
162
172
  # @return [Wordprocessingml::Run]
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uniword
4
+ # Tagging module for document types accepted by DocumentWriter and FormatConverter.
5
+ module DocumentInput
6
+ end
7
+ end
@@ -136,11 +136,7 @@ module Uniword
136
136
  # @raise [ArgumentError] if document is invalid
137
137
  def validate_document(doc)
138
138
  raise ArgumentError, "Document cannot be nil" if doc.nil?
139
-
140
- # Accept OOXML Document
141
- return if doc.is_a?(Uniword::Wordprocessingml::DocumentRoot)
142
- # Accept MHTML Document
143
- return if doc.is_a?(Uniword::Mhtml::Document)
139
+ return if doc.is_a?(Uniword::DocumentInput)
144
140
 
145
141
  raise ArgumentError, "Must be a Document instance"
146
142
  end
@@ -33,18 +33,14 @@ document_rels)
33
33
  document_rels)
34
34
  # Package infrastructure
35
35
  content["[Content_Types].xml"] =
36
- content_types.to_xml(encoding: "UTF-8", declaration: true,
37
- standalone: true)
36
+ serialize_infrastructure(content_types)
38
37
  content["_rels/.rels"] =
39
- package_rels.to_xml(encoding: "UTF-8", declaration: true,
40
- standalone: true)
38
+ serialize_infrastructure(package_rels)
41
39
 
42
40
  # Document properties
43
41
  if core_properties
44
42
  content["docProps/core.xml"] =
45
- core_properties.to_xml(encoding: "UTF-8",
46
- prefix: true,
47
- standalone: true)
43
+ serialize_part(core_properties)
48
44
  end
49
45
  if app_properties
50
46
  content["docProps/app.xml"] =
@@ -78,70 +74,57 @@ document_rels)
78
74
  # Document parts
79
75
  if document
80
76
  content["word/document.xml"] =
81
- document.to_xml(encoding: "UTF-8", prefix: true,
82
- standalone: true)
77
+ serialize_part(document)
83
78
  end
84
79
  if styles
85
80
  content["word/styles.xml"] =
86
- styles.to_xml(encoding: "UTF-8", prefix: true,
87
- standalone: true)
81
+ serialize_part(styles)
88
82
  end
89
83
  if numbering
90
84
  content["word/numbering.xml"] =
91
- numbering.to_xml(encoding: "UTF-8", prefix: true,
92
- standalone: true)
85
+ serialize_part(numbering)
93
86
  end
94
87
  if settings
95
88
  content["word/settings.xml"] =
96
- settings.to_xml(encoding: "UTF-8", prefix: true,
97
- standalone: true)
89
+ serialize_part(settings)
98
90
  end
99
91
  if font_table
100
92
  content["word/fontTable.xml"] =
101
- font_table.to_xml(encoding: "UTF-8", prefix: true,
102
- standalone: true)
93
+ serialize_part(font_table)
103
94
  end
104
95
  if web_settings
105
96
  content["word/webSettings.xml"] =
106
- web_settings.to_xml(encoding: "UTF-8", prefix: true,
107
- standalone: true)
97
+ serialize_part(web_settings)
108
98
  end
109
99
  if document_rels
110
100
  content["word/_rels/document.xml.rels"] =
111
- document_rels.to_xml(encoding: "UTF-8", declaration: true,
112
- standalone: true)
101
+ serialize_infrastructure(document_rels)
113
102
  end
114
103
 
115
104
  # Theme
116
105
  if theme
117
106
  content["word/theme/theme1.xml"] =
118
- theme.to_xml(encoding: "UTF-8", prefix: true,
119
- standalone: true)
107
+ serialize_part(theme)
120
108
  end
121
109
  if theme_rels
122
110
  content["word/theme/_rels/theme1.xml.rels"] =
123
- theme_rels.to_xml(encoding: "UTF-8", declaration: true,
124
- standalone: true)
111
+ serialize_infrastructure(theme_rels)
125
112
  end
126
113
 
127
114
  # Notes
128
115
  if footnotes
129
116
  content["word/footnotes.xml"] =
130
- footnotes.to_xml(encoding: "UTF-8", prefix: true,
131
- standalone: true)
117
+ serialize_part(footnotes)
132
118
  end
133
119
  if endnotes
134
120
  content["word/endnotes.xml"] =
135
- endnotes.to_xml(encoding: "UTF-8", prefix: true,
136
- standalone: true)
121
+ serialize_part(endnotes)
137
122
  end
138
123
 
139
124
  # Bibliography sources
140
125
  if document&.bibliography_sources
141
126
  content["word/sources.xml"] =
142
- document.bibliography_sources.to_xml(encoding: "UTF-8",
143
- declaration: true,
144
- standalone: true)
127
+ serialize_infrastructure(document.bibliography_sources)
145
128
  end
146
129
 
147
130
  # Headers and footers
@@ -150,6 +133,16 @@ document_rels)
150
133
  serialize_header_footer_parts(content)
151
134
  end
152
135
 
136
+ # Serialize an OOXML document part with standard encoding
137
+ def serialize_part(model)
138
+ model.to_xml(encoding: "UTF-8", prefix: true, standalone: true)
139
+ end
140
+
141
+ # Serialize package infrastructure (rels, content types) with declaration
142
+ def serialize_infrastructure(model)
143
+ model.to_xml(encoding: "UTF-8", declaration: true, standalone: true)
144
+ end
145
+
153
146
  private
154
147
 
155
148
  def inject_image_parts(content, content_types, document_rels)
@@ -451,8 +444,7 @@ document_rels)
451
444
  document.headers.each_value do |header_obj|
452
445
  idx += 1
453
446
  content["word/header#{idx}.xml"] =
454
- header_obj.to_xml(encoding: "UTF-8", prefix: true,
455
- standalone: true)
447
+ serialize_part(header_obj)
456
448
  end
457
449
  end
458
450
 
@@ -463,8 +455,7 @@ document_rels)
463
455
  document.footers.each_value do |footer_obj|
464
456
  idx += 1
465
457
  content["word/footer#{idx}.xml"] =
466
- footer_obj.to_xml(encoding: "UTF-8", prefix: true,
467
- standalone: true)
458
+ serialize_part(footer_obj)
468
459
  end
469
460
  end
470
461
 
@@ -473,8 +464,7 @@ document_rels)
473
464
 
474
465
  document.header_footer_parts.each do |part|
475
466
  content["word/#{part[:target]}"] =
476
- part[:content].to_xml(encoding: "UTF-8", prefix: true,
477
- standalone: true)
467
+ serialize_part(part[:content])
478
468
  end
479
469
  end
480
470
  end
@@ -85,15 +85,15 @@ module Uniword
85
85
  ].compact
86
86
 
87
87
  parts.each do |part|
88
- part.instance_variable_set(:@pending_namespace_data, nil)
89
- part.instance_variable_set(:@import_declaration_plan, nil)
88
+ part.pending_namespace_data = nil
89
+ part.import_declaration_plan = nil
90
+ part.pending_plan_root_element = nil
91
+ # TODO: @xml_input_namespaces has no public accessor in lutaml-model.
92
+ # Replace with public API once lutaml-model exposes one.
90
93
  part.instance_variable_set(:@xml_input_namespaces, nil)
91
- part.instance_variable_set(:@pending_plan_root_element, nil)
92
94
  end
93
95
  end
94
96
 
95
- # -- Section Properties --
96
-
97
97
  def reconcile_section_properties
98
98
  return unless package.document&.body
99
99
 
@@ -313,7 +313,7 @@ module Uniword
313
313
  color_name = color_name.to_sym
314
314
  # Map folHlink to fol_hlink for attribute access
315
315
  attr_name = color_name == :folHlink ? :fol_hlink : color_name
316
- color_obj = instance_variable_get("@#{attr_name}")
316
+ color_obj = public_send(attr_name)
317
317
  color_obj&.value
318
318
  end
319
319
 
@@ -345,7 +345,7 @@ module Uniword
345
345
 
346
346
  color_obj = color_class.new
347
347
  color_obj.rgb = value
348
- instance_variable_set("@#{attr_name}", color_obj)
348
+ public_send(:"#{attr_name}=", color_obj)
349
349
  @colors_hash[color_name] = value
350
350
  end
351
351
 
@@ -277,30 +277,15 @@ module Uniword
277
277
  # @param document [Object] Document (OOXML or MHTML)
278
278
  # @return [Hash] Statistics hash
279
279
  def document_stats(document)
280
- # Handle OOXML document
281
- if document.is_a?(Uniword::Wordprocessingml::DocumentRoot) || document.is_a?(Uniword::Wordprocessingml::Body)
280
+ if document.is_a?(Uniword::Wordprocessingml::Body)
282
281
  return {
283
282
  paragraphs: document.paragraphs.count,
284
- tables: document.is_a?(Uniword::Wordprocessingml::DocumentRoot) ? document.tables.count : 0,
285
- images: document.is_a?(Uniword::Wordprocessingml::DocumentRoot) ? document.images.count : 0
283
+ tables: 0,
284
+ images: 0
286
285
  }
287
286
  end
288
287
 
289
- # Handle MHTML document - estimate from HTML content
290
- html = if document.is_a?(Mhtml::Document) && document.raw_html
291
- document.raw_html
292
- elsif document.is_a?(Mhtml::Document) && document.html_content
293
- document.html_content
294
- end
295
- if html
296
- {
297
- paragraphs: html.scan(/<p[\s>]/i).count,
298
- tables: html.scan(/<table/i).count,
299
- images: html.scan(/<img/i).count
300
- }
301
- else
302
- { paragraphs: 0, tables: 0, images: 0 }
303
- end
288
+ document.document_stats
304
289
  end
305
290
 
306
291
  # Validate conversion parameters
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uniword
4
+ # Tagging module for elements that track their position among sibling runs.
5
+ # Used by the MHTML renderer to interleave hyperlinks and math objects
6
+ # at the correct position within a paragraph.
7
+ module HasRunPosition
8
+ end
9
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "lutaml/model"
4
+ require_relative "../has_run_position"
4
5
 
5
6
  module Uniword
6
7
  module Math
@@ -9,6 +10,8 @@ module Uniword
9
10
  # Generated from OOXML schema: math.yml
10
11
  # Element: <m:oMath>
11
12
  class OMath < Lutaml::Model::Serializable
13
+ include Uniword::HasRunPosition
14
+
12
15
  # Pattern 0: Attributes BEFORE xml mappings
13
16
  attribute :runs, MathRun, collection: true, initialize_empty: true
14
17
  attribute :functions, Function, collection: true, initialize_empty: true
@@ -32,6 +35,9 @@ module Uniword
32
35
  attribute :equation_arrays, EquationArray, collection: true,
33
36
  initialize_empty: true
34
37
 
38
+ # Transient: position among sibling runs (used by MHTML renderer)
39
+ attribute :run_position, :integer
40
+
35
41
  xml do
36
42
  element "oMath"
37
43
  namespace Uniword::Ooxml::Namespaces::MathML
@@ -2,6 +2,8 @@
2
2
 
3
3
  require "lutaml/model"
4
4
 
5
+ require_relative "../document_input"
6
+
5
7
  module Uniword
6
8
  module Mhtml
7
9
  # MHTML Document — top-level model for .mht/.mhtml/.doc files.
@@ -17,6 +19,8 @@ module Uniword
17
19
  # ├── word_document_settings (Metadata::WordDocumentSettings)
18
20
  # └── filelist_xml (String)
19
21
  class Document < Lutaml::Model::Serializable
22
+ include Uniword::DocumentInput
23
+
20
24
  # MIME headers
21
25
  attribute :mime_version, :string, default: -> { "1.0" }
22
26
  attribute :boundary, :string
@@ -161,6 +165,20 @@ module Uniword
161
165
  "#<#{self.class} parts=#{parts.length} images=#{image_parts.length} " \
162
166
  "xml=#{xml_parts.length} theme=#{theme_part ? 'yes' : 'no'}>"
163
167
  end
168
+
169
+ # @return [Hash] Document statistics (paragraphs, tables, images)
170
+ def document_stats
171
+ html = raw_html || html_content
172
+ if html
173
+ {
174
+ paragraphs: html.scan(/<p[\s>]/i).count,
175
+ tables: html.scan(/<table/i).count,
176
+ images: html.scan(/<img/i).count,
177
+ }
178
+ else
179
+ { paragraphs: 0, tables: 0, images: 0 }
180
+ end
181
+ end
164
182
  end
165
183
  end
166
184
  end
@@ -1,40 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "lutaml/model"
3
+ require_relative "boolean_element_factory"
4
4
 
5
5
  module Uniword
6
6
  module Properties
7
- # Bold formatting element
8
- #
9
- # OOXML: <w:b/> means bold=true (absence of val= means true)
10
- # <w:b w:val="false"/> means bold=false
11
- #
12
- # Model: nil = true (no val attribute), 'false' = explicit false
13
- class Bold < Lutaml::Model::Serializable
14
- include Uniword::Properties::BooleanElement
15
-
16
- attribute :val, :string, default: nil
17
- include Uniword::Properties::BooleanValSetter
18
-
19
- xml do
20
- element "b"
21
- namespace Uniword::Ooxml::Namespaces::WordProcessingML
22
- map_attribute "val", to: :val, render_nil: false, render_default: false
23
- end
24
- end
25
-
26
- # Complex script bold
27
- class BoldCs < Lutaml::Model::Serializable
28
- include Uniword::Properties::BooleanElement
29
-
30
- attribute :val, :string, default: nil
31
- include Uniword::Properties::BooleanValSetter
32
-
33
- xml do
34
- element "bCs"
35
- namespace Uniword::Ooxml::Namespaces::WordProcessingML
36
- map_attribute "val", to: :val, render_nil: false, render_default: false
37
- end
38
- end
7
+ BooleanElementFactory.define("b", "Bold")
8
+ BooleanElementFactory.define("bCs", "BoldCs")
39
9
  end
40
10
  end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lutaml/model"
4
+
5
+ module Uniword
6
+ module Properties
7
+ # OOXML boolean element mixin for value/val getter logic only.
8
+ # The val= setter must be defined AFTER attribute :val to override
9
+ # the generated setter.
10
+ module BooleanElement
11
+ def value
12
+ val != "false"
13
+ end
14
+ end
15
+
16
+ # Helper to define val= override after attribute declaration.
17
+ # Uses alias_method to save the generated setter, then overrides it.
18
+ module BooleanValSetter
19
+ def self.included(base)
20
+ base.class_eval do
21
+ alias_method :__original_val_setter=, :val= if method_defined?(:val=)
22
+ define_method(:val=) do |v|
23
+ @val = if v.nil?
24
+ nil
25
+ elsif v == false || v.to_s == "false"
26
+ "false"
27
+ elsif v == true || v.to_s == "true"
28
+ nil
29
+ else
30
+ v
31
+ end
32
+ value_set_for(:val)
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ # Factory for OOXML boolean element classes.
39
+ #
40
+ # Generates a lutaml-model class that maps to an XML element like
41
+ # <w:b/> (true) or <w:b w:val="false"/> (false).
42
+ #
43
+ # @param element_name [String] XML element name (e.g. "b", "i", "strike")
44
+ # @param class_name [String] Ruby class name (e.g. "Bold", "Italic")
45
+ # @return [Class] The generated class, also assigned as a constant
46
+ #
47
+ # @example
48
+ # BooleanElementFactory.define("b", "Bold")
49
+ # BooleanElementFactory.define("bCs", "BoldCs")
50
+ module BooleanElementFactory
51
+ WML_NS = Uniword::Ooxml::Namespaces::WordProcessingML
52
+
53
+ def self.define(element_name, class_name)
54
+ klass = Class.new(Lutaml::Model::Serializable) do
55
+ include BooleanElement
56
+
57
+ attribute :val, :string, default: nil
58
+ include BooleanValSetter
59
+
60
+ xml do
61
+ element element_name
62
+ namespace WML_NS
63
+ map_attribute "val", to: :val, render_nil: false,
64
+ render_default: false
65
+ end
66
+ end
67
+
68
+ Properties.const_set(class_name, klass)
69
+ klass
70
+ end
71
+ end
72
+ end
73
+ end
@@ -1,228 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "lutaml/model"
3
+ require_relative "boolean_element_factory"
4
4
 
5
5
  module Uniword
6
6
  module Properties
7
- # OOXML boolean element mixin for value/val getter logic only.
8
- # The val= setter must be defined AFTER attribute :val to override
9
- # the generated setter.
10
- module BooleanElement
11
- def value
12
- @val != "false"
13
- end
14
-
15
- def value=(v)
16
- @val = v ? nil : "false"
17
- end
18
- end
19
-
20
- # Helper to define val= override after attribute declaration.
21
- # Uses alias_method to save the generated setter, then overrides it.
22
- module BooleanValSetter
23
- def self.included(base)
24
- base.class_eval do
25
- # Save the generated val= method
26
- alias_method :__original_val_setter=, :val= if method_defined?(:val=)
27
- # Override val= with our custom logic
28
- define_method(:val=) do |v|
29
- @val = if v.nil?
30
- nil
31
- elsif v == false || v.to_s == "false"
32
- "false"
33
- elsif v == true || v.to_s == "true"
34
- nil
35
- else
36
- v
37
- end
38
- value_set_for(:val) if @val
39
- end
40
- end
41
- end
42
- end
43
-
44
- # Strike-through formatting
45
- class Strike < Lutaml::Model::Serializable
46
- include BooleanElement
47
-
48
- attribute :val, :string, default: nil
49
- include BooleanValSetter
50
-
51
- xml do
52
- element "strike"
53
- namespace Uniword::Ooxml::Namespaces::WordProcessingML
54
- map_attribute "val", to: :val, render_nil: false, render_default: false
55
- end
56
- end
57
-
58
- # Double strike-through formatting
59
- class DoubleStrike < Lutaml::Model::Serializable
60
- include BooleanElement
61
-
62
- attribute :val, :string, default: nil
63
- include BooleanValSetter
64
-
65
- xml do
66
- element "dstrike"
67
- namespace Uniword::Ooxml::Namespaces::WordProcessingML
68
- map_attribute "val", to: :val, render_nil: false, render_default: false
69
- end
70
- end
71
-
72
- # Small caps formatting
73
- class SmallCaps < Lutaml::Model::Serializable
74
- include BooleanElement
75
-
76
- attribute :val, :string, default: nil
77
- include BooleanValSetter
78
-
79
- xml do
80
- element "smallCaps"
81
- namespace Uniword::Ooxml::Namespaces::WordProcessingML
82
- map_attribute "val", to: :val, render_nil: false, render_default: false
83
- end
84
- end
85
-
86
- # All caps formatting
87
- class Caps < Lutaml::Model::Serializable
88
- include BooleanElement
89
-
90
- attribute :val, :string, default: nil
91
- include BooleanValSetter
92
-
93
- xml do
94
- element "caps"
95
- namespace Uniword::Ooxml::Namespaces::WordProcessingML
96
- map_attribute "val", to: :val, render_nil: false, render_default: false
97
- end
98
- end
99
-
100
- # Hidden text
101
- class Vanish < Lutaml::Model::Serializable
102
- include BooleanElement
103
-
104
- attribute :val, :string, default: nil
105
- include BooleanValSetter
106
-
107
- xml do
108
- element "vanish"
109
- namespace Uniword::Ooxml::Namespaces::WordProcessingML
110
- map_attribute "val", to: :val, render_nil: false, render_default: false
111
- end
112
- end
113
-
114
- # Web hidden text (hidden from web view)
115
- class WebHidden < Lutaml::Model::Serializable
116
- include BooleanElement
117
-
118
- attribute :val, :string, default: nil
119
- include BooleanValSetter
120
-
121
- xml do
122
- element "webHidden"
123
- namespace Uniword::Ooxml::Namespaces::WordProcessingML
124
- map_attribute "val", to: :val, render_nil: false, render_default: false
125
- end
126
- end
127
-
128
- # No proofing (disable spell/grammar check)
129
- class NoProof < Lutaml::Model::Serializable
130
- include BooleanElement
131
-
132
- attribute :val, :string, default: nil
133
- include BooleanValSetter
134
-
135
- xml do
136
- element "noProof"
137
- namespace Uniword::Ooxml::Namespaces::WordProcessingML
138
- map_attribute "val", to: :val, render_nil: false, render_default: false
139
- end
140
- end
141
-
142
- # Shadow formatting
143
- class Shadow < Lutaml::Model::Serializable
144
- include BooleanElement
145
-
146
- attribute :val, :string, default: nil
147
- include BooleanValSetter
148
-
149
- xml do
150
- element "shadow"
151
- namespace Uniword::Ooxml::Namespaces::WordProcessingML
152
- map_attribute "val", to: :val, render_nil: false, render_default: false
153
- end
154
- end
155
-
156
- # Emboss formatting
157
- class Emboss < Lutaml::Model::Serializable
158
- include BooleanElement
159
-
160
- attribute :val, :string, default: nil
161
- include BooleanValSetter
162
-
163
- xml do
164
- element "emboss"
165
- namespace Uniword::Ooxml::Namespaces::WordProcessingML
166
- map_attribute "val", to: :val, render_nil: false, render_default: false
167
- end
168
- end
169
-
170
- # Imprint formatting
171
- class Imprint < Lutaml::Model::Serializable
172
- include BooleanElement
173
-
174
- attribute :val, :string, default: nil
175
- include BooleanValSetter
176
-
177
- xml do
178
- element "imprint"
179
- namespace Uniword::Ooxml::Namespaces::WordProcessingML
180
- map_attribute "val", to: :val, render_nil: false, render_default: false
181
- end
182
- end
7
+ # Run-level boolean formatting elements
8
+ BooleanElementFactory.define("strike", "Strike")
9
+ BooleanElementFactory.define("dstrike", "DoubleStrike")
10
+ BooleanElementFactory.define("smallCaps", "SmallCaps")
11
+ BooleanElementFactory.define("caps", "Caps")
12
+ BooleanElementFactory.define("vanish", "Vanish")
13
+ BooleanElementFactory.define("webHidden", "WebHidden")
14
+ BooleanElementFactory.define("noProof", "NoProof")
15
+ BooleanElementFactory.define("shadow", "Shadow")
16
+ BooleanElementFactory.define("emboss", "Emboss")
17
+ BooleanElementFactory.define("imprint", "Imprint")
183
18
 
184
19
  # Style-level boolean elements
185
-
186
- # Quick format flag
187
- class QuickFormat < Lutaml::Model::Serializable
188
- include BooleanElement
189
-
190
- attribute :val, :string, default: nil
191
- include BooleanValSetter
192
-
193
- xml do
194
- element "qFormat"
195
- namespace Uniword::Ooxml::Namespaces::WordProcessingML
196
- map_attribute "val", to: :val, render_nil: false, render_default: false
197
- end
198
- end
199
-
200
- # Keep with next paragraph
201
- class KeepNext < Lutaml::Model::Serializable
202
- include BooleanElement
203
-
204
- attribute :val, :string, default: nil
205
- include BooleanValSetter
206
-
207
- xml do
208
- element "keepNext"
209
- namespace Uniword::Ooxml::Namespaces::WordProcessingML
210
- map_attribute "val", to: :val, render_nil: false, render_default: false
211
- end
212
- end
213
-
214
- # Keep lines together
215
- class KeepLines < Lutaml::Model::Serializable
216
- include BooleanElement
217
-
218
- attribute :val, :string, default: nil
219
- include BooleanValSetter
220
-
221
- xml do
222
- element "keepLines"
223
- namespace Uniword::Ooxml::Namespaces::WordProcessingML
224
- map_attribute "val", to: :val, render_nil: false, render_default: false
225
- end
226
- end
20
+ BooleanElementFactory.define("qFormat", "QuickFormat")
21
+ BooleanElementFactory.define("keepNext", "KeepNext")
22
+ BooleanElementFactory.define("keepLines", "KeepLines")
227
23
  end
228
24
  end
@@ -1,40 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "lutaml/model"
3
+ require_relative "boolean_element_factory"
4
4
 
5
5
  module Uniword
6
6
  module Properties
7
- # Italic formatting element
8
- #
9
- # OOXML: <w:i/> means italic=true (absence of val= means true)
10
- # <w:i w:val="false"/> means italic=false
11
- #
12
- # Model: nil = true (no val attribute), 'false' = explicit false
13
- class Italic < Lutaml::Model::Serializable
14
- include Uniword::Properties::BooleanElement
15
-
16
- attribute :val, :string, default: nil
17
- include Uniword::Properties::BooleanValSetter
18
-
19
- xml do
20
- element "i"
21
- namespace Uniword::Ooxml::Namespaces::WordProcessingML
22
- map_attribute "val", to: :val, render_nil: false, render_default: false
23
- end
24
- end
25
-
26
- # Complex script italic
27
- class ItalicCs < Lutaml::Model::Serializable
28
- include Uniword::Properties::BooleanElement
29
-
30
- attribute :val, :string, default: nil
31
- include Uniword::Properties::BooleanValSetter
32
-
33
- xml do
34
- element "iCs"
35
- namespace Uniword::Ooxml::Namespaces::WordProcessingML
36
- map_attribute "val", to: :val, render_nil: false, render_default: false
37
- end
38
- end
7
+ BooleanElementFactory.define("i", "Italic")
8
+ BooleanElementFactory.define("iCs", "ItalicCs")
39
9
  end
40
10
  end
@@ -23,10 +23,13 @@ module Uniword
23
23
  autoload :NumberingProperties, "uniword/properties/numbering_properties"
24
24
  autoload :Tabs, "uniword/properties/tabs"
25
25
 
26
+ # Boolean element infrastructure
27
+ autoload :BooleanElement, "uniword/properties/boolean_element_factory"
28
+ autoload :BooleanValSetter, "uniword/properties/boolean_element_factory"
29
+ autoload :BooleanElementFactory,
30
+ "uniword/properties/boolean_element_factory"
31
+
26
32
  # Boolean formatting classes (from boolean_formatting.rb)
27
- # Note: BooleanElement and BooleanValSetter are also in boolean_formatting.rb
28
- autoload :BooleanElement, "uniword/properties/boolean_formatting"
29
- autoload :BooleanValSetter, "uniword/properties/boolean_formatting"
30
33
  autoload :Strike, "uniword/properties/boolean_formatting"
31
34
  autoload :DoubleStrike, "uniword/properties/boolean_formatting"
32
35
  autoload :SmallCaps, "uniword/properties/boolean_formatting"
@@ -49,7 +52,9 @@ module Uniword
49
52
 
50
53
  # Bold and Italic (from bold.rb and italic.rb)
51
54
  autoload :Bold, "uniword/properties/bold"
55
+ autoload :BoldCs, "uniword/properties/bold"
52
56
  autoload :Italic, "uniword/properties/italic"
57
+ autoload :ItalicCs, "uniword/properties/italic"
53
58
 
54
59
  # Complex objects
55
60
  autoload :Spacing, "uniword/properties/spacing"
@@ -79,7 +79,7 @@ module Uniword
79
79
 
80
80
  # Set the color using the appropriate attribute
81
81
  attr_name = color_name == "folHlink" ? :fol_hlink : color_name.to_sym
82
- scheme.instance_variable_set("@#{attr_name}", color_obj)
82
+ scheme.public_send(:"#{attr_name}=", color_obj)
83
83
  end
84
84
 
85
85
  # Sync hash interface
@@ -543,11 +543,11 @@ module Uniword
543
543
  parts.join
544
544
  end
545
545
 
546
- # Build position-indexed map from elements that have @_run_position markers
546
+ # Build position-indexed map from elements that have run_position markers
547
547
  def build_position_map(elements, fallback_pos)
548
548
  by_pos = {}
549
549
  elements.each do |el|
550
- pos = el.instance_variable_get(:@_run_position)
550
+ pos = el.run_position if el.is_a?(Uniword::HasRunPosition)
551
551
  pos ||= fallback_pos
552
552
  by_pos[pos] ||= []
553
553
  by_pos[pos] << el
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Uniword
4
- VERSION = "1.0.7"
4
+ VERSION = "1.0.9"
5
5
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "lutaml/model"
4
4
 
5
+ require_relative "../document_input"
5
6
  require_relative "document_root/feature_facade"
6
7
 
7
8
  module Uniword
@@ -12,6 +13,7 @@ module Uniword
12
13
  # Element: <w:document>
13
14
  class DocumentRoot < Lutaml::Model::Serializable
14
15
  include FeatureFacade
16
+ include Uniword::DocumentInput
15
17
 
16
18
  attribute :mc_ignorable, Uniword::Ooxml::Types::McIgnorable
17
19
  attribute :body, Body, default: -> { Body.new }
@@ -210,6 +212,15 @@ module Uniword
210
212
  end.compact
211
213
  end
212
214
 
215
+ # @return [Hash] Document statistics (paragraphs, tables, images)
216
+ def document_stats
217
+ {
218
+ paragraphs: paragraphs.count,
219
+ tables: tables.count,
220
+ images: images.count,
221
+ }
222
+ end
223
+
213
224
  # Check if document structure is valid.
214
225
  # Runs structural checks via Validation::StructuralValidator.
215
226
  # Use the verify CLI command for full OPC + XSD + semantic validation.
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "lutaml/model"
4
+ require_relative "../has_run_position"
4
5
 
5
6
  module Uniword
6
7
  module Wordprocessingml
@@ -9,12 +10,17 @@ module Uniword
9
10
  # Generated from OOXML schema: wordprocessingml.yml
10
11
  # Element: <w:hyperlink>
11
12
  class Hyperlink < Lutaml::Model::Serializable
13
+ include Uniword::HasRunPosition
14
+
12
15
  attribute :id, :string
13
16
  attribute :anchor, :string
14
17
  attribute :tooltip, :string
15
18
  attribute :history, Uniword::Properties::HistoryValue
16
19
  attribute :runs, Run, collection: true, initialize_empty: true
17
20
 
21
+ # Transient: position among sibling runs (used by MHTML renderer)
22
+ attribute :run_position, :integer
23
+
18
24
  xml do
19
25
  element "hyperlink"
20
26
  namespace Uniword::Ooxml::Namespaces::WordProcessingML
@@ -178,34 +178,22 @@ module Uniword
178
178
  def merge_properties(base, override)
179
179
  merged = RunProperties.new
180
180
 
181
- # Get all attribute names from RunProperties class
182
181
  RunProperties.attributes.each_key do |attr_name|
183
182
  override_val = override.public_send(attr_name)
184
183
  base_val = base.public_send(attr_name)
185
184
 
186
- # Use override if it's non-nil AND not using default value
187
- # For boolean properties like Bold, check if it was explicitly set
188
185
  use_override = if override_val.is_a?(Lutaml::Model::Serializable)
189
- # Check if the property has any non-default/non-nil values
190
186
  override_val.class.attributes.any? do |k, _|
191
- iv = override_val.instance_variable_get(:"@#{k}")
192
- iv && !override_val.using_default?(k)
187
+ !override_val.using_default?(k)
193
188
  end
194
189
  else
195
190
  !override_val.nil?
196
191
  end
197
192
 
198
- begin
199
- if use_override
200
- merged.public_send(:"#{attr_name}=", override_val)
201
- elsif base_val
202
- merged.public_send(:"#{attr_name}=", base_val)
203
- end
204
- rescue StandardError => e
205
- Uniword.logger&.debug do
206
- "Skipping attribute #{attr_name}: #{e.message}"
207
- end
208
- end
193
+ value = use_override ? override_val : base_val
194
+ next unless value
195
+
196
+ merged.public_send(:"#{attr_name}=", value)
209
197
  end
210
198
 
211
199
  merged
@@ -251,10 +251,6 @@ module Uniword
251
251
  def run_properties
252
252
  rPr
253
253
  end
254
-
255
- def to_xml(options = {})
256
- super
257
- end
258
254
  end
259
255
  end
260
256
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: uniword
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.7
4
+ version: 1.0.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-05-08 00:00:00.000000000 Z
11
+ date: 2026-05-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: logger
@@ -771,6 +771,7 @@ files:
771
771
  - lib/uniword/diff/package_diff_result.rb
772
772
  - lib/uniword/diff/package_differ.rb
773
773
  - lib/uniword/document_factory.rb
774
+ - lib/uniword/document_input.rb
774
775
  - lib/uniword/document_variables.rb
775
776
  - lib/uniword/document_variables/data_type.rb
776
777
  - lib/uniword/document_variables/default_value.rb
@@ -966,6 +967,7 @@ files:
966
967
  - lib/uniword/glossary/glossary_document.rb
967
968
  - lib/uniword/glossary/style_id.rb
968
969
  - lib/uniword/glossary/text_box.rb
970
+ - lib/uniword/has_run_position.rb
969
971
  - lib/uniword/header.rb
970
972
  - lib/uniword/headers_footers.rb
971
973
  - lib/uniword/headers_footers/manager.rb
@@ -1157,7 +1159,6 @@ files:
1157
1159
  - lib/uniword/ooxml/types/dcterms_modified_type.rb
1158
1160
  - lib/uniword/ooxml/types/dcterms_w3cdtf_type.rb
1159
1161
  - lib/uniword/ooxml/types/mc_ignorable_type.rb
1160
- - lib/uniword/ooxml/types/on_off_type.rb
1161
1162
  - lib/uniword/ooxml/types/ooxml_boolean.rb
1162
1163
  - lib/uniword/ooxml/types/ooxml_boolean_optional.rb
1163
1164
  - lib/uniword/ooxml/types/relationship_id.rb
@@ -1232,6 +1233,7 @@ files:
1232
1233
  - lib/uniword/properties/auto_space_de.rb
1233
1234
  - lib/uniword/properties/auto_space_dn.rb
1234
1235
  - lib/uniword/properties/bold.rb
1236
+ - lib/uniword/properties/boolean_element_factory.rb
1235
1237
  - lib/uniword/properties/boolean_formatting.rb
1236
1238
  - lib/uniword/properties/border.rb
1237
1239
  - lib/uniword/properties/borders.rb
@@ -1,64 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "lutaml/model"
4
-
5
- module Uniword
6
- module Ooxml
7
- module Types
8
- # OOXML On/Off (boolean) type for attributes.
9
- #
10
- # OOXML uses "0" and "1" for boolean attribute values, not "false"/"true".
11
- # When an attribute is not present (nil), it should remain nil, not be
12
- # serialized as "0". This preserves the original document structure.
13
- #
14
- # @example Serialize a boolean attribute
15
- # # Ruby true → XML "1"
16
- # # Ruby false → XML "0"
17
- # # Ruby nil → attribute not rendered
18
- class OnOffType < Lutaml::Model::Type::String
19
- # Bypass parent's initialize to avoid infinite recursion.
20
- # Parent calls self.class.cast(value) which calls self.new(value).
21
- def initialize(value)
22
- @value = value
23
- end
24
-
25
- # Cast value to OnOffType instance (for attribute serialization).
26
- # nil remains nil (not present in source), preserving original structure.
27
- # Already-wrapped OnOffType instances are returned as-is.
28
- def self.cast(value, _options = {})
29
- return nil if value.nil?
30
- return value if value.is_a?(OnOffType)
31
-
32
- raw = case value
33
- when true, "true", "1", 1
34
- true
35
- when false, "false", "0", 0
36
- false
37
- else
38
- false
39
- end
40
- new(raw)
41
- end
42
-
43
- # Convert stored boolean to OOXML "1" or "0"
44
- def to_xml
45
- return nil if @value.nil?
46
-
47
- @value == true ? "1" : "0"
48
- end
49
-
50
- # Fallback to_s also returns "1"/"0" for compatibility
51
- def to_s
52
- return "" if @value.nil?
53
-
54
- to_xml
55
- end
56
-
57
- # Return the raw boolean value for Ruby comparisons
58
- def to_bool
59
- @value == true
60
- end
61
- end
62
- end
63
- end
64
- end