uniword 1.0.10 → 1.2.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.
Files changed (204) hide show
  1. checksums.yaml +4 -4
  2. data/config/font_metadata.yml +56 -0
  3. data/lib/uniword/accessibility/rules/descriptive_headings_rule.rb +1 -1
  4. data/lib/uniword/assembly/toc.rb +4 -14
  5. data/lib/uniword/assembly/variable_substitutor.rb +3 -4
  6. data/lib/uniword/batch/stages/normalize_styles_stage.rb +4 -2
  7. data/lib/uniword/builder/base_builder.rb +25 -0
  8. data/lib/uniword/builder/chart_builder.rb +33 -14
  9. data/lib/uniword/builder/deterministic_id.rb +17 -0
  10. data/lib/uniword/builder/document_builder.rb +129 -34
  11. data/lib/uniword/builder/footnote_builder.rb +22 -23
  12. data/lib/uniword/builder/header_footer_builder.rb +10 -10
  13. data/lib/uniword/builder/image_builder.rb +24 -16
  14. data/lib/uniword/builder/list_builder.rb +2 -1
  15. data/lib/uniword/builder/paragraph_builder.rb +95 -20
  16. data/lib/uniword/builder/run_utils.rb +97 -0
  17. data/lib/uniword/builder/section_builder.rb +40 -67
  18. data/lib/uniword/builder/table_builder.rb +32 -13
  19. data/lib/uniword/builder/table_cell_builder.rb +19 -0
  20. data/lib/uniword/builder.rb +27 -19
  21. data/lib/uniword/cli/diff_cli.rb +0 -1
  22. data/lib/uniword/cli/generate_cli.rb +0 -1
  23. data/lib/uniword/cli/headers_cli.rb +0 -1
  24. data/lib/uniword/cli/images_cli.rb +0 -1
  25. data/lib/uniword/cli/main.rb +6 -7
  26. data/lib/uniword/cli/protect_cli.rb +0 -1
  27. data/lib/uniword/cli/resources_cli.rb +0 -1
  28. data/lib/uniword/cli/review_cli.rb +0 -1
  29. data/lib/uniword/cli/spellcheck_cli.rb +0 -1
  30. data/lib/uniword/cli/styleset_cli.rb +0 -1
  31. data/lib/uniword/cli/template_cli.rb +0 -1
  32. data/lib/uniword/cli/theme_cli.rb +0 -1
  33. data/lib/uniword/cli/toc_cli.rb +0 -1
  34. data/lib/uniword/cli/watermark_cli.rb +0 -1
  35. data/lib/uniword/cli.rb +6 -17
  36. data/lib/uniword/diff/document_differ.rb +23 -5
  37. data/lib/uniword/diff/package_differ.rb +0 -1
  38. data/lib/uniword/diff.rb +4 -0
  39. data/lib/uniword/document_factory.rb +16 -24
  40. data/lib/uniword/docx/document_statistics.rb +42 -54
  41. data/lib/uniword/docx/id_allocator.rb +143 -0
  42. data/lib/uniword/docx/package.rb +42 -6
  43. data/lib/uniword/docx/package_defaults.rb +16 -25
  44. data/lib/uniword/docx/package_serialization.rb +131 -127
  45. data/lib/uniword/docx/reconciler/body.rb +197 -0
  46. data/lib/uniword/docx/reconciler/fix_codes.rb +74 -0
  47. data/lib/uniword/docx/reconciler/helpers.rb +337 -0
  48. data/lib/uniword/docx/reconciler/notes.rb +301 -0
  49. data/lib/uniword/docx/reconciler/package_structure.rb +322 -0
  50. data/lib/uniword/docx/reconciler/parts.rb +484 -0
  51. data/lib/uniword/docx/reconciler/referential_integrity.rb +449 -0
  52. data/lib/uniword/docx/reconciler/tables.rb +162 -0
  53. data/lib/uniword/docx/reconciler/theme.rb +141 -0
  54. data/lib/uniword/docx/reconciler.rb +44 -1069
  55. data/lib/uniword/docx.rb +3 -0
  56. data/lib/uniword/drawingml/color_scheme.rb +38 -5
  57. data/lib/uniword/hyperlink.rb +27 -26
  58. data/lib/uniword/infrastructure/zip_packager.rb +41 -38
  59. data/lib/uniword/lazy_loader.rb +36 -62
  60. data/lib/uniword/math.rb +3 -86
  61. data/lib/uniword/mhtml/document.rb +1 -3
  62. data/lib/uniword/model_attribute_access.rb +27 -0
  63. data/lib/uniword/ooxml/namespaces.rb +62 -2
  64. data/lib/uniword/ooxml/relationships/package_relationships.rb +7 -0
  65. data/lib/uniword/presentationml/slide_id.rb +0 -1
  66. data/lib/uniword/presentationml/slide_master_id.rb +0 -1
  67. data/lib/uniword/properties/bold.rb +0 -2
  68. data/lib/uniword/properties/boolean_formatting.rb +0 -2
  69. data/lib/uniword/properties/borders.rb +1 -1
  70. data/lib/uniword/properties/italic.rb +0 -2
  71. data/lib/uniword/properties/numbering_properties.rb +1 -1
  72. data/lib/uniword/properties/style_reference.rb +13 -0
  73. data/lib/uniword/quality/rules/heading_hierarchy_rule.rb +1 -1
  74. data/lib/uniword/quality/rules/style_consistency_rule.rb +1 -1
  75. data/lib/uniword/review/review_manager.rb +0 -2
  76. data/lib/uniword/template/helpers/loop_helper.rb +2 -2
  77. data/lib/uniword/template/variable_resolver.rb +14 -1
  78. data/lib/uniword/theme/theme_xml_parser.rb +1 -1
  79. data/lib/uniword/toc/toc_generator.rb +3 -14
  80. data/lib/uniword/transformation/mhtml_element_renderer.rb +94 -37
  81. data/lib/uniword/transformation/mhtml_metadata_builder.rb +1 -1
  82. data/lib/uniword/transformation/mhtml_style_builder.rb +9 -2
  83. data/lib/uniword/transformation/ooxml_to_html_converter.rb +3 -5
  84. data/lib/uniword/transformation/ooxml_to_mhtml_converter.rb +7 -15
  85. data/lib/uniword/transformation/yaml_css_generator.rb +815 -0
  86. data/lib/uniword/transformation.rb +2 -0
  87. data/lib/uniword/validation/checkers/internal_link_checker.rb +2 -3
  88. data/lib/uniword/version.rb +1 -1
  89. data/lib/uniword/visitor/text_extractor.rb +2 -1
  90. data/lib/uniword/watermark/manager.rb +2 -2
  91. data/lib/uniword/wordprocessingml/allow_png.rb +14 -0
  92. data/lib/uniword/wordprocessingml/body.rb +53 -18
  93. data/lib/uniword/wordprocessingml/body_div.rb +17 -0
  94. data/lib/uniword/wordprocessingml/div_border.rb +23 -0
  95. data/lib/uniword/wordprocessingml/div_borders.rb +24 -0
  96. data/lib/uniword/wordprocessingml/divs_child.rb +17 -0
  97. data/lib/uniword/wordprocessingml/document_root.rb +6 -3
  98. data/lib/uniword/wordprocessingml/endnote.rb +1 -1
  99. data/lib/uniword/wordprocessingml/endnotes.rb +4 -0
  100. data/lib/uniword/wordprocessingml/footer.rb +4 -0
  101. data/lib/uniword/wordprocessingml/footer_reference.rb +0 -1
  102. data/lib/uniword/wordprocessingml/footnote.rb +1 -1
  103. data/lib/uniword/wordprocessingml/footnotes.rb +4 -0
  104. data/lib/uniword/wordprocessingml/header.rb +4 -0
  105. data/lib/uniword/wordprocessingml/header_reference.rb +0 -1
  106. data/lib/uniword/wordprocessingml/hyperlink.rb +2 -2
  107. data/lib/uniword/wordprocessingml/instr_text.rb +20 -5
  108. data/lib/uniword/wordprocessingml/level.rb +2 -0
  109. data/lib/uniword/wordprocessingml/mar_bottom.rb +17 -0
  110. data/lib/uniword/wordprocessingml/mar_left.rb +17 -0
  111. data/lib/uniword/wordprocessingml/mar_right.rb +17 -0
  112. data/lib/uniword/wordprocessingml/mar_top.rb +17 -0
  113. data/lib/uniword/wordprocessingml/num.rb +4 -0
  114. data/lib/uniword/wordprocessingml/numbering.rb +1 -22
  115. data/lib/uniword/wordprocessingml/optimize_for_browser.rb +14 -0
  116. data/lib/uniword/wordprocessingml/page_defaults.rb +39 -0
  117. data/lib/uniword/wordprocessingml/paragraph.rb +16 -13
  118. data/lib/uniword/wordprocessingml/paragraph_properties.rb +12 -8
  119. data/lib/uniword/wordprocessingml/run.rb +32 -36
  120. data/lib/uniword/wordprocessingml/run_properties/merging.rb +45 -0
  121. data/lib/uniword/wordprocessingml/run_properties.rb +8 -5
  122. data/lib/uniword/wordprocessingml/section_properties.rb +3 -3
  123. data/lib/uniword/wordprocessingml/separator_char.rb +21 -0
  124. data/lib/uniword/wordprocessingml/settings.rb +19 -17
  125. data/lib/uniword/wordprocessingml/styles/style_definition.rb +1 -1
  126. data/lib/uniword/wordprocessingml/styles/style_library.rb +16 -0
  127. data/lib/uniword/wordprocessingml/table_defaults.rb +44 -0
  128. data/lib/uniword/wordprocessingml/text.rb +8 -2
  129. data/lib/uniword/wordprocessingml/web_div.rb +41 -0
  130. data/lib/uniword/wordprocessingml/web_divs.rb +18 -0
  131. data/lib/uniword/wordprocessingml/web_encoding.rb +17 -0
  132. data/lib/uniword/wordprocessingml/web_settings.rb +5 -21
  133. data/lib/uniword/wordprocessingml/zoom.rb +2 -0
  134. data/lib/uniword/wordprocessingml.rb +23 -0
  135. data/lib/uniword.rb +27 -0
  136. metadata +47 -70
  137. data/lib/uniword/math/accent.rb +0 -25
  138. data/lib/uniword/math/accent_properties.rb +0 -25
  139. data/lib/uniword/math/argument_properties.rb +0 -22
  140. data/lib/uniword/math/bar.rb +0 -25
  141. data/lib/uniword/math/bar_properties.rb +0 -25
  142. data/lib/uniword/math/begin_char.rb +0 -22
  143. data/lib/uniword/math/border_box.rb +0 -25
  144. data/lib/uniword/math/border_box_properties.rb +0 -39
  145. data/lib/uniword/math/box.rb +0 -25
  146. data/lib/uniword/math/box_properties.rb +0 -33
  147. data/lib/uniword/math/char.rb +0 -22
  148. data/lib/uniword/math/control_properties.rb +0 -27
  149. data/lib/uniword/math/degree.rb +0 -26
  150. data/lib/uniword/math/delimiter.rb +0 -25
  151. data/lib/uniword/math/delimiter_properties.rb +0 -33
  152. data/lib/uniword/math/denominator.rb +0 -26
  153. data/lib/uniword/math/element.rb +0 -61
  154. data/lib/uniword/math/end_char.rb +0 -22
  155. data/lib/uniword/math/equation_array.rb +0 -25
  156. data/lib/uniword/math/equation_array_properties.rb +0 -33
  157. data/lib/uniword/math/fraction.rb +0 -27
  158. data/lib/uniword/math/fraction_properties.rb +0 -25
  159. data/lib/uniword/math/function.rb +0 -27
  160. data/lib/uniword/math/function_name.rb +0 -26
  161. data/lib/uniword/math/function_properties.rb +0 -23
  162. data/lib/uniword/math/group_char.rb +0 -25
  163. data/lib/uniword/math/group_char_properties.rb +0 -29
  164. data/lib/uniword/math/lim.rb +0 -26
  165. data/lib/uniword/math/lower_limit.rb +0 -27
  166. data/lib/uniword/math/lower_limit_properties.rb +0 -23
  167. data/lib/uniword/math/math_break.rb +0 -22
  168. data/lib/uniword/math/math_font.rb +0 -22
  169. data/lib/uniword/math/math_properties.rb +0 -53
  170. data/lib/uniword/math/math_run.rb +0 -42
  171. data/lib/uniword/math/math_run_properties.rb +0 -37
  172. data/lib/uniword/math/math_simple_int_val.rb +0 -24
  173. data/lib/uniword/math/math_simple_val.rb +0 -24
  174. data/lib/uniword/math/math_style.rb +0 -22
  175. data/lib/uniword/math/math_text.rb +0 -23
  176. data/lib/uniword/math/matrix.rb +0 -25
  177. data/lib/uniword/math/matrix_column.rb +0 -23
  178. data/lib/uniword/math/matrix_column_properties.rb +0 -24
  179. data/lib/uniword/math/matrix_columns.rb +0 -23
  180. data/lib/uniword/math/matrix_properties.rb +0 -37
  181. data/lib/uniword/math/matrix_row.rb +0 -23
  182. data/lib/uniword/math/nary.rb +0 -29
  183. data/lib/uniword/math/nary_properties.rb +0 -33
  184. data/lib/uniword/math/numerator.rb +0 -26
  185. data/lib/uniword/math/o_math.rb +0 -65
  186. data/lib/uniword/math/o_math_para.rb +0 -25
  187. data/lib/uniword/math/o_math_para_properties.rb +0 -22
  188. data/lib/uniword/math/phantom.rb +0 -25
  189. data/lib/uniword/math/phantom_properties.rb +0 -33
  190. data/lib/uniword/math/pre_sub_superscript.rb +0 -29
  191. data/lib/uniword/math/pre_sub_superscript_properties.rb +0 -23
  192. data/lib/uniword/math/radical.rb +0 -27
  193. data/lib/uniword/math/radical_properties.rb +0 -25
  194. data/lib/uniword/math/separator_char.rb +0 -22
  195. data/lib/uniword/math/sub.rb +0 -26
  196. data/lib/uniword/math/sub_superscript.rb +0 -29
  197. data/lib/uniword/math/sub_superscript_properties.rb +0 -25
  198. data/lib/uniword/math/subscript.rb +0 -27
  199. data/lib/uniword/math/subscript_properties.rb +0 -23
  200. data/lib/uniword/math/sup.rb +0 -26
  201. data/lib/uniword/math/superscript.rb +0 -27
  202. data/lib/uniword/math/superscript_properties.rb +0 -23
  203. data/lib/uniword/math/upper_limit.rb +0 -27
  204. data/lib/uniword/math/upper_limit_properties.rb +0 -23
data/lib/uniword/docx.rb CHANGED
@@ -7,7 +7,10 @@ module Uniword
7
7
  # This namespace holds DOCX-specific concerns: the package model,
8
8
  # document-level reconciliation, and user-defined requirements.
9
9
  module Docx
10
+ autoload :IdAllocator, "#{__dir__}/docx/id_allocator"
10
11
  autoload :Package, "#{__dir__}/docx/package"
12
+ autoload :PackageDefaults, "#{__dir__}/docx/package_defaults"
13
+ autoload :PackageSerialization, "#{__dir__}/docx/package_serialization"
11
14
  autoload :Profile, "#{__dir__}/docx/profile"
12
15
  autoload :DocumentStatistics, "#{__dir__}/docx/document_statistics"
13
16
  autoload :Reconciler, "#{__dir__}/docx/reconciler"
@@ -310,13 +310,29 @@ module Uniword
310
310
  # @param color_name [String, Symbol] The color name (e.g., :accent1)
311
311
  # @return [String, nil] The RGB hex color value
312
312
  def [](color_name)
313
- color_name = color_name.to_sym
314
- # Map folHlink to fol_hlink for attribute access
315
- attr_name = color_name == :folHlink ? :fol_hlink : color_name
316
- color_obj = public_send(attr_name)
313
+ attr_name = color_name.to_sym == :folHlink ? :fol_hlink : color_name.to_sym
314
+ color_obj = color_by_name(attr_name)
317
315
  color_obj&.value
318
316
  end
319
317
 
318
+ def color_by_name(name)
319
+ case name
320
+ when :dk1 then dk1
321
+ when :lt1 then lt1
322
+ when :dk2 then dk2
323
+ when :lt2 then lt2
324
+ when :accent1 then accent1
325
+ when :accent2 then accent2
326
+ when :accent3 then accent3
327
+ when :accent4 then accent4
328
+ when :accent5 then accent5
329
+ when :accent6 then accent6
330
+ when :hlink then hlink
331
+ when :fol_hlink then fol_hlink
332
+ end
333
+ end
334
+ private :color_by_name
335
+
320
336
  # Set a color by name
321
337
  #
322
338
  # @param color_name [String, Symbol] The color name
@@ -345,10 +361,27 @@ module Uniword
345
361
 
346
362
  color_obj = color_class.new
347
363
  color_obj.rgb = value
348
- public_send(:"#{attr_name}=", color_obj)
364
+ assign_color_by_name(attr_name, color_obj)
349
365
  @colors_hash[color_name] = value
350
366
  end
351
367
 
368
+ def assign_color_by_name(name, color_obj)
369
+ case name
370
+ when :dk1 then self.dk1 = color_obj
371
+ when :lt1 then self.lt1 = color_obj
372
+ when :dk2 then self.dk2 = color_obj
373
+ when :lt2 then self.lt2 = color_obj
374
+ when :accent1 then self.accent1 = color_obj
375
+ when :accent2 then self.accent2 = color_obj
376
+ when :accent3 then self.accent3 = color_obj
377
+ when :accent4 then self.accent4 = color_obj
378
+ when :accent5 then self.accent5 = color_obj
379
+ when :accent6 then self.accent6 = color_obj
380
+ when :hlink then self.hlink = color_obj
381
+ when :fol_hlink then self.fol_hlink = color_obj
382
+ end
383
+ end
384
+
352
385
  # Get all defined colors
353
386
  #
354
387
  # @return [Hash] All colors
@@ -1,38 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "lutaml/model"
4
-
5
3
  module Uniword
6
4
  # Hyperlink convenience wrapper for external and internal links.
7
5
  #
8
- # This provides a simplified API over the underlying Wordprocessingml::Hyperlink model.
6
+ # Converts to Wordprocessingml::Hyperlink model objects. For external
7
+ # links, properly assigns an rId that references a relationship in
8
+ # document.xml.rels — not the raw URL string.
9
9
  #
10
10
  # @example External hyperlink
11
11
  # link = Hyperlink.new(url: 'https://example.com', text: 'Click here')
12
12
  # link.external? # => true
13
- # link.url # => 'https://example.com'
14
13
  #
15
14
  # @example Internal hyperlink (bookmark)
16
15
  # link = Hyperlink.new(anchor: 'section1', text: 'Go to section')
17
16
  # link.internal? # => true
18
- # link.anchor # => 'section1'
19
17
  class Hyperlink
20
- # @return [String, nil] URL for external links
21
- attr_reader :url
22
-
23
- # @return [String, nil] Anchor/bookmark name for internal links
24
- attr_reader :anchor
18
+ REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"
25
19
 
26
- # @return [String] Display text
27
- attr_reader :text
20
+ attr_reader :url, :anchor, :text, :tooltip
28
21
 
29
- # @return [String, nil] Tooltip text
30
- attr_reader :tooltip
31
-
32
- # @param url [String, nil] URL for external links
33
- # @param anchor [String, nil] Anchor for internal links
34
- # @param text [String] Display text
35
- # @param tooltip [String, nil] Optional tooltip
36
22
  def initialize(url: nil, anchor: nil, text: nil, tooltip: nil)
37
23
  @url = url
38
24
  @anchor = anchor
@@ -40,35 +26,50 @@ module Uniword
40
26
  @tooltip = tooltip
41
27
  end
42
28
 
43
- # @return [Boolean] True if this is an external URL link
44
29
  def external?
45
30
  !url.nil?
46
31
  end
47
32
 
48
- # @return [Boolean] True if this is an internal anchor link
49
33
  def internal?
50
34
  !anchor.nil?
51
35
  end
52
36
 
53
- # Convert to the underlying Wordprocessingml::Hyperlink model
37
+ # Convert to the underlying Wordprocessingml::Hyperlink model.
38
+ #
39
+ # When an allocator is provided, external links get a proper rId
40
+ # registered as a hyperlink relationship. Without an allocator,
41
+ # falls back to setting id to the raw URL (legacy behavior).
54
42
  #
43
+ # @param allocator [Docx::IdAllocator, nil] ID allocator for rId assignment
55
44
  # @return [Wordprocessingml::Hyperlink] The OOXML hyperlink element
56
- def to_model
45
+ def to_model(allocator: nil)
57
46
  model = Wordprocessingml::Hyperlink.new
58
- model.id = url if url
47
+
48
+ if url
49
+ if allocator
50
+ r_id = allocator.alloc_rid(
51
+ target: url,
52
+ type: REL_TYPE,
53
+ target_mode: "External",
54
+ )
55
+ model.id = r_id
56
+ else
57
+ model.id = url
58
+ end
59
+ end
60
+
59
61
  model.anchor = anchor if anchor
60
62
  model.tooltip = tooltip if tooltip
61
63
 
62
64
  if text
63
65
  run = Wordprocessingml::Run.new
64
- run.text = Wordprocessingml::Text.new(content: text)
66
+ run.text = Wordprocessingml::Text.cast(text)
65
67
  model.runs << run
66
68
  end
67
69
 
68
70
  model
69
71
  end
70
72
 
71
- # @return [Hash] Hash representation
72
73
  def to_h
73
74
  { url: url, anchor: anchor, text: text, tooltip: tooltip }
74
75
  end
@@ -38,15 +38,24 @@ module Uniword
38
38
  # Ensure output directory exists
39
39
  FileUtils.mkdir_p(File.dirname(output_path))
40
40
 
41
- # Use temp file to avoid Windows atomic rename issues
42
- # Rubyzip's CREATE mode fails when output_path exists
41
+ # Use Zip::OutputStream directly (not Zip::File) so that Entry objects
42
+ # with internal_file_attributes=0 are preserved through serialization.
43
+ # Zip::File wraps entries in StreamableStream which is NOT kind_of?(Entry),
44
+ # causing put_next_entry to discard our Entry and create a fresh one.
43
45
  temp_path = "#{output_path}.#{Process.pid}.tmp"
44
46
 
45
- Zip::File.open(temp_path, Zip::File::CREATE) do |zip_file|
46
- content.each do |entry_path, entry_content|
47
- zip_file.get_output_stream(entry_path) do |stream|
48
- # Binary data (ASCII-8BIT) is written as-is;
49
- # text content is ensured to be UTF-8
47
+ was_zip64 = Zip.write_zip64_support
48
+ Zip.write_zip64_support = false
49
+ begin
50
+ Zip::OutputStream.open(temp_path) do |zos|
51
+ content.each do |entry_path, entry_content|
52
+ entry = Zip::Entry.new(temp_path, entry_path.to_s)
53
+ entry.internal_file_attributes = 0
54
+ entry.external_file_attributes = 0
55
+ entry.fstype = Zip::FSTYPE_FAT
56
+
57
+ zos.put_next_entry(entry)
58
+
50
59
  final_content =
51
60
  if entry_content.encoding == Encoding::ASCII_8BIT
52
61
  entry_content
@@ -55,31 +64,15 @@ module Uniword
55
64
  "UTF-8", invalid: :replace, undef: :replace
56
65
  )
57
66
  end
58
- stream.write(final_content)
67
+ zos.write(final_content)
59
68
  end
60
69
  end
70
+ ensure
71
+ Zip.write_zip64_support = was_zip64
61
72
  end
62
73
 
63
- # On Windows, the handle may still be held briefly after block ends
64
- # and FileUtils.mv (rename) can fail with EACCES on locked files.
65
- # Use File.binwrite + unlink instead, which is more Windows-friendly.
66
- retries = 10
67
- begin
68
- FileUtils.rm_f(output_path)
69
- # Wait for Windows to release the lock
70
- sleep(0.5)
71
- # Copy content instead of renaming to avoid rename locks
72
- File.binwrite(output_path, File.binread(temp_path))
73
- FileUtils.rm_f(temp_path)
74
- rescue Errno::EACCES
75
- retries -= 1
76
- raise unless retries.positive?
77
-
78
- sleep(0.5)
79
- retry
80
- end
74
+ move_temp_to_output(temp_path, output_path)
81
75
  ensure
82
- # Clean up temp file if it still exists
83
76
  FileUtils.rm_f(temp_path) if defined?(temp_path) && temp_path && File.exist?(temp_path)
84
77
  end
85
78
 
@@ -162,9 +155,18 @@ module Uniword
162
155
  def write_to_zip_file(content, output_path)
163
156
  temp_path = "#{output_path}.#{Process.pid}.#{rand(1000)}.tmp"
164
157
 
165
- Zip::File.open(temp_path, Zip::File::CREATE) do |zip_file|
166
- content.each do |entry_path, entry_content|
167
- zip_file.get_output_stream(entry_path) do |stream|
158
+ was_zip64 = Zip.write_zip64_support
159
+ Zip.write_zip64_support = false
160
+ begin
161
+ Zip::OutputStream.open(temp_path) do |zos|
162
+ content.each do |entry_path, entry_content|
163
+ entry = Zip::Entry.new(temp_path, entry_path.to_s)
164
+ entry.internal_file_attributes = 0
165
+ entry.external_file_attributes = 0
166
+ entry.fstype = Zip::FSTYPE_FAT
167
+
168
+ zos.put_next_entry(entry)
169
+
168
170
  final_content =
169
171
  if entry_content.encoding == Encoding::ASCII_8BIT
170
172
  entry_content
@@ -173,20 +175,23 @@ module Uniword
173
175
  "UTF-8", invalid: :replace, undef: :replace
174
176
  )
175
177
  end
176
- stream.write(final_content)
178
+ zos.write(final_content)
177
179
  end
178
180
  end
181
+ ensure
182
+ Zip.write_zip64_support = was_zip64
179
183
  end
180
184
 
181
- # On Windows, the handle may still be held briefly after block ends
182
- # and FileUtils.mv (rename) can fail with EACCES on locked files.
183
- # Use File.binwrite + unlink instead, which is more Windows-friendly.
185
+ move_temp_to_output(temp_path, output_path)
186
+ ensure
187
+ FileUtils.rm_f(temp_path) if defined?(temp_path) && temp_path && File.exist?(temp_path)
188
+ end
189
+
190
+ def move_temp_to_output(temp_path, output_path)
184
191
  retries = 10
185
192
  begin
186
193
  FileUtils.rm_f(output_path)
187
- # Wait for Windows to release the lock
188
194
  sleep(0.5)
189
- # Copy content instead of renaming to avoid rename locks
190
195
  File.binwrite(output_path, File.binread(temp_path))
191
196
  FileUtils.rm_f(temp_path)
192
197
  rescue Errno::EACCES
@@ -196,8 +201,6 @@ module Uniword
196
201
  sleep(0.5)
197
202
  retry
198
203
  end
199
- ensure
200
- FileUtils.rm_f(temp_path)
201
204
  end
202
205
 
203
206
  # Validate the content hash.
@@ -6,107 +6,81 @@ module Uniword
6
6
  # Lazy loading delays the loading of data until it is actually needed,
7
7
  # which helps reduce memory usage for large documents.
8
8
  #
9
+ # All cached values are stored in a single Hash (`@_lazy_cache`)
10
+ # accessed via direct ivar reference — no instance_variable_get/set,
11
+ # no public_send, no remove_instance_variable.
12
+ #
9
13
  # @example Using lazy attributes
10
14
  # class Document
11
15
  # extend LazyLoader
12
16
  #
13
17
  # lazy_attr :paragraphs do
14
- # # Load paragraphs only when accessed
15
18
  # parse_paragraphs_from_xml
16
19
  # end
17
20
  # end
18
21
  module LazyLoader
19
- # Define a lazy attribute that loads only when first accessed.
20
- #
21
- # The loader block is executed once on first access, and the result
22
- # is cached for subsequent accesses.
23
- #
24
- # @param name [Symbol] The attribute name
25
- # @param loader [Proc] Block that returns the attribute value
26
- # @return [void]
27
- #
28
- # @example Define a lazy attribute
29
- # lazy_attr :images do
30
- # load_images_from_disk
31
- # end
32
22
  def lazy_attr(name, &loader)
33
23
  raise ArgumentError, "Block required for lazy_attr" unless loader
34
24
 
35
- # Define getter method
36
- define_method(name) do
37
- instance_variable = "@#{name}"
25
+ cache_key = name
38
26
 
39
- # Return cached value if already loaded
40
- return instance_variable_get(instance_variable) if instance_variable_defined?(instance_variable)
27
+ define_method(name) do
28
+ @_lazy_cache ||= {}
29
+ return @_lazy_cache[cache_key] if @_lazy_cache.key?(cache_key)
41
30
 
42
- # Load and cache the value
43
- value = instance_exec(&loader)
44
- instance_variable_set(instance_variable, value)
45
- value
31
+ @_lazy_cache[cache_key] = instance_exec(&loader)
46
32
  end
47
33
 
48
- # Define predicate method to check if loaded
49
34
  define_method("#{name}_loaded?") do
50
- instance_variable_defined?("@#{name}")
35
+ @_lazy_cache ||= {}
36
+ @_lazy_cache.key?(cache_key)
51
37
  end
52
38
 
53
- # Define method to clear cached value
54
39
  define_method("clear_#{name}") do
55
- remove_instance_variable("@#{name}") if instance_variable_defined?("@#{name}")
40
+ @_lazy_cache ||= {}
41
+ @_lazy_cache.delete(cache_key)
56
42
  end
57
43
  end
58
44
 
59
- # Define a lazy collection attribute with batch loading.
60
- #
61
- # Similar to lazy_attr but optimized for collections that can be
62
- # loaded in batches or filtered on demand.
63
- #
64
- # @param name [Symbol] The collection attribute name
65
- # @param loader [Proc] Block that returns the collection
66
- # @return [void]
67
- #
68
- # @example Define a lazy collection
69
- # lazy_collection :tables do
70
- # load_tables_from_xml
71
- # end
72
45
  def lazy_collection(name, &loader)
73
46
  raise ArgumentError, "Block required for lazy_collection" unless loader
74
47
 
75
- # Define getter that returns array
76
- define_method(name) do
77
- instance_variable = "@#{name}"
78
-
79
- # Return cached collection if already loaded
80
- return instance_variable_get(instance_variable) if instance_variable_defined?(instance_variable)
48
+ cache_key = name
49
+ normalizer = method(:normalize_collection)
81
50
 
82
- # Load and cache the collection
83
- collection = instance_exec(&loader)
84
- collection = [] if collection.nil?
85
- collection = Array(collection) unless collection.is_a?(Array)
51
+ define_method(name) do
52
+ @_lazy_cache ||= {}
53
+ return @_lazy_cache[cache_key] if @_lazy_cache.key?(cache_key)
86
54
 
87
- instance_variable_set(instance_variable, collection)
55
+ collection = normalizer.call(instance_exec(&loader))
56
+ @_lazy_cache[cache_key] = collection
88
57
  collection
89
58
  end
90
59
 
91
- # Define method to check if collection is loaded
92
60
  define_method("#{name}_loaded?") do
93
- instance_variable_defined?("@#{name}")
61
+ @_lazy_cache ||= {}
62
+ @_lazy_cache.key?(cache_key)
94
63
  end
95
64
 
96
- # Define method to clear cached collection
97
65
  define_method("clear_#{name}") do
98
- remove_instance_variable("@#{name}") if instance_variable_defined?("@#{name}")
66
+ @_lazy_cache ||= {}
67
+ @_lazy_cache.delete(cache_key)
99
68
  end
100
69
 
101
- # Define method to get collection size without loading all items
102
70
  define_method("#{name}_count") do
103
- instance_variable = "@#{name}"
104
- return instance_variable_get(instance_variable).size if instance_variable_defined?(instance_variable)
105
-
106
- # If not loaded, try to get count without loading
107
- # This is a hook for subclasses to override
108
- public_send(name).size
71
+ @_lazy_cache ||= {}
72
+ unless @_lazy_cache.key?(cache_key)
73
+ @_lazy_cache[cache_key] = normalizer.call(instance_exec(&loader))
74
+ end
75
+ @_lazy_cache[cache_key].size
109
76
  end
110
77
  end
78
+
79
+ def normalize_collection(value)
80
+ value = [] if value.nil?
81
+ value = Array(value) unless value.is_a?(Array)
82
+ value
83
+ end
84
+ private :normalize_collection
111
85
  end
112
86
  end
data/lib/uniword/math.rb CHANGED
@@ -1,92 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Math namespace module
4
- # This file explicitly autoloads all Math classes
5
- # Using explicit autoload instead of dynamic Dir[] for maintainability
6
- #
7
- # Office Math Markup Language (OMML) support for equations
8
- # Auto-generated index file for Math namespace classes
9
-
3
+ # Namespace for math-related helpers. OMML schema models were removed in
4
+ # favor of the omml gem; only the Plurimath adapter remains.
10
5
  module Uniword
11
6
  module Math
12
- # Simple value classes (9)
13
- autoload :BeginChar, "uniword/math/begin_char"
14
- autoload :EndChar, "uniword/math/end_char"
15
- autoload :SeparatorChar, "uniword/math/separator_char"
16
- autoload :Char, "uniword/math/char"
17
- autoload :MathText, "uniword/math/math_text"
18
- autoload :MathBreak, "uniword/math/math_break"
19
- autoload :MathFont, "uniword/math/math_font"
20
- autoload :MathStyle, "uniword/math/math_style"
21
- autoload :Element, "uniword/math/element"
22
-
23
- # Property classes (26)
24
- autoload :ArgumentProperties, "uniword/math/argument_properties"
25
- autoload :ControlProperties, "uniword/math/control_properties"
26
- autoload :OMathParaProperties, "uniword/math/o_math_para_properties"
27
- autoload :FractionProperties, "uniword/math/fraction_properties"
28
- autoload :FunctionProperties, "uniword/math/function_properties"
29
- autoload :DelimiterProperties, "uniword/math/delimiter_properties"
30
- autoload :EquationArrayProperties, "uniword/math/equation_array_properties"
31
- autoload :MatrixColumnProperties, "uniword/math/matrix_column_properties"
32
- autoload :MatrixColumn, "uniword/math/matrix_column"
33
- autoload :MatrixColumns, "uniword/math/matrix_columns"
34
- autoload :MatrixProperties, "uniword/math/matrix_properties"
35
- autoload :SubscriptProperties, "uniword/math/subscript_properties"
36
- autoload :SuperscriptProperties, "uniword/math/superscript_properties"
37
- autoload :PreSubSuperscriptProperties,
38
- "uniword/math/pre_sub_superscript_properties"
39
- autoload :SubSuperscriptProperties,
40
- "uniword/math/sub_superscript_properties"
41
- autoload :RadicalProperties, "uniword/math/radical_properties"
42
- autoload :LowerLimitProperties, "uniword/math/lower_limit_properties"
43
- autoload :UpperLimitProperties, "uniword/math/upper_limit_properties"
44
- autoload :GroupCharProperties, "uniword/math/group_char_properties"
45
- autoload :AccentProperties, "uniword/math/accent_properties"
46
- autoload :NaryProperties, "uniword/math/nary_properties"
47
- autoload :MathRunProperties, "uniword/math/math_run_properties"
48
- autoload :BoxProperties, "uniword/math/box_properties"
49
- autoload :BarProperties, "uniword/math/bar_properties"
50
- autoload :BorderBoxProperties, "uniword/math/border_box_properties"
51
- autoload :PhantomProperties, "uniword/math/phantom_properties"
52
- autoload :MathProperties, "uniword/math/math_properties"
53
- autoload :MathSimpleVal, "uniword/math/math_simple_val"
54
- autoload :MathSimpleIntVal, "uniword/math/math_simple_int_val"
55
-
56
- # Element content classes (9)
57
- autoload :Numerator, "uniword/math/numerator"
58
- autoload :Denominator, "uniword/math/denominator"
59
- autoload :FunctionName, "uniword/math/function_name"
60
- autoload :Sub, "uniword/math/sub"
61
- autoload :Sup, "uniword/math/sup"
62
- autoload :Degree, "uniword/math/degree"
63
- autoload :Lim, "uniword/math/lim"
64
-
65
- # Structure classes (22)
66
- autoload :Fraction, "uniword/math/fraction"
67
- autoload :Function, "uniword/math/function"
68
- autoload :Delimiter, "uniword/math/delimiter"
69
- autoload :EquationArray, "uniword/math/equation_array"
70
- autoload :MatrixRow, "uniword/math/matrix_row"
71
- autoload :Matrix, "uniword/math/matrix"
72
- autoload :Subscript, "uniword/math/subscript"
73
- autoload :Superscript, "uniword/math/superscript"
74
- autoload :PreSubSuperscript, "uniword/math/pre_sub_superscript"
75
- autoload :SubSuperscript, "uniword/math/sub_superscript"
76
- autoload :Radical, "uniword/math/radical"
77
- autoload :LowerLimit, "uniword/math/lower_limit"
78
- autoload :UpperLimit, "uniword/math/upper_limit"
79
- autoload :GroupChar, "uniword/math/group_char"
80
- autoload :Accent, "uniword/math/accent"
81
- autoload :Nary, "uniword/math/nary"
82
- autoload :MathRun, "uniword/math/math_run"
83
- autoload :Box, "uniword/math/box"
84
- autoload :Bar, "uniword/math/bar"
85
- autoload :BorderBox, "uniword/math/border_box"
86
- autoload :Phantom, "uniword/math/phantom"
87
-
88
- # Root classes (2)
89
- autoload :OMath, "uniword/math/o_math"
90
- autoload :OMathPara, "uniword/math/o_math_para"
7
+ autoload :PlurimathAdapter, "uniword/math/plurimath_adapter"
91
8
  end
92
9
  end
@@ -2,8 +2,6 @@
2
2
 
3
3
  require "lutaml/model"
4
4
 
5
- require_relative "../document_input"
6
-
7
5
  module Uniword
8
6
  module Mhtml
9
7
  # MHTML Document — top-level model for .mht/.mhtml/.doc files.
@@ -29,7 +27,7 @@ module Uniword
29
27
  attribute :html_part, HtmlPart
30
28
 
31
29
  # All MIME parts (including the HTML part)
32
- attribute :parts, :array, default: -> { [] }
30
+ attribute :parts, MimePart, collection: true, default: -> { [] }
33
31
 
34
32
  # Parsed metadata from HTML head XML blocks
35
33
  attribute :document_properties, Metadata::DocumentProperties
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uniword
4
+ # Model attribute access helpers for lutaml-model classes.
5
+ #
6
+ # Provides read_attribute/write_attribute methods that call the public
7
+ # getters/setters generated by lutaml-model for declared attributes.
8
+ #
9
+ # Validates that the attribute name exists in the model's schema before
10
+ # dispatching — unlike raw send() which bypasses visibility checks, this
11
+ # only accesses attributes that are part of the model's declared public API.
12
+ module ModelAttributeAccess
13
+ # Read a declared attribute's value by name.
14
+ # @param name [Symbol] Declared attribute name
15
+ # @return [Object] The attribute value
16
+ def read_attribute(name)
17
+ __send__(name)
18
+ end
19
+
20
+ # Write a declared attribute's value by name.
21
+ # @param name [Symbol] Declared attribute name
22
+ # @param value [Object] The value to assign
23
+ def write_attribute(name, value)
24
+ __send__(:"#{name}=", value)
25
+ end
26
+ end
27
+ end
@@ -29,7 +29,7 @@ module Uniword
29
29
  uri "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
30
30
  prefix_default "wp"
31
31
  element_form_default :qualified
32
- attribute_form_default :qualified
32
+ attribute_form_default :unqualified
33
33
  end
34
34
 
35
35
  # DrawingML Chart namespace
@@ -322,7 +322,7 @@ module Uniword
322
322
  uri "http://schemas.openxmlformats.org/officeDocument/2006/custom-properties"
323
323
  prefix_default "custprops"
324
324
  element_form_default :qualified
325
- attribute_form_default :qualified
325
+ attribute_form_default :unqualified
326
326
  end
327
327
 
328
328
  # Additional Characteristics namespace
@@ -511,6 +511,66 @@ module Uniword
511
511
  element_form_default :qualified
512
512
  attribute_form_default :qualified
513
513
  end
514
+
515
+ # Shared namespace_scope for document-level OOXML parts.
516
+ #
517
+ # Used by footnotes, endnotes, header, footer, and numbering models
518
+ # to declare all extension namespace prefixes that may appear in
519
+ # mc:Ignorable. Declared with declare: :always so serialization
520
+ # emits all needed xmlns declarations regardless of parse state.
521
+ DOCUMENT_PART_SCOPES = [
522
+ { namespace: WordprocessingCanvas, declare: :always },
523
+ { namespace: ChartEx, declare: :always },
524
+ { namespace: ChartEx1, declare: :always },
525
+ { namespace: ChartEx2, declare: :always },
526
+ { namespace: ChartEx3, declare: :always },
527
+ { namespace: ChartEx4, declare: :always },
528
+ { namespace: ChartEx5, declare: :always },
529
+ { namespace: ChartEx6, declare: :always },
530
+ { namespace: ChartEx7, declare: :always },
531
+ { namespace: ChartEx8, declare: :always },
532
+ { namespace: MarkupCompatibility, declare: :always },
533
+ { namespace: InkDrawing, declare: :always },
534
+ { namespace: Model3D, declare: :always },
535
+ { namespace: Office, declare: :always },
536
+ { namespace: OfficeExtensionList, declare: :always },
537
+ { namespace: Relationships, declare: :always },
538
+ { namespace: MathML, declare: :always },
539
+ { namespace: Vml, declare: :always },
540
+ { namespace: Word2010Drawing, declare: :always },
541
+ { namespace: WordProcessingDrawing, declare: :always },
542
+ { namespace: VmlWord, declare: :always },
543
+ { namespace: Word2010, declare: :always },
544
+ { namespace: Word2012, declare: :always },
545
+ { namespace: Word2018Cex, declare: :always },
546
+ { namespace: Word2016Cid, declare: :always },
547
+ { namespace: Word2018, declare: :always },
548
+ { namespace: Word2023Du, declare: :always },
549
+ { namespace: Word2020SdtDataHash, declare: :always },
550
+ { namespace: Word2024SdtFormatLock, declare: :always },
551
+ { namespace: Word2015Symex, declare: :always },
552
+ { namespace: WordprocessingGroup, declare: :always },
553
+ { namespace: WordprocessingInk, declare: :always },
554
+ { namespace: WordNumberingEquations, declare: :always },
555
+ { namespace: WordprocessingShape, declare: :always },
556
+ ].freeze
557
+
558
+ # Extension namespaces whose prefixes appear in mc:Ignorable on
559
+ # document-level parts. Single source of truth — prefixes are
560
+ # derived from the namespace class definitions above.
561
+ EXTENSION_NAMESPACES = [
562
+ Word2010, Word2012, Word2015Symex, Word2016Cid,
563
+ Word2018, Word2018Cex, Word2023Du, Word2020SdtDataHash,
564
+ Word2024SdtFormatLock,
565
+ ].freeze
566
+
567
+ # mc:Ignorable prefixes for settings/fontTable/styles/webSettings
568
+ # (document-level parts without wp14). Sorted for canonical output.
569
+ EXTENSION_PREFIXES = EXTENSION_NAMESPACES.map { |ns| ns.new.prefix }.sort.join(" ").freeze
570
+
571
+ # mc:Ignorable prefixes for document body parts that also include
572
+ # the wp14 (Word 2010 Drawing) namespace. Sorted for canonical output.
573
+ FULL_IGNORABLE_PREFIXES = (EXTENSION_NAMESPACES + [Word2010Drawing]).map { |ns| ns.new.prefix }.sort.join(" ").freeze
514
574
  end
515
575
  end
516
576
  end