uniword 1.6.0 → 1.7.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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/exe/uniword +9 -0
  3. data/lib/uniword/accessibility/accessibility_report.rb +8 -0
  4. data/lib/uniword/builder/chart_builder.rb +129 -134
  5. data/lib/uniword/cli/completions.rb +99 -0
  6. data/lib/uniword/cli/generate_cli.rb +5 -0
  7. data/lib/uniword/cli/help_switch.rb +45 -0
  8. data/lib/uniword/cli/helpers.rb +1 -0
  9. data/lib/uniword/cli/main.rb +118 -21
  10. data/lib/uniword/cli/no_color.rb +21 -0
  11. data/lib/uniword/diff/package_differ.rb +16 -15
  12. data/lib/uniword/document_factory.rb +2 -2
  13. data/lib/uniword/document_writer.rb +32 -10
  14. data/lib/uniword/docx/package_integrity_checker.rb +10 -11
  15. data/lib/uniword/docx/reconciler/body.rb +1 -0
  16. data/lib/uniword/docx/reconciler/helpers.rb +27 -12
  17. data/lib/uniword/docx/reconciler/parts.rb +5 -0
  18. data/lib/uniword/find_replace/paragraph_walker.rb +49 -12
  19. data/lib/uniword/find_replace/scope.rb +7 -4
  20. data/lib/uniword/infrastructure/mime_packager.rb +3 -10
  21. data/lib/uniword/infrastructure/mime_parser.rb +23 -1
  22. data/lib/uniword/math/plurimath_adapter.rb +28 -24
  23. data/lib/uniword/math_equation.rb +1 -1
  24. data/lib/uniword/mhtml/html_part.rb +19 -13
  25. data/lib/uniword/mhtml/math_converter.rb +18 -12
  26. data/lib/uniword/mhtml/mime_part.rb +5 -16
  27. data/lib/uniword/mhtml/xml_part.rb +3 -3
  28. data/lib/uniword/ooxml/additional_characteristics.rb +4 -4
  29. data/lib/uniword/ooxml/element_order.rb +21 -0
  30. data/lib/uniword/ooxml/schema/element_serializer.rb +27 -35
  31. data/lib/uniword/ooxml/schema_library.rb +2 -2
  32. data/lib/uniword/ooxml/types/characteristics_val.rb +16 -0
  33. data/lib/uniword/ooxml/types/schema_library_val.rb +17 -0
  34. data/lib/uniword/ooxml/types/variant_types.rb +10 -2
  35. data/lib/uniword/ooxml/types/vt_attr.rb +19 -0
  36. data/lib/uniword/ooxml/types/wml_int.rb +17 -0
  37. data/lib/uniword/ooxml/types/wml_val.rb +19 -0
  38. data/lib/uniword/ooxml/types.rb +12 -0
  39. data/lib/uniword/properties/alignment.rb +4 -0
  40. data/lib/uniword/properties/boolean_element_factory.rb +1 -1
  41. data/lib/uniword/properties/font_size.rb +3 -0
  42. data/lib/uniword/review/review_manager.rb +34 -5
  43. data/lib/uniword/review/revision_resolver.rb +190 -0
  44. data/lib/uniword/review.rb +1 -0
  45. data/lib/uniword/revision.rb +3 -3
  46. data/lib/uniword/serialization/ooxml_serializer.rb +3 -3
  47. data/lib/uniword/streaming_parser.rb +22 -20
  48. data/lib/uniword/theme/theme_xml_parser.rb +4 -6
  49. data/lib/uniword/transformation/html_element_builder.rb +62 -48
  50. data/lib/uniword/transformation/html_formatting_mapper.rb +3 -3
  51. data/lib/uniword/transformation/html_to_ooxml_converter.rb +11 -11
  52. data/lib/uniword/transformation/mhtml_element_renderer.rb +1 -2
  53. data/lib/uniword/validation/opc_validator.rb +6 -7
  54. data/lib/uniword/validation/rules/document_context.rb +3 -3
  55. data/lib/uniword/validation/rules/images_rule.rb +1 -1
  56. data/lib/uniword/validation/rules/theme_rule.rb +2 -2
  57. data/lib/uniword/validation/schema_registry.rb +10 -17
  58. data/lib/uniword/validation/validators/xml_schema_validator.rb +75 -26
  59. data/lib/uniword/validation/verify_orchestrator.rb +1 -1
  60. data/lib/uniword/version.rb +1 -1
  61. data/lib/uniword/wordprocessingml/deletion.rb +41 -0
  62. data/lib/uniword/wordprocessingml/insertion.rb +41 -0
  63. data/lib/uniword/wordprocessingml/page_margins.rb +7 -7
  64. data/lib/uniword/wordprocessingml/paragraph.rb +30 -4
  65. data/lib/uniword/wordprocessingml/settings.rb +3 -4
  66. data/lib/uniword/wordprocessingml/style_cleanup.rb +5 -14
  67. data/lib/uniword/wordprocessingml/table_borders.rb +1 -1
  68. data/lib/uniword/wordprocessingml/table_cell_borders.rb +1 -1
  69. data/lib/uniword/wordprocessingml/text.rb +0 -1
  70. data/lib/uniword/wordprocessingml.rb +5 -0
  71. data/lib/uniword/wp_drawing/position_h.rb +1 -1
  72. data/lib/uniword/wp_drawing/position_v.rb +1 -1
  73. data/lib/uniword/wp_drawing/size_rel_h.rb +1 -1
  74. data/lib/uniword/wp_drawing/size_rel_v.rb +1 -1
  75. data/lib/uniword.rb +9 -7
  76. metadata +19 -15
  77. data/lib/uniword/infrastructure/xml_namespace_normalizer.rb +0 -107
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1153bb3528efaa764919405fb7e3ac11e0e9aedcaae81e6dd0b692293e3876a2
4
- data.tar.gz: 0e59bf185f59fc2377dc56c1dd1bfacbc773814d47dcef9f19bb6f855cf0298c
3
+ metadata.gz: 34ddbf784b5b85ba8aedc6b4df9b255763e2b8014bcdea0124563e261555b32b
4
+ data.tar.gz: e4ef3fce0529a6db2642cbf547c28628512834339ce636f15d90c30dccba7eb2
5
5
  SHA512:
6
- metadata.gz: 540da6a280c292c6563daf77a1c024fefbc1266048d7fab197cff2e7bb7d0529d2953e1cc596d8b426668702adc03a3b1821645a8bd028fc6d71cfe937862a0c
7
- data.tar.gz: 9543bd3d00ce96e4d47480f5db92a05ab60a7fc1a6e85cf60037ace089106f703f30d6be825dcaa939319c5fe559d5e35b9669f8103a6c9cabd8c5c4415552f1
6
+ metadata.gz: ae06c93c3b13ffffc70b0bddc7137945e4a035f2a4f75e315406547166c4423cdfe8ff8835b47ae5df50be9c6a021f500b89bd17512ae646cb3dd4c890bf8d8b
7
+ data.tar.gz: 7a37bcf5e4cc1ece3a92ad962757a48613be7f960890bd52017dcf8343bb1989172c7b9bd931a384dfe4bcee58c24de27b80b76e9dd4d3eca09280b078f8e3f1
data/exe/uniword CHANGED
@@ -10,5 +10,14 @@ if defined?(RubyVM::YJIT) && !RubyVM::YJIT.enabled? &&
10
10
  RubyVM::YJIT.enable
11
11
  end
12
12
 
13
+ # POSIX NO_COLOR convention: suppress colored CLI output.
14
+ if ENV["NO_COLOR"] && !ENV["NO_COLOR"].empty?
15
+ require "uniword/cli/no_color"
16
+ end
17
+
18
+ # Never let a task's return value become the process exit status:
19
+ # success is 0, and failures raise SystemExit inside `start`
20
+ # (exit_on_failure? / handle_error).
13
21
  require "uniword"
14
22
  Uniword::CLI.start(ARGV)
23
+ exit 0
@@ -46,6 +46,14 @@ module Uniword
46
46
  errors.empty?
47
47
  end
48
48
 
49
+ # Report-contract predicate shared with Quality::QualityReport:
50
+ # true when the document has no error-level violations.
51
+ #
52
+ # @return [Boolean]
53
+ def valid?
54
+ compliant?
55
+ end
56
+
49
57
  # Get all error-level violations
50
58
  #
51
59
  # @return [Array<AccessibilityViolation>] Error violations
@@ -28,13 +28,16 @@ module Uniword
28
28
  # end
29
29
  class ChartBuilder
30
30
  CHART_NS = "http://schemas.openxmlformats.org/drawingml/2006/chart"
31
+ DRAWINGML_MAIN_NS =
32
+ "http://schemas.openxmlformats.org/drawingml/2006/main"
33
+ OFFICE_DOCUMENT_RELS_NS =
34
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
31
35
  CHART_REL_TYPE = Ooxml::PartRegistry.find_by_key(:chart).rel_type
32
36
  CHART_CONTENT_TYPE =
33
37
  Ooxml::PartRegistry.find_by_key(:chart).content_type
34
38
 
35
- attr_reader :chart_type, :title_text, :series_list
36
-
37
- attr_reader :show_legend, :width, :height, :series_list
39
+ attr_reader :chart_type, :title_text, :series_list, :show_legend, :width,
40
+ :height
38
41
 
39
42
  def initialize(chart_type: :bar)
40
43
  @chart_type = chart_type
@@ -101,16 +104,15 @@ module Uniword
101
104
  #
102
105
  # @return [String] Chart XML
103
106
  def build_xml
104
- builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
105
- xml["c"].chartSpace("xmlns:c" => CHART_NS,
106
- "xmlns:a" =>
107
- "http://schemas.openxmlformats.org/drawingml/2006/main",
108
- "xmlns:r" =>
109
- "http://schemas.openxmlformats.org/officeDocument/2006/relationships") do
110
- build_chart_xml(xml)
111
- end
112
- end
113
- builder.to_xml
107
+ @document = Moxml.new.create_document
108
+ @document.add_child(@document.create_declaration("1.0", "UTF-8"))
109
+ root = @document.create_element("c:chartSpace")
110
+ root.add_namespace("c", CHART_NS)
111
+ root.add_namespace("a", DRAWINGML_MAIN_NS)
112
+ root.add_namespace("r", OFFICE_DOCUMENT_RELS_NS)
113
+ @document.root = root
114
+ build_chart(root)
115
+ @document.to_xml
114
116
  end
115
117
 
116
118
  # Register chart on a document and create the Drawing element
@@ -147,166 +149,159 @@ module Uniword
147
149
  include DeterministicId
148
150
 
149
151
  # Build the inner chart XML
150
- def build_chart_xml(xml)
151
- xml["c"].chart do
152
- if @title_text
153
- xml["c"].title do
154
- xml["c"].tx do
155
- xml["c"].rich do
156
- xml["a"].bodyPr
157
- xml["a"].lstStyle
158
- xml["a"].p do
159
- xml["a"].r do
160
- xml["a"].t(@title_text)
161
- end
162
- end
163
- end
164
- end
165
- xml["c"].overlay("val" => "0")
166
- end
167
- end
168
-
169
- xml["c"].autoTitleDeleted("val" => "0")
170
-
171
- xml["c"].plotArea do
172
- build_chart_type(xml)
173
-
174
- unless @chart_type == :pie
175
- build_axis(xml, "catAx", "10", "b", "20")
176
- build_axis(xml, "valAx", "20", "l", "10")
177
- end
178
- end
179
-
180
- if @show_legend
181
- xml["c"].legend do
182
- xml["c"].legendPos("val" => @legend_position)
183
- xml["c"].overlay("val" => "0")
184
- end
185
- end
186
-
187
- xml["c"].plotVisOnly("val" => "1")
152
+ def build_chart(parent)
153
+ chart = add_element(parent, "c:chart")
154
+ if @title_text
155
+ title = add_element(chart, "c:title")
156
+ tx = add_element(title, "c:tx")
157
+ rich = add_element(tx, "c:rich")
158
+ add_element(rich, "a:bodyPr")
159
+ add_element(rich, "a:lstStyle")
160
+ p_el = add_element(rich, "a:p")
161
+ r_el = add_element(p_el, "a:r")
162
+ add_text(add_element(r_el, "a:t"), @title_text)
163
+ add_element(title, "c:overlay", "val" => "0")
164
+ end
165
+
166
+ add_element(chart, "c:autoTitleDeleted", "val" => "0")
167
+
168
+ plot_area = add_element(chart, "c:plotArea")
169
+ build_chart_type(plot_area)
170
+
171
+ unless @chart_type == :pie
172
+ build_axis(plot_area, "catAx", "10", "b", "20")
173
+ build_axis(plot_area, "valAx", "20", "l", "10")
174
+ end
175
+
176
+ if @show_legend
177
+ legend = add_element(chart, "c:legend")
178
+ add_element(legend, "c:legendPos", "val" => @legend_position)
179
+ add_element(legend, "c:overlay", "val" => "0")
188
180
  end
181
+
182
+ add_element(chart, "c:plotVisOnly", "val" => "1")
183
+ chart
189
184
  end
190
185
 
191
186
  # Build the specific chart type element
192
- def build_chart_type(xml)
187
+ def build_chart_type(parent)
193
188
  case @chart_type
194
189
  when :bar
195
- xml["c"].barChart do
196
- xml["c"].barDir("val" => "col")
197
- xml["c"].grouping("val" => "clustered")
198
- xml["c"].varyColors("val" => "0")
199
- build_all_series(xml)
200
- xml["c"].axId("val" => "10")
201
- xml["c"].axId("val" => "20")
202
- end
190
+ chart = add_element(parent, "c:barChart")
191
+ add_element(chart, "c:barDir", "val" => "col")
192
+ add_element(chart, "c:grouping", "val" => "clustered")
193
+ add_element(chart, "c:varyColors", "val" => "0")
194
+ build_all_series(chart)
195
+ add_element(chart, "c:axId", "val" => "10")
196
+ add_element(chart, "c:axId", "val" => "20")
203
197
  when :line
204
- xml["c"].lineChart do
205
- xml["c"].grouping("val" => "standard")
206
- xml["c"].varyColors("val" => "0")
207
- build_all_series(xml)
208
- xml["c"].axId("val" => "10")
209
- xml["c"].axId("val" => "20")
210
- end
198
+ chart = add_element(parent, "c:lineChart")
199
+ add_element(chart, "c:grouping", "val" => "standard")
200
+ add_element(chart, "c:varyColors", "val" => "0")
201
+ build_all_series(chart)
202
+ add_element(chart, "c:axId", "val" => "10")
203
+ add_element(chart, "c:axId", "val" => "20")
211
204
  when :pie
212
- xml["c"].pieChart do
213
- xml["c"].varyColors("val" => "1")
214
- build_all_series(xml)
215
- end
205
+ chart = add_element(parent, "c:pieChart")
206
+ add_element(chart, "c:varyColors", "val" => "1")
207
+ build_all_series(chart)
216
208
  end
217
209
  end
218
210
 
219
211
  # Build all series XML blocks
220
- def build_all_series(xml)
212
+ def build_all_series(parent)
221
213
  @series_list.each_with_index do |s, i|
222
- build_series(xml, s, i)
214
+ build_series(parent, s, i)
223
215
  end
224
216
  end
225
217
 
226
218
  # Build a single series XML block
227
- def build_series(xml, series, index)
228
- xml["c"].ser do
229
- xml["c"].idx("val" => index)
230
- xml["c"].order("val" => index)
231
- xml["c"].tx do
232
- xml["c"].strRef do
233
- xml["c"].f("Sheet1!$B$#{index + 1}")
234
- xml["c"].strCache do
235
- xml["c"].ptCount("val" => 1)
236
- xml["c"].pt("idx" => 0) { xml["c"].v(series[:name]) }
237
- end
238
- end
239
- end
240
- build_categories(xml)
241
- build_values(xml, series[:data])
242
- end
219
+ def build_series(parent, series, index)
220
+ ser = add_element(parent, "c:ser")
221
+ add_element(ser, "c:idx", "val" => index)
222
+ add_element(ser, "c:order", "val" => index)
223
+ tx = add_element(ser, "c:tx")
224
+ str_ref = add_element(tx, "c:strRef")
225
+ add_text(add_element(str_ref, "c:f"), "Sheet1!$B$#{index + 1}")
226
+ str_cache = add_element(str_ref, "c:strCache")
227
+ add_element(str_cache, "c:ptCount", "val" => 1)
228
+ pt = add_element(str_cache, "c:pt", "idx" => 0)
229
+ add_text(add_element(pt, "c:v"), series[:name])
230
+ build_categories(ser)
231
+ build_values(ser, series[:data])
232
+ ser
243
233
  end
244
234
 
245
235
  # Build category data XML
246
- def build_categories(xml)
236
+ def build_categories(parent)
247
237
  return if @categories.empty?
248
238
 
249
- xml["c"].cat do
250
- xml["c"].strRef do
251
- xml["c"].f("Sheet1!$A$2:$A$100")
252
- xml["c"].strCache do
253
- xml["c"].ptCount("val" => @categories.size)
254
- @categories.each_with_index do |cat, idx|
255
- xml["c"].pt("idx" => idx) { xml["c"].v(cat) }
256
- end
257
- end
258
- end
239
+ cat = add_element(parent, "c:cat")
240
+ str_ref = add_element(cat, "c:strRef")
241
+ add_text(add_element(str_ref, "c:f"), "Sheet1!$A$2:$A$100")
242
+ str_cache = add_element(str_ref, "c:strCache")
243
+ add_element(str_cache, "c:ptCount", "val" => @categories.size)
244
+ @categories.each_with_index do |category, idx|
245
+ pt = add_element(str_cache, "c:pt", "idx" => idx)
246
+ add_text(add_element(pt, "c:v"), category)
259
247
  end
260
248
  end
261
249
 
262
250
  # Build values data XML
263
- def build_values(xml, data)
264
- xml["c"].val do
265
- xml["c"].numRef do
266
- xml["c"].f("Sheet1!$C$2:$C$100")
267
- xml["c"].numCache do
268
- xml["c"].formatCode("General")
269
- xml["c"].ptCount("val" => data.size)
270
- data.each_with_index do |val, idx|
271
- xml["c"].pt("idx" => idx) { xml["c"].v(val.to_s) }
272
- end
273
- end
274
- end
251
+ def build_values(parent, data)
252
+ val = add_element(parent, "c:val")
253
+ num_ref = add_element(val, "c:numRef")
254
+ add_text(add_element(num_ref, "c:f"), "Sheet1!$C$2:$C$100")
255
+ num_cache = add_element(num_ref, "c:numCache")
256
+ add_text(add_element(num_cache, "c:formatCode"), "General")
257
+ add_element(num_cache, "c:ptCount", "val" => data.size)
258
+ data.each_with_index do |value, idx|
259
+ pt = add_element(num_cache, "c:pt", "idx" => idx)
260
+ add_text(add_element(pt, "c:v"), value.to_s)
275
261
  end
276
262
  end
277
263
 
278
264
  # Build a chart axis
279
- def build_axis(xml, tag, id, position, cross_id)
265
+ def build_axis(parent, tag, id, position, cross_id)
280
266
  case tag
281
267
  when "catAx"
282
- xml["c"].catAx do
283
- write_axis_content(xml, id, position, cross_id)
284
- end
268
+ write_axis_content(add_element(parent, "c:catAx"), id, position,
269
+ cross_id)
285
270
  when "valAx"
286
- xml["c"].valAx do
287
- write_axis_content(xml, id, position, cross_id)
288
- end
271
+ write_axis_content(add_element(parent, "c:valAx"), id, position,
272
+ cross_id)
289
273
  when "serAx"
290
- xml["c"].serAx do
291
- write_axis_content(xml, id, position, cross_id)
292
- end
274
+ write_axis_content(add_element(parent, "c:serAx"), id, position,
275
+ cross_id)
293
276
  else
294
277
  raise ArgumentError, "Unknown axis type: #{tag}"
295
278
  end
296
279
  end
297
280
 
298
- def write_axis_content(xml, id, position, cross_id)
299
- xml["c"].axId("val" => id)
300
- xml["c"].scaling do
301
- xml["c"].orientation("val" => "minMax")
302
- end
303
- xml["c"].delete("val" => "0")
304
- xml["c"].axPos("val" => position)
305
- xml["c"].majorGridlines
306
- xml["c"].numFmt("formatCode" => "General",
307
- "sourceLinked" => "1")
308
- xml["c"].tickLblPos("val" => "nextTo")
309
- xml["c"].crossAx("val" => cross_id)
281
+ def write_axis_content(axis, id, position, cross_id)
282
+ add_element(axis, "c:axId", "val" => id)
283
+ scaling = add_element(axis, "c:scaling")
284
+ add_element(scaling, "c:orientation", "val" => "minMax")
285
+ add_element(axis, "c:delete", "val" => "0")
286
+ add_element(axis, "c:axPos", "val" => position)
287
+ add_element(axis, "c:majorGridlines")
288
+ add_element(axis, "c:numFmt", "formatCode" => "General",
289
+ "sourceLinked" => "1")
290
+ add_element(axis, "c:tickLblPos", "val" => "nextTo")
291
+ add_element(axis, "c:crossAx", "val" => cross_id)
292
+ end
293
+
294
+ # Append an element with attributes to a parent node.
295
+ def add_element(parent, name, attributes = {})
296
+ element = @document.create_element(name)
297
+ attributes.each { |key, value| element[key] = value }
298
+ parent.add_child(element)
299
+ element
300
+ end
301
+
302
+ # Append a text node to a parent node.
303
+ def add_text(parent, content)
304
+ parent.add_child(@document.create_text(content))
310
305
  end
311
306
 
312
307
  # Create a Drawing element with chart reference
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uniword
4
+ module Cli
5
+ # Builds shell completion scripts from live CLI metadata so new
6
+ # commands appear in completions without editing the scripts.
7
+ module Completions
8
+ BUILTINS = %w[help completions].freeze
9
+
10
+ module_function
11
+
12
+ # @param shell [String] "bash", "zsh", or "fish"
13
+ # @param root [Class] the root Thor class (Uniword::CLI)
14
+ # @return [String] completion script
15
+ def script(shell, root:)
16
+ commands = root.all_commands.keys - BUILTINS
17
+ subcommands = subcommand_map(root)
18
+
19
+ case shell
20
+ when "bash" then bash(commands, subcommands)
21
+ when "zsh" then zsh(commands, subcommands)
22
+ when "fish" then fish(commands, subcommands)
23
+ else
24
+ raise ArgumentError,
25
+ "unsupported shell: #{shell.inspect} (bash|zsh|fish)"
26
+ end
27
+ end
28
+
29
+ # Map command name → registered subcommand Thor class, derived
30
+ # from class names (ThemeCLI → "theme"), so `subcommand` call
31
+ # sites stay the single source of truth.
32
+ # Space-separated subcommand word list for one class.
33
+ def words_for(klass)
34
+ klass.all_commands.keys.reject { |c| c == "help" }.join(" ")
35
+ end
36
+
37
+ def subcommand_map(root)
38
+ root.subcommand_classes.filter_map do |name, klass|
39
+ [name, klass]
40
+ end.to_h
41
+ end
42
+
43
+ def bash(commands, subcommands)
44
+ cmds = commands.join(" ")
45
+ script = <<~BASH
46
+ # uniword bash completion — generated by `uniword completions bash`
47
+ _uniword() {
48
+ local cur="${COMP_WORDS[COMP_CWORD]}"
49
+ local prev="${COMP_WORDS[COMP_CWORD-1]}"
50
+ local cmds="#{cmds}"
51
+ BASH
52
+ subcommands.each do |name, klass|
53
+ words = words_for(klass)
54
+ script << " if [ \"$prev\" = \"#{name}\" ]; then\n " \
55
+ "COMPREPLY=( $(compgen -W \"#{words}\" " \
56
+ "-- \"$cur\") )\n return\n fi\n"
57
+ end
58
+ script << " COMPREPLY=( $(compgen -W \"$cmds\" -- \"$cur\") )\n" \
59
+ "}\n" \
60
+ "complete -F _uniword uniword\n"
61
+ script
62
+ end
63
+
64
+ def zsh(commands, subcommands)
65
+ script = <<~ZSH
66
+ #compdef uniword
67
+ # uniword zsh completion — generated by `uniword completions zsh`
68
+ _uniword() {
69
+ local -a cmds
70
+ cmds=(#{commands.map { |c| "'#{c}'" }.join(' ')})
71
+ _describe 'command' cmds
72
+ ZSH
73
+ subcommands.each do |name, klass|
74
+ words = words_for(klass).split.map { |c| "'#{c}'" }.join(" ")
75
+ script << " if (( CURRENT == 2 )) && [[ #{name} == * ]]; " \
76
+ "then\n local -a sub\n sub=(#{words})\n " \
77
+ "_describe '#{name} command' sub\n fi\n"
78
+ end
79
+ script << "}\n"
80
+ script
81
+ end
82
+
83
+ def fish(commands, subcommands)
84
+ script = +<<~FISH
85
+ # uniword fish completion — generated by `uniword completions fish`
86
+ FISH
87
+ commands.each do |c|
88
+ script << "complete -c uniword -n '__fish_use_subcommand' -a '#{c}'\n"
89
+ end
90
+ subcommands.each do |name, klass|
91
+ words = words_for(klass)
92
+ script << "complete -c uniword -n '__fish_seen_subcommand_from " \
93
+ "#{name}' -a '#{words}'\n"
94
+ end
95
+ script
96
+ end
97
+ end
98
+ end
99
+ end
@@ -10,6 +10,11 @@ module Uniword
10
10
  class GenerateCLI < Thor
11
11
  include CLIHelpers
12
12
 
13
+ # `uniword generate INPUT OUTPUT ...` dispatches straight to the
14
+ # generate task; the explicit `generate generate ...` form stays
15
+ # valid for backwards compatibility.
16
+ default_task :generate
17
+
13
18
  desc "generate INPUT OUTPUT",
14
19
  "Generate DOCX from structured text"
15
20
  long_desc <<~DESC
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uniword
4
+ module Cli
5
+ # Rewrites help/version flag invocations before Thor dispatch so
6
+ # help works on tasks with required arguments:
7
+ #
8
+ # uniword convert --help → uniword help convert
9
+ # uniword images add --help → uniword help images add
10
+ # uniword --help / -h → root help
11
+ # uniword --version / -V → uniword version
12
+ #
13
+ # The rewrite only fires when the help/version flag is the first
14
+ # flag-like token and everything before it looks like a task path
15
+ # (no options), so real option values (e.g. `--from docx`) are
16
+ # never mistaken for the chain.
17
+ module HelpSwitch
18
+ HELP_FLAGS = %w[--help -h].freeze
19
+ VERSION_FLAGS = %w[--version -V].freeze
20
+
21
+ def start(given_args = ARGV, config = {})
22
+ args = given_args.map(&:to_s)
23
+
24
+ if (help_idx = args.index { |a| HELP_FLAGS.include?(a) })
25
+ chain = args[0...help_idx]
26
+ if option_free_task_path?(chain)
27
+ return super(chain.unshift("help"), config)
28
+ end
29
+ end
30
+
31
+ if args.size == 1 && VERSION_FLAGS.include?(args[0])
32
+ return super(["version"], config)
33
+ end
34
+
35
+ super
36
+ end
37
+
38
+ private
39
+
40
+ def option_free_task_path?(chain)
41
+ chain.all? { |a| !a.start_with?("-") }
42
+ end
43
+ end
44
+ end
45
+ end
@@ -10,6 +10,7 @@ module Uniword
10
10
  module CLIHelpers
11
11
  def self.included(base)
12
12
  base.extend(ClassMethods)
13
+ base.extend(Cli::HelpSwitch)
13
14
  end
14
15
 
15
16
  module ClassMethods