uniword 1.2.5 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +313 -0
  3. data/CONTRIBUTING.md +1 -1
  4. data/config/ooxml/schemas/shared_types.yml +5 -4
  5. data/data/schemas/ecma/dc.xsd +118 -0
  6. data/data/schemas/ecma/dcmitype.xsd +50 -0
  7. data/data/schemas/ecma/dcterms.xsd +331 -0
  8. data/data/schemas/ecma/opc-coreProperties.xsd +2 -2
  9. data/data/schemas/ecma/xml.xsd +117 -0
  10. data/lib/uniword/builder/bibliography_builder.rb +1 -1
  11. data/lib/uniword/builder/chart_builder.rb +22 -14
  12. data/lib/uniword/builder/comment_anchorer.rb +221 -0
  13. data/lib/uniword/builder/document_builder.rb +97 -6
  14. data/lib/uniword/builder/image_builder.rb +11 -10
  15. data/lib/uniword/builder/run_utils.rb +2 -0
  16. data/lib/uniword/builder.rb +1 -0
  17. data/lib/uniword/cli/fonts_cli.rb +52 -0
  18. data/lib/uniword/cli/main.rb +110 -11
  19. data/lib/uniword/cli/page_cli.rb +85 -0
  20. data/lib/uniword/cli/styles_cli.rb +204 -0
  21. data/lib/uniword/cli/theme_cli.rb +88 -0
  22. data/lib/uniword/cli/toc_cli.rb +22 -2
  23. data/lib/uniword/comment.rb +20 -15
  24. data/lib/uniword/comment_range.rb +3 -3
  25. data/lib/uniword/comments_part.rb +51 -19
  26. data/lib/uniword/configuration/configuration_loader.rb +5 -3
  27. data/lib/uniword/configuration.rb +93 -1
  28. data/lib/uniword/content_types.rb +19 -39
  29. data/lib/uniword/document_factory.rb +12 -18
  30. data/lib/uniword/document_writer.rb +16 -5
  31. data/lib/uniword/docx/chart_part.rb +34 -0
  32. data/lib/uniword/docx/custom_xml_item.rb +91 -0
  33. data/lib/uniword/docx/header_footer_part.rb +140 -0
  34. data/lib/uniword/docx/header_footer_part_collection.rb +135 -0
  35. data/lib/uniword/docx/header_footer_view.rb +225 -0
  36. data/lib/uniword/docx/id_allocator.rb +130 -36
  37. data/lib/uniword/docx/image_part.rb +83 -0
  38. data/lib/uniword/docx/package.rb +141 -333
  39. data/lib/uniword/docx/package_defaults.rb +83 -99
  40. data/lib/uniword/docx/package_integrity_checker.rb +312 -0
  41. data/lib/uniword/docx/package_serialization.rb +144 -233
  42. data/lib/uniword/docx/part.rb +125 -0
  43. data/lib/uniword/docx/part_collection.rb +131 -0
  44. data/lib/uniword/docx/part_loader/chart_loader.rb +50 -0
  45. data/lib/uniword/docx/part_loader/custom_xml_loader.rb +53 -0
  46. data/lib/uniword/docx/part_loader/embedding_loader.rb +26 -0
  47. data/lib/uniword/docx/part_loader/header_footer_loader.rb +101 -0
  48. data/lib/uniword/docx/part_loader/image_loader.rb +105 -0
  49. data/lib/uniword/docx/part_loader/load_context.rb +85 -0
  50. data/lib/uniword/docx/part_loader/raw_part_loader.rb +183 -0
  51. data/lib/uniword/docx/part_loader/theme_media_loader.rb +39 -0
  52. data/lib/uniword/docx/part_loader/xml_model_loader.rb +56 -0
  53. data/lib/uniword/docx/part_loader.rb +92 -0
  54. data/lib/uniword/docx/raw_part.rb +75 -0
  55. data/lib/uniword/docx/reconciler/body.rb +59 -68
  56. data/lib/uniword/docx/reconciler/fix.rb +46 -0
  57. data/lib/uniword/docx/reconciler/fix_codes.rb +37 -30
  58. data/lib/uniword/docx/reconciler/helpers.rb +18 -31
  59. data/lib/uniword/docx/reconciler/notes.rb +17 -8
  60. data/lib/uniword/docx/reconciler/package_structure.rb +168 -221
  61. data/lib/uniword/docx/reconciler/parts.rb +36 -13
  62. data/lib/uniword/docx/reconciler/referential_integrity.rb +222 -123
  63. data/lib/uniword/docx/reconciler/tables.rb +78 -16
  64. data/lib/uniword/docx/reconciler/theme.rb +7 -4
  65. data/lib/uniword/docx/reconciler.rb +36 -14
  66. data/lib/uniword/docx.rb +13 -0
  67. data/lib/uniword/drawingml/blip.rb +0 -1
  68. data/lib/uniword/drawingml/color_scheme.rb +5 -6
  69. data/lib/uniword/drawingml/font_scheme.rb +4 -8
  70. data/lib/uniword/drawingml/theme.rb +12 -6
  71. data/lib/uniword/errors.rb +8 -1
  72. data/lib/uniword/footer.rb +4 -0
  73. data/lib/uniword/header.rb +4 -0
  74. data/lib/uniword/hyperlink.rb +1 -1
  75. data/lib/uniword/image.rb +6 -6
  76. data/lib/uniword/images/image_manager.rb +13 -13
  77. data/lib/uniword/lazy_loader.rb +1 -2
  78. data/lib/uniword/model_attribute_access.rb +20 -4
  79. data/lib/uniword/ooxml/dotx_package.rb +42 -2
  80. data/lib/uniword/ooxml/element_order.rb +94 -0
  81. data/lib/uniword/ooxml/part_definition.rb +329 -0
  82. data/lib/uniword/ooxml/part_registry.rb +504 -0
  83. data/lib/uniword/ooxml/relationships/image_relationship.rb +6 -3
  84. data/lib/uniword/ooxml/relationships/package_relationships.rb +20 -22
  85. data/lib/uniword/ooxml/schema/element_serializer.rb +8 -7
  86. data/lib/uniword/ooxml/types/hex_color_value.rb +38 -0
  87. data/lib/uniword/ooxml/types/ooxml_boolean.rb +27 -14
  88. data/lib/uniword/ooxml/types/ooxml_boolean_optional.rb +21 -16
  89. data/lib/uniword/ooxml/types/theme_color_value.rb +22 -0
  90. data/lib/uniword/ooxml/types/unsigned_decimal_number.rb +30 -0
  91. data/lib/uniword/ooxml/types.rb +10 -0
  92. data/lib/uniword/ooxml.rb +3 -0
  93. data/lib/uniword/properties/alignment.rb +8 -3
  94. data/lib/uniword/properties/border.rb +52 -6
  95. data/lib/uniword/properties/cell_vertical_align.rb +4 -1
  96. data/lib/uniword/properties/color_value.rb +8 -3
  97. data/lib/uniword/properties/highlight.rb +9 -5
  98. data/lib/uniword/properties/shading.rb +4 -3
  99. data/lib/uniword/properties/tab_stop.rb +6 -1
  100. data/lib/uniword/properties/table_justification.rb +4 -1
  101. data/lib/uniword/properties/underline.rb +4 -2
  102. data/lib/uniword/properties/vertical_align.rb +6 -3
  103. data/lib/uniword/review/review_manager.rb +23 -9
  104. data/lib/uniword/revision.rb +9 -9
  105. data/lib/uniword/schema/model_generator.rb +31 -15
  106. data/lib/uniword/shared_types/hex_color.rb +4 -1
  107. data/lib/uniword/shared_types/pixel_measure.rb +4 -1
  108. data/lib/uniword/shared_types/point_measure.rb +4 -1
  109. data/lib/uniword/shared_types/text_alignment.rb +6 -1
  110. data/lib/uniword/shared_types/twips_measure.rb +4 -1
  111. data/lib/uniword/spreadsheetml/phonetic_pr.rb +1 -1
  112. data/lib/uniword/spreadsheetml/shared_string_table.rb +2 -2
  113. data/lib/uniword/spreadsheetml/table_formula.rb +1 -1
  114. data/lib/uniword/template/variable_resolver.rb +40 -7
  115. data/lib/uniword/themes/theme_transformation.rb +55 -41
  116. data/lib/uniword/toc/toc_generator.rb +6 -5
  117. data/lib/uniword/transformation/mhtml_element_renderer.rb +1 -5
  118. data/lib/uniword/transformation/mhtml_metadata_builder.rb +6 -6
  119. data/lib/uniword/validation/engine.rb +35 -0
  120. data/lib/uniword/validation/link_checker.rb +1 -1
  121. data/lib/uniword/validation/opc_validator.rb +16 -2
  122. data/lib/uniword/validation/report/layer_result.rb +0 -1
  123. data/lib/uniword/validation/report/terminal_formatter.rb +0 -1
  124. data/lib/uniword/validation/report/verification_report.rb +0 -2
  125. data/lib/uniword/validation/report.rb +20 -0
  126. data/lib/uniword/validation/rules/base.rb +10 -2
  127. data/lib/uniword/validation/rules/bookmark_pairing_rule.rb +38 -0
  128. data/lib/uniword/validation/rules/bookmark_uniqueness_rule.rb +48 -0
  129. data/lib/uniword/validation/rules/bookmarks_rule.rb +0 -2
  130. data/lib/uniword/validation/rules/content_types_coverage_rule.rb +0 -2
  131. data/lib/uniword/validation/rules/core_properties_namespace_rule.rb +0 -2
  132. data/lib/uniword/validation/rules/document_body_rule.rb +25 -0
  133. data/lib/uniword/validation/rules/document_context.rb +7 -0
  134. data/lib/uniword/validation/rules/empty_paragraphs_rule.rb +27 -0
  135. data/lib/uniword/validation/rules/font_table_signature_rule.rb +0 -2
  136. data/lib/uniword/validation/rules/fonts_rule.rb +0 -2
  137. data/lib/uniword/validation/rules/footnotes_rule.rb +0 -2
  138. data/lib/uniword/validation/rules/headers_footers_rule.rb +0 -2
  139. data/lib/uniword/validation/rules/images_rule.rb +0 -2
  140. data/lib/uniword/validation/rules/mc_ignorable_namespace_rule.rb +0 -2
  141. data/lib/uniword/validation/rules/model_context.rb +32 -0
  142. data/lib/uniword/validation/rules/model_rule.rb +34 -0
  143. data/lib/uniword/validation/rules/numbering_preservation_rule.rb +0 -2
  144. data/lib/uniword/validation/rules/numbering_rule.rb +0 -2
  145. data/lib/uniword/validation/rules/relationship_integrity_rule.rb +0 -1
  146. data/lib/uniword/validation/rules/rsid_rule.rb +0 -2
  147. data/lib/uniword/validation/rules/section_properties_rule.rb +0 -2
  148. data/lib/uniword/validation/rules/settings_rule.rb +0 -2
  149. data/lib/uniword/validation/rules/settings_values_rule.rb +0 -2
  150. data/lib/uniword/validation/rules/style_references_rule.rb +0 -2
  151. data/lib/uniword/validation/rules/table_grid_rule.rb +27 -0
  152. data/lib/uniword/validation/rules/table_properties_rule.rb +27 -0
  153. data/lib/uniword/validation/rules/tables_rule.rb +0 -2
  154. data/lib/uniword/validation/rules/theme_completeness_rule.rb +0 -2
  155. data/lib/uniword/validation/rules/theme_rule.rb +0 -2
  156. data/lib/uniword/validation/rules.rb +61 -23
  157. data/lib/uniword/validation/schema_registry.rb +11 -0
  158. data/lib/uniword/validation/validators/document_semantics_validator.rb +1 -9
  159. data/lib/uniword/validation/validators/xml_schema_validator.rb +46 -2
  160. data/lib/uniword/validation/validators.rb +0 -9
  161. data/lib/uniword/validation/verify_orchestrator.rb +0 -7
  162. data/lib/uniword/validation.rb +7 -2
  163. data/lib/uniword/version.rb +1 -1
  164. data/lib/uniword/vml/imagedata.rb +0 -1
  165. data/lib/uniword/wordprocessingml/attached_template.rb +0 -1
  166. data/lib/uniword/wordprocessingml/deleted_text.rb +1 -1
  167. data/lib/uniword/wordprocessingml/document_root.rb +121 -122
  168. data/lib/uniword/wordprocessingml/document_styling.rb +246 -0
  169. data/lib/uniword/wordprocessingml/endnotes.rb +4 -1
  170. data/lib/uniword/wordprocessingml/font_replacer.rb +147 -0
  171. data/lib/uniword/wordprocessingml/footnotes.rb +4 -1
  172. data/lib/uniword/wordprocessingml/hdr_shape_defaults.rb +0 -1
  173. data/lib/uniword/wordprocessingml/hyperlink.rb +0 -1
  174. data/lib/uniword/wordprocessingml/level.rb +6 -20
  175. data/lib/uniword/wordprocessingml/math_pr.rb +0 -11
  176. data/lib/uniword/wordprocessingml/numbering_definition.rb +16 -8
  177. data/lib/uniword/wordprocessingml/numbering_elements.rb +0 -15
  178. data/lib/uniword/wordprocessingml/page_setup.rb +181 -0
  179. data/lib/uniword/wordprocessingml/paragraph_properties.rb +33 -28
  180. data/lib/uniword/wordprocessingml/rsids.rb +0 -2
  181. data/lib/uniword/wordprocessingml/run.rb +3 -0
  182. data/lib/uniword/wordprocessingml/section_properties.rb +2 -2
  183. data/lib/uniword/wordprocessingml/settings.rb +8 -34
  184. data/lib/uniword/wordprocessingml/shape_defaults.rb +0 -1
  185. data/lib/uniword/wordprocessingml/style.rb +2 -1
  186. data/lib/uniword/wordprocessingml/style_cleanup.rb +198 -0
  187. data/lib/uniword/wordprocessingml/table_cell_properties.rb +6 -6
  188. data/lib/uniword/wordprocessingml/update_fields.rb +27 -0
  189. data/lib/uniword/wordprocessingml/w14_attributes.rb +30 -2
  190. data/lib/uniword/wordprocessingml.rb +5 -2
  191. data/lib/uniword.rb +27 -2
  192. metadata +52 -19
  193. data/config/validation_rules.yml +0 -60
  194. data/config/warning_rules.yml +0 -57
  195. data/lib/uniword/validation/document_validator.rb +0 -272
  196. data/lib/uniword/validation/structural_validator.rb +0 -116
  197. data/lib/uniword/validation/validators/content_type_validator.rb +0 -165
  198. data/lib/uniword/validation/validators/file_structure_validator.rb +0 -104
  199. data/lib/uniword/validation/validators/ooxml_part_validator.rb +0 -128
  200. data/lib/uniword/validation/validators/relationship_validator.rb +0 -147
  201. data/lib/uniword/validation/validators/zip_integrity_validator.rb +0 -110
  202. data/lib/uniword/validators/element_validator.rb +0 -93
  203. data/lib/uniword/validators/paragraph_validator.rb +0 -116
  204. data/lib/uniword/validators/table_validator.rb +0 -134
  205. data/lib/uniword/validators.rb +0 -9
  206. data/lib/uniword/warnings/warning.rb +0 -130
  207. data/lib/uniword/warnings/warning_collector.rb +0 -234
  208. data/lib/uniword/warnings/warning_report.rb +0 -159
  209. data/lib/uniword/warnings.rb +0 -9
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uniword
4
+ module Wordprocessingml
5
+ # Replaces one font family with another across a document —
6
+ # Word's Home → Replace → Replace Fonts as an API.
7
+ #
8
+ # Covers style definitions and defaults, body content (paragraphs,
9
+ # tables, hyperlinks), headers and footers, footnotes, endnotes,
10
+ # comments, and numbering levels. Theme font references
11
+ # (asciiTheme/hAnsiTheme/...) are NOT rewritten — those are
12
+ # controlled by the theme's font scheme
13
+ # (see DocumentRoot#apply_font_scheme).
14
+ #
15
+ # @example Replace Calibri with Carlito throughout a document
16
+ # replacer = FontReplacer.new(from: "Calibri", to: "Carlito")
17
+ # replacer.replace(document)
18
+ # puts replacer.count # number of rFonts values rewritten
19
+ class FontReplacer
20
+ # RunFonts attributes rewritten by font replacement.
21
+ FONT_ATTRIBUTES = %i[ascii h_ansi east_asia cs].freeze
22
+
23
+ # Create a replacer.
24
+ #
25
+ # @param from [String] Font family to replace (exact match,
26
+ # Word semantics)
27
+ # @param to [String] Replacement font family
28
+ def initialize(from:, to:)
29
+ @from = from.to_s
30
+ @to = to.to_s
31
+ @count = 0
32
+ end
33
+
34
+ # Number of rFonts attribute values replaced so far.
35
+ #
36
+ # @return [Integer] Replacement count
37
+ attr_reader :count
38
+
39
+ # Replace @from with @to throughout the document (mutated).
40
+ #
41
+ # @param document [DocumentRoot] Document to rewrite
42
+ # @return [Integer] Replacement count
43
+ def replace(document)
44
+ replace_in_styles(document.styles_configuration)
45
+ replace_in_numbering(document.numbering_configuration)
46
+ replace_in_container(document.body)
47
+ part_containers(document).each do |container|
48
+ replace_in_paragraphs(container)
49
+ end
50
+ count
51
+ end
52
+
53
+ private
54
+
55
+ # Styles: docDefaults and every style definition.
56
+ def replace_in_styles(styles)
57
+ return unless styles
58
+
59
+ replace_in_run_properties(styles.doc_defaults&.rPrDefault&.rPr)
60
+ (styles.styles || []).each do |style|
61
+ replace_in_run_properties(style.rPr)
62
+ end
63
+ end
64
+
65
+ # Numbering: every level of every abstract definition.
66
+ def replace_in_numbering(numbering)
67
+ return unless numbering
68
+
69
+ (numbering.definitions || []).each do |definition|
70
+ (definition.levels || []).each do |level|
71
+ replace_in_run_properties(level.rPr)
72
+ end
73
+ end
74
+ end
75
+
76
+ # Containers outside the body: header/footer parts, note
77
+ # entries, and comments — model objects holding paragraphs.
78
+ def part_containers(document)
79
+ header_footer_containers(document) +
80
+ note_containers(document) + comment_containers(document)
81
+ end
82
+
83
+ def header_footer_containers(document)
84
+ (document.header_footer_parts || []).filter_map do |part|
85
+ part.content if part.content.is_a?(Lutaml::Model::Serializable)
86
+ end
87
+ end
88
+
89
+ def note_containers(document)
90
+ (document.footnotes&.footnote_entries || []) +
91
+ (document.endnotes&.endnote_entries || [])
92
+ end
93
+
94
+ def comment_containers(document)
95
+ document.comments&.comments || []
96
+ end
97
+
98
+ # Walk a container's paragraphs, recursing into (nested) tables.
99
+ def replace_in_container(container)
100
+ return unless container
101
+
102
+ replace_in_paragraphs(container)
103
+ (container.tables || []).each { |table| replace_in_table(table) }
104
+ end
105
+
106
+ def replace_in_table(table)
107
+ (table.rows || []).each do |row|
108
+ (row.cells || []).each { |cell| replace_in_container(cell) }
109
+ end
110
+ end
111
+
112
+ # Walk only paragraphs — for note/comment/header/footer parts,
113
+ # whose models do not expose nested tables.
114
+ def replace_in_paragraphs(container)
115
+ (container.paragraphs || []).each do |paragraph|
116
+ replace_in_paragraph(paragraph)
117
+ end
118
+ end
119
+
120
+ def replace_in_paragraph(paragraph)
121
+ replace_in_runs(paragraph.runs)
122
+ (paragraph.hyperlinks || []).each do |hyperlink|
123
+ replace_in_runs(hyperlink.runs)
124
+ end
125
+ end
126
+
127
+ def replace_in_runs(runs)
128
+ (runs || []).each do |run|
129
+ replace_in_run_properties(run.properties)
130
+ end
131
+ end
132
+
133
+ # Rewrite matching RunFonts attribute values in place.
134
+ def replace_in_run_properties(run_properties)
135
+ fonts = run_properties&.fonts
136
+ return unless fonts
137
+
138
+ FONT_ATTRIBUTES.each do |attr|
139
+ next unless fonts.method(attr).call == @from
140
+
141
+ fonts.method(:"#{attr}=").call(@to)
142
+ @count += 1
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
@@ -32,8 +32,11 @@ module Uniword
32
32
  end
33
33
 
34
34
  missing = footnote_entries.size - counts["footnote"]
35
+ return unless missing.positive?
36
+
37
+ order = Ooxml::ElementOrder.mutable_order(self)
35
38
  missing.times do
36
- element_order << Lutaml::Xml::Element.new("Element", "footnote")
39
+ order << Lutaml::Xml::Element.new("Element", "footnote")
37
40
  end
38
41
  end
39
42
 
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "lutaml/model"
4
- require_relative "../vml_office"
5
4
 
6
5
  module Uniword
7
6
  module Wordprocessingml
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "lutaml/model"
4
- require "uniword/ooxml/types/relationship_id"
5
4
 
6
5
  module Uniword
7
6
  module Wordprocessingml
@@ -17,20 +17,6 @@ module Uniword
17
17
  end
18
18
  end
19
19
 
20
- # Tabs container for numbering level
21
- #
22
- # Element: <w:tabs>
23
- class Tabs < Lutaml::Model::Serializable
24
- attribute :tab, Tab, collection: true
25
-
26
- xml do
27
- element "tabs"
28
- namespace Uniword::Ooxml::Namespaces::WordProcessingML
29
- mixed_content
30
- map_element "tab", to: :tab, render_nil: false
31
- end
32
- end
33
-
34
20
  # Numbering level suffix type
35
21
  #
36
22
  # Element: <w:suff>
@@ -63,6 +49,10 @@ module Uniword
63
49
  #
64
50
  # Generated from OOXML schema: wordprocessingml.yml
65
51
  # Element: <w:lvl>
52
+ #
53
+ # Per ECMA-376 CT_Lvl, `w:ind` and `w:tabs` are NOT direct children of
54
+ # `w:lvl` — they belong to the level's `w:pPr`. They are therefore not
55
+ # mapped here; use `pPr.indentation` / `pPr.tabs`.
66
56
  class Level < Lutaml::Model::Serializable
67
57
  attribute :ilvl, :integer
68
58
  attribute :tentative, :string
@@ -75,8 +65,6 @@ module Uniword
75
65
  attribute :lvlText, LvlText
76
66
  attribute :lvlJc, LvlJc
77
67
  attribute :pPr, ParagraphProperties
78
- attribute :tabs, Tabs
79
- attribute :ind, Ind
80
68
  attribute :rPr, RunProperties
81
69
 
82
70
  xml do
@@ -95,8 +83,6 @@ module Uniword
95
83
  map_element "lvlText", to: :lvlText, render_nil: false
96
84
  map_element "lvlJc", to: :lvlJc, render_nil: false
97
85
  map_element "pPr", to: :pPr, render_nil: false
98
- map_element "tabs", to: :tabs, render_nil: false
99
- map_element "ind", to: :ind, render_nil: false
100
86
  map_element "rPr", to: :rPr, render_nil: false
101
87
  end
102
88
 
@@ -122,12 +108,12 @@ module Uniword
122
108
 
123
109
  # Get left indent (convenience method - returns integer)
124
110
  def left_indent_value
125
- ind&.left&.to_i
111
+ pPr&.indentation&.left.to_i
126
112
  end
127
113
 
128
114
  # Get hanging indent (convenience method - returns integer)
129
115
  def hanging_indent_value
130
- ind&.hanging&.to_i
116
+ pPr&.indentation&.hanging.to_i
131
117
  end
132
118
 
133
119
  # Get level text value (convenience method - returns string)
@@ -1,17 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "lutaml/model"
4
- require_relative "math_font"
5
- require_relative "brk_bin"
6
- require_relative "brk_bin_sub"
7
- require_relative "small_frac"
8
- require_relative "disp_def"
9
- require_relative "l_margin"
10
- require_relative "r_margin"
11
- require_relative "def_jc"
12
- require_relative "wrap_indent"
13
- require_relative "int_lim"
14
- require_relative "nary_lim"
15
4
 
16
5
  module Uniword
17
6
  module Wordprocessingml
@@ -80,8 +80,10 @@ module Uniword
80
80
  start: Uniword::Wordprocessingml::Start.new(val: 1),
81
81
  numFmt: Uniword::Wordprocessingml::NumFmt.new(val: "decimal"),
82
82
  lvlJc: Uniword::Wordprocessingml::LvlJc.new(val: "left"),
83
- ind: Uniword::Wordprocessingml::Ind.new(
84
- left: (720 * (i + 1)).to_s, hanging: "360",
83
+ pPr: Uniword::Wordprocessingml::ParagraphProperties.new(
84
+ indentation: Uniword::Properties::Indentation.new(
85
+ left: 720 * (i + 1), hanging: 360,
86
+ ),
85
87
  ),
86
88
  )
87
89
  end
@@ -104,8 +106,10 @@ module Uniword
104
106
  numFmt: Uniword::Wordprocessingml::NumFmt.new(val: "bullet"),
105
107
  lvlText: Uniword::Wordprocessingml::LvlText.new(val: level_char),
106
108
  lvlJc: Uniword::Wordprocessingml::LvlJc.new(val: "left"),
107
- ind: Uniword::Wordprocessingml::Ind.new(
108
- left: (720 * (i + 1)).to_s, hanging: "360",
109
+ pPr: Uniword::Wordprocessingml::ParagraphProperties.new(
110
+ indentation: Uniword::Properties::Indentation.new(
111
+ left: 720 * (i + 1), hanging: 360,
112
+ ),
109
113
  ),
110
114
  rPr: Uniword::Wordprocessingml::RunProperties.new(
111
115
  fonts: Properties::RunFonts.new(ascii: "Symbol",
@@ -126,8 +130,10 @@ module Uniword
126
130
  start: Uniword::Wordprocessingml::Start.new(val: 1),
127
131
  numFmt: Uniword::Wordprocessingml::NumFmt.new(val: format),
128
132
  lvlJc: Uniword::Wordprocessingml::LvlJc.new(val: "left"),
129
- ind: Uniword::Wordprocessingml::Ind.new(
130
- left: (720 * (i + 1)).to_s, hanging: "360",
133
+ pPr: Uniword::Wordprocessingml::ParagraphProperties.new(
134
+ indentation: Uniword::Properties::Indentation.new(
135
+ left: 720 * (i + 1), hanging: 360,
136
+ ),
131
137
  ),
132
138
  )
133
139
  end
@@ -144,8 +150,10 @@ module Uniword
144
150
  start: Uniword::Wordprocessingml::Start.new(val: 1),
145
151
  numFmt: Uniword::Wordprocessingml::NumFmt.new(val: format),
146
152
  lvlJc: Uniword::Wordprocessingml::LvlJc.new(val: "left"),
147
- ind: Uniword::Wordprocessingml::Ind.new(
148
- left: (720 * (i + 1)).to_s, hanging: "360",
153
+ pPr: Uniword::Wordprocessingml::ParagraphProperties.new(
154
+ indentation: Uniword::Properties::Indentation.new(
155
+ left: 720 * (i + 1), hanging: 360,
156
+ ),
149
157
  ),
150
158
  )
151
159
  end
@@ -30,21 +30,6 @@ module Uniword
30
30
  end
31
31
  end
32
32
 
33
- # Indentation for numbering level
34
- #
35
- # Element: <w:ind>
36
- class Ind < Lutaml::Model::Serializable
37
- attribute :left, :string
38
- attribute :hanging, :string
39
-
40
- xml do
41
- element "ind"
42
- namespace Uniword::Ooxml::Namespaces::WordProcessingML
43
- map_attribute "left", to: :left
44
- map_attribute "hanging", to: :hanging
45
- end
46
- end
47
-
48
33
  # Run fonts for numbering
49
34
  #
50
35
  # Element: <w:rFonts>
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uniword
4
+ module Wordprocessingml
5
+ # Applies uniform page setup across every section of a document —
6
+ # Word's Layout dialog as an API.
7
+ #
8
+ # Paper size (named presets), orientation (with dimension swap, like
9
+ # Word), and margins (twips, or strings with in/cm/mm suffix).
10
+ # Header/footer/gutter margins are left untouched.
11
+ #
12
+ # @example A4 landscape with 2 cm margins
13
+ # setup = PageSetup.new(size: "a4", orientation: "landscape",
14
+ # margins: "2cm")
15
+ # setup.apply(document)
16
+ # setup.sections_updated # => 1
17
+ class PageSetup
18
+ # Named paper sizes in twips [width, height] (portrait).
19
+ PAPER_SIZES = {
20
+ "letter" => [12_240, 15_840],
21
+ "legal" => [12_240, 20_160],
22
+ "a4" => [11_906, 16_838],
23
+ "a5" => [8_391, 11_906],
24
+ "executive" => [10_440, 15_120],
25
+ }.freeze
26
+
27
+ TWIPS_PER_INCH = 1440
28
+ TWIPS_PER_CM = 567
29
+ TWIPS_PER_MM = 57
30
+
31
+ ORIENTATIONS = %w[portrait landscape].freeze
32
+
33
+ # @return [Integer] number of sections updated
34
+ attr_reader :sections_updated
35
+
36
+ # @param size [String, nil] Paper size name (see PAPER_SIZES)
37
+ # @param orientation [String, nil] "portrait" or "landscape"
38
+ # @param margins [Integer, String, nil] Uniform margin for all four
39
+ # sides (twips, or "1in" / "2.5cm" / "25mm")
40
+ # @param side_margins [Hash] Per-side overrides
41
+ # (:top, :right, :bottom, :left)
42
+ # @raise [ArgumentError] on unknown size, orientation, or margin
43
+ def initialize(size: nil, orientation: nil, margins: nil,
44
+ **side_margins)
45
+ @size = normalize_size(size)
46
+ @orientation = normalize_orientation(orientation)
47
+ @margins = side_margins.to_h do |side, value|
48
+ [side, parse_margin(value)]
49
+ end
50
+ uniform = parse_margin(margins)
51
+ %i[top right bottom left].each do |side|
52
+ @margins[side] = uniform if @margins[side].nil?
53
+ end
54
+ @sections_updated = 0
55
+ end
56
+
57
+ # Apply the setup to every section in the document.
58
+ #
59
+ # @param document [DocumentRoot] Document to update (mutated)
60
+ # @return [Integer] number of sections updated
61
+ def apply(document)
62
+ each_section_properties(document) do |sect_pr|
63
+ apply_page_size(sect_pr) if @size || @orientation
64
+ apply_margins(sect_pr)
65
+ @sections_updated += 1
66
+ end
67
+ sections_updated
68
+ end
69
+
70
+ private
71
+
72
+ def normalize_size(size)
73
+ return nil if size.nil?
74
+
75
+ dims = PAPER_SIZES[size.to_s.downcase]
76
+ unless dims
77
+ raise ArgumentError,
78
+ "Unknown paper size '#{size}'. " \
79
+ "Available: #{PAPER_SIZES.keys.join(', ')}"
80
+ end
81
+
82
+ dims
83
+ end
84
+
85
+ def normalize_orientation(orientation)
86
+ return nil if orientation.nil?
87
+
88
+ value = orientation.to_s.downcase
89
+ return value if ORIENTATIONS.include?(value)
90
+
91
+ raise ArgumentError,
92
+ "Unknown orientation '#{orientation}'. " \
93
+ "Available: #{ORIENTATIONS.join(', ')}"
94
+ end
95
+
96
+ # Parse a margin value: Integer (twips) or String with an
97
+ # in/cm/mm/dxa suffix (bare numbers are inches).
98
+ def parse_margin(value)
99
+ case value
100
+ when nil then nil
101
+ when Numeric then value.round
102
+ when /\A([\d.]+)\s*(in|cm|mm|dxa|twips?)?\z/i
103
+ amount = Regexp.last_match(1).to_f
104
+ unit = Regexp.last_match(2)&.downcase || "in"
105
+ to_twips(amount, unit)
106
+ else
107
+ raise ArgumentError, "Invalid margin '#{value}' " \
108
+ "(use twips, or '1in' / '2.5cm' / '25mm')"
109
+ end
110
+ end
111
+
112
+ def to_twips(amount, unit)
113
+ factor = case unit
114
+ when "in" then TWIPS_PER_INCH
115
+ when "cm" then TWIPS_PER_CM
116
+ when "mm" then TWIPS_PER_MM
117
+ else 1
118
+ end
119
+ (amount * factor).round
120
+ end
121
+
122
+ def each_section_properties(document, &block)
123
+ body = document.body
124
+ return unless body
125
+
126
+ yield body.section_properties if body.section_properties
127
+ each_paragraph_section(body, &block)
128
+ end
129
+
130
+ def each_paragraph_section(body)
131
+ (body.paragraphs || []).each do |paragraph|
132
+ sect_pr = paragraph.properties&.section_properties
133
+ yield sect_pr if sect_pr
134
+ end
135
+ end
136
+
137
+ def apply_page_size(sect_pr)
138
+ width, height = target_dimensions(sect_pr)
139
+
140
+ sect_pr.page_size ||= PageSize.new
141
+ sect_pr.page_size.width = width
142
+ sect_pr.page_size.height = height
143
+ sect_pr.page_size.orientation = @orientation if @orientation
144
+ end
145
+
146
+ def target_dimensions(sect_pr)
147
+ width, height = current_dimensions(sect_pr)
148
+ width, height = @size if @size
149
+ width, height = orient(width, height) if @orientation
150
+ [width, height]
151
+ end
152
+
153
+ def current_dimensions(sect_pr)
154
+ size = sect_pr.page_size
155
+ [size&.width || PageDefaults.default_page_size.width,
156
+ size&.height || PageDefaults.default_page_size.height]
157
+ end
158
+
159
+ # Word swaps width/height when flipping orientation.
160
+ def orient(width, height)
161
+ if (@orientation == "landscape" && width < height) ||
162
+ (@orientation == "portrait" && width > height)
163
+ [height, width]
164
+ else
165
+ [width, height]
166
+ end
167
+ end
168
+
169
+ def apply_margins(sect_pr)
170
+ return if @margins.values.all?(&:nil?)
171
+
172
+ sect_pr.page_margins ||= PageMargins.new
173
+ @margins.each do |side, value|
174
+ next if value.nil?
175
+
176
+ sect_pr.page_margins.method(:"#{side}=").call(value)
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
@@ -116,7 +116,8 @@ module Uniword
116
116
  with: { from: :yaml_widow_control_from, to: :yaml_widow_control_to }
117
117
  end
118
118
 
119
- # YAML transform methods (instance methods - called via send on an instance)
119
+ # YAML transform methods (instance methods called by lutaml-model's
120
+ # `with:` transform mechanism)
120
121
  def yaml_style_from(instance, value)
121
122
  if value
122
123
  instance.style = [
@@ -200,21 +201,6 @@ module Uniword
200
201
  # Style reference - maps w:pStyle w:val="..." to style attribute
201
202
  map_element "pStyle", to: :style, render_nil: false
202
203
 
203
- # Alignment - maps w:jc w:val="..." to alignment attribute
204
- map_element "jc", to: :alignment, render_nil: false
205
-
206
- # Spacing (complex object - WORKS)
207
- map_element "spacing", to: :spacing, render_nil: false
208
-
209
- # Indentation (complex object)
210
- map_element "ind", to: :indentation, render_nil: false
211
-
212
- # Outline level (wrapper object)
213
- map_element "outlineLvl", to: :outline_level, render_nil: false
214
-
215
- # Numbering properties (wrapped in w:numPr)
216
- map_element "numPr", to: :numbering_properties, render_nil: false
217
-
218
204
  # Keep options (only render if true)
219
205
  map_element "keepNext", to: :keep_next_wrapper, render_nil: false,
220
206
  render_default: false
@@ -225,32 +211,51 @@ module Uniword
225
211
  map_element "widowControl", to: :widow_control_wrapper, render_nil: false,
226
212
  render_default: false
227
213
 
228
- # Spacing options (only render if true)
229
- map_element "contextualSpacing", to: :contextual_spacing, render_nil: false,
230
- render_default: false
214
+ # Numbering properties (wrapped in w:numPr)
215
+ map_element "numPr", to: :numbering_properties, render_nil: false
216
+
217
+ # Suppress line numbers (only render if true)
231
218
  map_element "suppressLineNumbers", to: :suppress_line_numbers, render_nil: false,
232
219
  render_default: false
233
220
 
234
- # Bidirectional (only render if true)
235
- map_element "bidi", to: :bidirectional, render_nil: false,
236
- render_default: false
221
+ # Borders (complex object)
222
+ map_element "pBdr", to: :borders, render_nil: false
223
+
224
+ # Shading (complex object)
225
+ map_element "shd", to: :shading, render_nil: false
226
+
227
+ # Tabs (complex object - collection)
228
+ map_element "tabs", to: :tabs, render_nil: false
237
229
 
238
230
  # East Asian typography
239
231
  map_element "autoSpaceDE", to: :auto_space_de, render_nil: false,
240
232
  render_default: false
241
233
  map_element "autoSpaceDN", to: :auto_space_dn, render_nil: false,
242
234
  render_default: false
235
+
236
+ # Bidirectional (only render if true)
237
+ map_element "bidi", to: :bidirectional, render_nil: false,
238
+ render_default: false
239
+
240
+ # Right indent adjustment
243
241
  map_element "adjustRightInd", to: :adjust_right_ind, render_nil: false,
244
242
  render_default: false
245
243
 
246
- # Borders (complex object)
247
- map_element "pBdr", to: :borders, render_nil: false
244
+ # Spacing (complex object - WORKS)
245
+ map_element "spacing", to: :spacing, render_nil: false
248
246
 
249
- # Tabs (complex object - collection)
250
- map_element "tabs", to: :tabs, render_nil: false
247
+ # Indentation (complex object)
248
+ map_element "ind", to: :indentation, render_nil: false
251
249
 
252
- # Shading (complex object)
253
- map_element "shd", to: :shading, render_nil: false
250
+ # Contextual spacing (only render if true)
251
+ map_element "contextualSpacing", to: :contextual_spacing, render_nil: false,
252
+ render_default: false
253
+
254
+ # Alignment - maps w:jc w:val="..." to alignment attribute
255
+ map_element "jc", to: :alignment, render_nil: false
256
+
257
+ # Outline level (wrapper object)
258
+ map_element "outlineLvl", to: :outline_level, render_nil: false
254
259
 
255
260
  # Run properties (for pPr/rPr - style overrides)
256
261
  map_element "rPr", to: :run_properties, render_nil: false
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "lutaml/model"
4
- require_relative "rsid_root"
5
- require_relative "rsid"
6
4
 
7
5
  module Uniword
8
6
  module Wordprocessingml
@@ -18,6 +18,7 @@ module Uniword
18
18
  attribute :alternate_content, AlternateContent, default: nil
19
19
  attribute :footnote_reference, FootnoteReference
20
20
  attribute :endnote_reference, EndnoteReference
21
+ attribute :comment_reference, CommentReference
21
22
  attribute :field_char, FieldChar
22
23
  attribute :instr_text, InstrText
23
24
  attribute :position_tab, PositionTab
@@ -55,6 +56,8 @@ module Uniword
55
56
  render_nil: false
56
57
  map_element "endnoteReference", to: :endnote_reference,
57
58
  render_nil: false
59
+ map_element "commentReference", to: :comment_reference,
60
+ render_nil: false
58
61
  map_element "fldChar", to: :field_char, render_nil: false
59
62
  map_element "instrText", to: :instr_text, render_nil: false
60
63
  map_element "ptab", to: :position_tab, render_nil: false
@@ -50,14 +50,14 @@ module Uniword
50
50
 
51
51
  map_element "headerReference", to: :header_references, render_nil: false
52
52
  map_element "footerReference", to: :footer_references, render_nil: false
53
+ map_element "footnotePr", to: :footnote_pr, render_nil: false
53
54
  map_element "pgSz", to: :page_size, render_nil: false
54
55
  map_element "pgMar", to: :page_margins, render_nil: false
55
56
  map_attribute "val", to: :type
56
57
  map_element "pgNumType", to: :page_numbering, render_nil: false
57
58
  map_element "cols", to: :columns, render_nil: false
58
- map_element "docGrid", to: :doc_grid, render_nil: false
59
- map_element "footnotePr", to: :footnote_pr, render_nil: false
60
59
  map_element "titlePg", to: :title_pg, render_nil: false
60
+ map_element "docGrid", to: :doc_grid, render_nil: false
61
61
  map_element "footnoteColumns", to: :footnote_columns, render_nil: false
62
62
  end
63
63
  end