uniword 1.2.5 → 1.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +223 -0
- data/CONTRIBUTING.md +1 -1
- data/config/ooxml/schemas/shared_types.yml +5 -4
- data/data/schemas/ecma/dc.xsd +118 -0
- data/data/schemas/ecma/dcmitype.xsd +50 -0
- data/data/schemas/ecma/dcterms.xsd +331 -0
- data/data/schemas/ecma/opc-coreProperties.xsd +2 -2
- data/data/schemas/ecma/xml.xsd +117 -0
- data/lib/uniword/builder/bibliography_builder.rb +1 -1
- data/lib/uniword/builder/chart_builder.rb +19 -9
- data/lib/uniword/builder/comment_anchorer.rb +221 -0
- data/lib/uniword/builder/document_builder.rb +97 -6
- data/lib/uniword/builder/image_builder.rb +6 -5
- data/lib/uniword/builder/run_utils.rb +2 -0
- data/lib/uniword/builder.rb +1 -0
- data/lib/uniword/cli/fonts_cli.rb +52 -0
- data/lib/uniword/cli/main.rb +110 -11
- data/lib/uniword/cli/page_cli.rb +85 -0
- data/lib/uniword/cli/styles_cli.rb +204 -0
- data/lib/uniword/cli/theme_cli.rb +88 -0
- data/lib/uniword/cli/toc_cli.rb +22 -2
- data/lib/uniword/comment.rb +20 -15
- data/lib/uniword/comment_range.rb +3 -3
- data/lib/uniword/comments_part.rb +51 -19
- data/lib/uniword/configuration/configuration_loader.rb +5 -3
- data/lib/uniword/configuration.rb +93 -1
- data/lib/uniword/content_types.rb +19 -39
- data/lib/uniword/document_factory.rb +9 -1
- data/lib/uniword/document_writer.rb +16 -5
- data/lib/uniword/docx/chart_part.rb +34 -0
- data/lib/uniword/docx/custom_xml_item.rb +83 -0
- data/lib/uniword/docx/header_footer_part.rb +140 -0
- data/lib/uniword/docx/header_footer_part_collection.rb +135 -0
- data/lib/uniword/docx/header_footer_view.rb +225 -0
- data/lib/uniword/docx/id_allocator.rb +130 -36
- data/lib/uniword/docx/package.rb +223 -35
- data/lib/uniword/docx/package_defaults.rb +58 -78
- data/lib/uniword/docx/package_integrity_checker.rb +312 -0
- data/lib/uniword/docx/package_serialization.rb +103 -231
- data/lib/uniword/docx/part.rb +106 -0
- data/lib/uniword/docx/part_collection.rb +123 -0
- data/lib/uniword/docx/reconciler/body.rb +59 -68
- data/lib/uniword/docx/reconciler/fix.rb +46 -0
- data/lib/uniword/docx/reconciler/fix_codes.rb +37 -30
- data/lib/uniword/docx/reconciler/helpers.rb +9 -1
- data/lib/uniword/docx/reconciler/notes.rb +17 -8
- data/lib/uniword/docx/reconciler/package_structure.rb +169 -221
- data/lib/uniword/docx/reconciler/parts.rb +36 -13
- data/lib/uniword/docx/reconciler/referential_integrity.rb +297 -123
- data/lib/uniword/docx/reconciler/tables.rb +78 -16
- data/lib/uniword/docx/reconciler/theme.rb +7 -4
- data/lib/uniword/docx/reconciler.rb +36 -14
- data/lib/uniword/docx.rb +10 -0
- data/lib/uniword/drawingml/blip.rb +0 -1
- data/lib/uniword/drawingml/color_scheme.rb +5 -6
- data/lib/uniword/drawingml/font_scheme.rb +4 -8
- data/lib/uniword/drawingml/theme.rb +12 -6
- data/lib/uniword/errors.rb +8 -1
- data/lib/uniword/footer.rb +4 -0
- data/lib/uniword/header.rb +4 -0
- data/lib/uniword/hyperlink.rb +1 -1
- data/lib/uniword/image.rb +6 -6
- data/lib/uniword/lazy_loader.rb +1 -2
- data/lib/uniword/model_attribute_access.rb +20 -4
- data/lib/uniword/ooxml/dotx_package.rb +42 -2
- data/lib/uniword/ooxml/part_definition.rb +171 -0
- data/lib/uniword/ooxml/part_registry.rb +311 -0
- data/lib/uniword/ooxml/relationships/image_relationship.rb +1 -1
- data/lib/uniword/ooxml/relationships/package_relationships.rb +20 -22
- data/lib/uniword/ooxml/schema/element_serializer.rb +8 -7
- data/lib/uniword/ooxml/types/hex_color_value.rb +38 -0
- data/lib/uniword/ooxml/types/ooxml_boolean.rb +27 -14
- data/lib/uniword/ooxml/types/ooxml_boolean_optional.rb +21 -16
- data/lib/uniword/ooxml/types/theme_color_value.rb +22 -0
- data/lib/uniword/ooxml/types/unsigned_decimal_number.rb +30 -0
- data/lib/uniword/ooxml/types.rb +10 -0
- data/lib/uniword/ooxml.rb +2 -0
- data/lib/uniword/properties/alignment.rb +8 -3
- data/lib/uniword/properties/border.rb +52 -6
- data/lib/uniword/properties/cell_vertical_align.rb +4 -1
- data/lib/uniword/properties/color_value.rb +8 -3
- data/lib/uniword/properties/highlight.rb +9 -5
- data/lib/uniword/properties/shading.rb +4 -3
- data/lib/uniword/properties/tab_stop.rb +6 -1
- data/lib/uniword/properties/table_justification.rb +4 -1
- data/lib/uniword/properties/underline.rb +4 -2
- data/lib/uniword/properties/vertical_align.rb +6 -3
- data/lib/uniword/review/review_manager.rb +23 -9
- data/lib/uniword/revision.rb +9 -9
- data/lib/uniword/schema/model_generator.rb +31 -15
- data/lib/uniword/shared_types/hex_color.rb +4 -1
- data/lib/uniword/shared_types/pixel_measure.rb +4 -1
- data/lib/uniword/shared_types/point_measure.rb +4 -1
- data/lib/uniword/shared_types/text_alignment.rb +6 -1
- data/lib/uniword/shared_types/twips_measure.rb +4 -1
- data/lib/uniword/spreadsheetml/phonetic_pr.rb +1 -1
- data/lib/uniword/spreadsheetml/shared_string_table.rb +2 -2
- data/lib/uniword/spreadsheetml/table_formula.rb +1 -1
- data/lib/uniword/template/variable_resolver.rb +40 -7
- data/lib/uniword/themes/theme_transformation.rb +55 -41
- data/lib/uniword/toc/toc_generator.rb +2 -0
- data/lib/uniword/validation/engine.rb +35 -0
- data/lib/uniword/validation/link_checker.rb +1 -1
- data/lib/uniword/validation/opc_validator.rb +0 -1
- data/lib/uniword/validation/report/layer_result.rb +0 -1
- data/lib/uniword/validation/report/terminal_formatter.rb +0 -1
- data/lib/uniword/validation/report/verification_report.rb +0 -2
- data/lib/uniword/validation/report.rb +20 -0
- data/lib/uniword/validation/rules/base.rb +10 -2
- data/lib/uniword/validation/rules/bookmark_pairing_rule.rb +38 -0
- data/lib/uniword/validation/rules/bookmark_uniqueness_rule.rb +48 -0
- data/lib/uniword/validation/rules/bookmarks_rule.rb +0 -2
- data/lib/uniword/validation/rules/content_types_coverage_rule.rb +0 -2
- data/lib/uniword/validation/rules/core_properties_namespace_rule.rb +0 -2
- data/lib/uniword/validation/rules/document_body_rule.rb +25 -0
- data/lib/uniword/validation/rules/document_context.rb +7 -0
- data/lib/uniword/validation/rules/empty_paragraphs_rule.rb +27 -0
- data/lib/uniword/validation/rules/font_table_signature_rule.rb +0 -2
- data/lib/uniword/validation/rules/fonts_rule.rb +0 -2
- data/lib/uniword/validation/rules/footnotes_rule.rb +0 -2
- data/lib/uniword/validation/rules/headers_footers_rule.rb +0 -2
- data/lib/uniword/validation/rules/images_rule.rb +0 -2
- data/lib/uniword/validation/rules/mc_ignorable_namespace_rule.rb +0 -2
- data/lib/uniword/validation/rules/model_context.rb +32 -0
- data/lib/uniword/validation/rules/model_rule.rb +34 -0
- data/lib/uniword/validation/rules/numbering_preservation_rule.rb +0 -2
- data/lib/uniword/validation/rules/numbering_rule.rb +0 -2
- data/lib/uniword/validation/rules/relationship_integrity_rule.rb +0 -1
- data/lib/uniword/validation/rules/rsid_rule.rb +0 -2
- data/lib/uniword/validation/rules/section_properties_rule.rb +0 -2
- data/lib/uniword/validation/rules/settings_rule.rb +0 -2
- data/lib/uniword/validation/rules/settings_values_rule.rb +0 -2
- data/lib/uniword/validation/rules/style_references_rule.rb +0 -2
- data/lib/uniword/validation/rules/table_grid_rule.rb +27 -0
- data/lib/uniword/validation/rules/table_properties_rule.rb +27 -0
- data/lib/uniword/validation/rules/tables_rule.rb +0 -2
- data/lib/uniword/validation/rules/theme_completeness_rule.rb +0 -2
- data/lib/uniword/validation/rules/theme_rule.rb +0 -2
- data/lib/uniword/validation/rules.rb +61 -23
- data/lib/uniword/validation/schema_registry.rb +11 -0
- data/lib/uniword/validation/validators/document_semantics_validator.rb +1 -9
- data/lib/uniword/validation/validators/xml_schema_validator.rb +0 -2
- data/lib/uniword/validation/validators.rb +0 -9
- data/lib/uniword/validation/verify_orchestrator.rb +0 -7
- data/lib/uniword/validation.rb +7 -2
- data/lib/uniword/version.rb +1 -1
- data/lib/uniword/vml/imagedata.rb +0 -1
- data/lib/uniword/wordprocessingml/attached_template.rb +0 -1
- data/lib/uniword/wordprocessingml/deleted_text.rb +1 -1
- data/lib/uniword/wordprocessingml/document_root.rb +211 -21
- data/lib/uniword/wordprocessingml/endnotes.rb +4 -0
- data/lib/uniword/wordprocessingml/font_replacer.rb +147 -0
- data/lib/uniword/wordprocessingml/footnotes.rb +4 -0
- data/lib/uniword/wordprocessingml/hdr_shape_defaults.rb +0 -1
- data/lib/uniword/wordprocessingml/hyperlink.rb +0 -1
- data/lib/uniword/wordprocessingml/level.rb +6 -20
- data/lib/uniword/wordprocessingml/math_pr.rb +0 -11
- data/lib/uniword/wordprocessingml/numbering_definition.rb +16 -8
- data/lib/uniword/wordprocessingml/page_setup.rb +181 -0
- data/lib/uniword/wordprocessingml/paragraph_properties.rb +33 -28
- data/lib/uniword/wordprocessingml/rsids.rb +0 -2
- data/lib/uniword/wordprocessingml/run.rb +3 -0
- data/lib/uniword/wordprocessingml/section_properties.rb +2 -2
- data/lib/uniword/wordprocessingml/settings.rb +8 -34
- data/lib/uniword/wordprocessingml/shape_defaults.rb +0 -1
- data/lib/uniword/wordprocessingml/style.rb +2 -1
- data/lib/uniword/wordprocessingml/style_cleanup.rb +198 -0
- data/lib/uniword/wordprocessingml/table_cell_properties.rb +6 -6
- data/lib/uniword/wordprocessingml/update_fields.rb +27 -0
- data/lib/uniword/wordprocessingml/w14_attributes.rb +30 -2
- data/lib/uniword/wordprocessingml.rb +4 -0
- data/lib/uniword.rb +27 -2
- metadata +38 -19
- data/config/validation_rules.yml +0 -60
- data/config/warning_rules.yml +0 -57
- data/lib/uniword/validation/document_validator.rb +0 -272
- data/lib/uniword/validation/structural_validator.rb +0 -116
- data/lib/uniword/validation/validators/content_type_validator.rb +0 -165
- data/lib/uniword/validation/validators/file_structure_validator.rb +0 -104
- data/lib/uniword/validation/validators/ooxml_part_validator.rb +0 -128
- data/lib/uniword/validation/validators/relationship_validator.rb +0 -147
- data/lib/uniword/validation/validators/zip_integrity_validator.rb +0 -110
- data/lib/uniword/validators/element_validator.rb +0 -93
- data/lib/uniword/validators/paragraph_validator.rb +0 -116
- data/lib/uniword/validators/table_validator.rb +0 -134
- data/lib/uniword/validators.rb +0 -9
- data/lib/uniword/warnings/warning.rb +0 -130
- data/lib/uniword/warnings/warning_collector.rb +0 -234
- data/lib/uniword/warnings/warning_report.rb +0 -159
- data/lib/uniword/warnings.rb +0 -9
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
|
|
5
|
+
module Uniword
|
|
6
|
+
# Page subcommands for Uniword CLI.
|
|
7
|
+
#
|
|
8
|
+
# Uniform page setup across a document — the CLI equivalent of Word's
|
|
9
|
+
# Layout dialog (size, orientation, margins).
|
|
10
|
+
class PageCLI < Thor
|
|
11
|
+
include CLIHelpers
|
|
12
|
+
|
|
13
|
+
desc "setup INPUT OUTPUT", "Apply page setup to every section"
|
|
14
|
+
long_desc <<~DESC
|
|
15
|
+
Apply uniform page setup — paper size, orientation, and margins —
|
|
16
|
+
to every section of a document, the CLI equivalent of Word's
|
|
17
|
+
Layout dialog.
|
|
18
|
+
|
|
19
|
+
Examples:
|
|
20
|
+
$ uniword page setup input.docx output.docx --size a4
|
|
21
|
+
$ uniword page setup input.docx output.docx --size a4 --orientation landscape
|
|
22
|
+
$ uniword page setup input.docx output.docx --margins 1in
|
|
23
|
+
$ uniword page setup input.docx output.docx --margins 2.5cm --margin-top 3cm
|
|
24
|
+
DESC
|
|
25
|
+
option :size, type: :string,
|
|
26
|
+
desc: "Paper size: letter, legal, a4, a5, executive"
|
|
27
|
+
option :orientation, type: :string,
|
|
28
|
+
desc: "portrait or landscape"
|
|
29
|
+
option :margins, type: :string,
|
|
30
|
+
desc: "Uniform margin for all sides " \
|
|
31
|
+
"(1in, 2.5cm, 25mm, 1440)"
|
|
32
|
+
option :margin_top, type: :string, desc: "Top margin override"
|
|
33
|
+
option :margin_right, type: :string, desc: "Right margin override"
|
|
34
|
+
option :margin_bottom, type: :string, desc: "Bottom margin override"
|
|
35
|
+
option :margin_left, type: :string, desc: "Left margin override"
|
|
36
|
+
option :verbose, aliases: "-v", desc: "Verbose output", type: :boolean,
|
|
37
|
+
default: false
|
|
38
|
+
def setup(input_path, output_path)
|
|
39
|
+
opts = setup_options
|
|
40
|
+
unless opts
|
|
41
|
+
say "Error: specify at least one of --size, --orientation, " \
|
|
42
|
+
"--margins, --margin-top/right/bottom/left", :red
|
|
43
|
+
exit 1
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
say "Loading document #{input_path}...", :green if options[:verbose]
|
|
47
|
+
|
|
48
|
+
sections = apply_setup(input_path, output_path, opts)
|
|
49
|
+
say "Page setup applied to #{sections} section(s) in #{output_path}",
|
|
50
|
+
:green
|
|
51
|
+
rescue Uniword::Error => e
|
|
52
|
+
handle_error(e)
|
|
53
|
+
rescue StandardError => e
|
|
54
|
+
handle_error(e, verbose: options[:verbose])
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def setup_options
|
|
60
|
+
map = base_setup_options.merge(margin_setup_options)
|
|
61
|
+
map unless map.values.all?(&:nil?)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def base_setup_options
|
|
65
|
+
{
|
|
66
|
+
size: options[:size], orientation: options[:orientation],
|
|
67
|
+
margins: options[:margins]
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def margin_setup_options
|
|
72
|
+
{
|
|
73
|
+
top: options[:margin_top], right: options[:margin_right],
|
|
74
|
+
bottom: options[:margin_bottom], left: options[:margin_left]
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def apply_setup(input_path, output_path, opts)
|
|
79
|
+
doc = load_document(input_path)
|
|
80
|
+
sections = doc.apply_page_setup(**opts)
|
|
81
|
+
doc.save(output_path)
|
|
82
|
+
sections
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
|
|
5
|
+
module Uniword
|
|
6
|
+
# Styles subcommands for Uniword CLI.
|
|
7
|
+
#
|
|
8
|
+
# Inspect the styles inside a document — the CLI equivalent of Word's
|
|
9
|
+
# Styles pane.
|
|
10
|
+
class StylesCLI < Thor
|
|
11
|
+
include CLIHelpers
|
|
12
|
+
|
|
13
|
+
desc "list FILE", "List styles in a document"
|
|
14
|
+
long_desc <<~DESC
|
|
15
|
+
List all styles defined in a document with their type, base
|
|
16
|
+
style, and key formatting — the CLI equivalent of Word's Styles
|
|
17
|
+
pane.
|
|
18
|
+
|
|
19
|
+
Examples:
|
|
20
|
+
$ uniword styles list report.docx
|
|
21
|
+
$ uniword styles list report.docx --type paragraph
|
|
22
|
+
$ uniword styles list report.docx --verbose
|
|
23
|
+
DESC
|
|
24
|
+
option :type, type: :string,
|
|
25
|
+
desc: "Filter by type: paragraph, character, table, numbering"
|
|
26
|
+
option :verbose, aliases: "-v", desc: "Show formatting details",
|
|
27
|
+
type: :boolean, default: false
|
|
28
|
+
def list(path)
|
|
29
|
+
styles = gather_styles(path)
|
|
30
|
+
if styles.empty?
|
|
31
|
+
say "No styles found.", :yellow
|
|
32
|
+
return
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
say "Styles in #{File.basename(path)} (#{styles.size}):", :green
|
|
36
|
+
styles.each { |style| print_style(style) }
|
|
37
|
+
rescue Uniword::Error => e
|
|
38
|
+
handle_error(e)
|
|
39
|
+
rescue StandardError => e
|
|
40
|
+
handle_error(e, verbose: options[:verbose])
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
desc "remove FILE OUTPUT", "Remove styles from a document"
|
|
44
|
+
long_desc <<~DESC
|
|
45
|
+
Remove styles from a document: one style with --id, or every
|
|
46
|
+
style that no content references with --unused (Word's Styles-pane
|
|
47
|
+
decluttering as a one-shot command). Default styles are kept.
|
|
48
|
+
|
|
49
|
+
Examples:
|
|
50
|
+
$ uniword styles remove input.docx output.docx --id ObsoleteStyle
|
|
51
|
+
$ uniword styles remove input.docx output.docx --unused
|
|
52
|
+
$ uniword styles remove input.docx output.docx --unused --dry-run
|
|
53
|
+
DESC
|
|
54
|
+
option :id, type: :string, desc: "Style id to remove"
|
|
55
|
+
option :unused, type: :boolean, default: false,
|
|
56
|
+
desc: "Remove all unreferenced styles"
|
|
57
|
+
option :dry_run, type: :boolean, default: false,
|
|
58
|
+
desc: "List what would be removed, without saving"
|
|
59
|
+
option :verbose, aliases: "-v", desc: "Verbose output",
|
|
60
|
+
type: :boolean, default: false
|
|
61
|
+
def remove(input_path, output_path)
|
|
62
|
+
doc = load_document(input_path)
|
|
63
|
+
removed = run_removal(doc)
|
|
64
|
+
report_removal(doc, removed, output_path)
|
|
65
|
+
rescue Uniword::Error => e
|
|
66
|
+
handle_error(e)
|
|
67
|
+
rescue StandardError => e
|
|
68
|
+
handle_error(e, verbose: options[:verbose])
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
desc "rename FILE OUTPUT", "Rename a style's display name"
|
|
72
|
+
long_desc <<~DESC
|
|
73
|
+
Rename a style's display name (w:name) — Word's style rename.
|
|
74
|
+
References target the styleId, so renamed styles stay linked from
|
|
75
|
+
all content. Identify the style by --id (w:styleId) or --from
|
|
76
|
+
(current display name).
|
|
77
|
+
|
|
78
|
+
Examples:
|
|
79
|
+
$ uniword styles rename input.docx output.docx --id Heading1 --name "Chapter Title"
|
|
80
|
+
$ uniword styles rename input.docx output.docx --from "Old Name" --name "New Name"
|
|
81
|
+
DESC
|
|
82
|
+
option :id, type: :string, desc: "Style id (w:styleId) to rename"
|
|
83
|
+
option :from, type: :string, desc: "Current display name of the style"
|
|
84
|
+
option :name, type: :string, required: true, desc: "New display name"
|
|
85
|
+
option :verbose, aliases: "-v", desc: "Verbose output",
|
|
86
|
+
type: :boolean, default: false
|
|
87
|
+
def rename(input_path, output_path)
|
|
88
|
+
identifier = rename_identifier
|
|
89
|
+
doc = load_document(input_path)
|
|
90
|
+
perform_rename(doc, identifier, output_path)
|
|
91
|
+
rescue Uniword::Error, StandardError => e
|
|
92
|
+
handle_error(e, verbose: options[:verbose])
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def rename_identifier
|
|
98
|
+
identifier = options[:id] || options[:from]
|
|
99
|
+
return identifier if identifier
|
|
100
|
+
|
|
101
|
+
say "Error: specify --id or --from to identify the style", :red
|
|
102
|
+
exit 1
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def perform_rename(doc, identifier, output_path)
|
|
106
|
+
unless doc.rename_style(identifier, options[:name])
|
|
107
|
+
say "Style '#{identifier}' not found", :yellow
|
|
108
|
+
exit 1
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
doc.save(output_path)
|
|
112
|
+
say "Renamed style '#{identifier}' to '#{options[:name]}' " \
|
|
113
|
+
"in #{output_path}", :green
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def run_removal(doc)
|
|
117
|
+
cleanup = Wordprocessingml::StyleCleanup.new(doc)
|
|
118
|
+
removal_error unless options[:unused] || options[:id]
|
|
119
|
+
|
|
120
|
+
removed_ids(cleanup)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def removed_ids(cleanup)
|
|
124
|
+
if options[:unused]
|
|
125
|
+
options[:dry_run] ? cleanup.unused_ids : cleanup.remove_unused
|
|
126
|
+
else
|
|
127
|
+
[options[:id]].select { |id| options[:dry_run] || cleanup.remove?(id) }
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def removal_error
|
|
132
|
+
say "Error: specify --id or --unused", :red
|
|
133
|
+
exit 1
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def report_removal(doc, removed, output_path)
|
|
137
|
+
if removed.empty?
|
|
138
|
+
say "No styles removed.", :yellow
|
|
139
|
+
return
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
verb = options[:dry_run] ? "Would remove" : "Removed"
|
|
143
|
+
say "#{verb} #{removed.size} style(s): #{removed.join(', ')}", :green
|
|
144
|
+
return if options[:dry_run]
|
|
145
|
+
|
|
146
|
+
doc.save(output_path)
|
|
147
|
+
say "Saved to #{output_path}", :green
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def gather_styles(path)
|
|
151
|
+
doc = load_document(path)
|
|
152
|
+
filter_type(doc.styles_configuration.styles || [])
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def filter_type(styles)
|
|
156
|
+
return styles unless options[:type]
|
|
157
|
+
|
|
158
|
+
styles.select { |s| s.type == options[:type] }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def print_style(style)
|
|
162
|
+
base = style.basedOn&.val || "-"
|
|
163
|
+
line = format(" %-24<id>s %-10<type>s base: %<base>s",
|
|
164
|
+
id: style.styleId, type: style.type, base: base)
|
|
165
|
+
line += " [#{style_details(style)}]" if options[:verbose]
|
|
166
|
+
say line
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def style_details(style)
|
|
170
|
+
(name_detail(style) + font_details(style) + format_details(style))
|
|
171
|
+
.join(", ")
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def name_detail(style)
|
|
175
|
+
style.name&.val ? ["name=#{style.name.val}"] : []
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def font_details(style)
|
|
179
|
+
rpr = style.rPr
|
|
180
|
+
return [] unless rpr
|
|
181
|
+
|
|
182
|
+
[font_name(rpr), font_size(rpr)].compact
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def font_name(rpr)
|
|
186
|
+
rpr.fonts&.ascii
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def font_size(rpr)
|
|
190
|
+
size = rpr.size&.value
|
|
191
|
+
"#{size.to_i / 2.0}pt" if size
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def format_details(style)
|
|
195
|
+
rpr = style.rPr
|
|
196
|
+
return [] unless rpr
|
|
197
|
+
|
|
198
|
+
flags = []
|
|
199
|
+
flags << "bold" if rpr.bold&.val
|
|
200
|
+
flags << "italic" if rpr.italic&.val
|
|
201
|
+
flags
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
@@ -147,6 +147,46 @@ module Uniword
|
|
|
147
147
|
handle_error(e, verbose: options[:verbose])
|
|
148
148
|
end
|
|
149
149
|
|
|
150
|
+
desc "fonts INPUT OUTPUT", "Apply a bundled font scheme to a document"
|
|
151
|
+
long_desc <<~DESC
|
|
152
|
+
Replace the document theme's font scheme (major/minor fonts) while
|
|
153
|
+
keeping colors and formats — the equivalent of Word's Design →
|
|
154
|
+
Fonts gallery. Use --list to see available font schemes.
|
|
155
|
+
|
|
156
|
+
Examples:
|
|
157
|
+
$ uniword theme fonts input.docx output.docx --name carlito_sans
|
|
158
|
+
$ uniword theme fonts --list
|
|
159
|
+
DESC
|
|
160
|
+
option :name, type: :string,
|
|
161
|
+
desc: "Bundled font scheme name (e.g., carlito_sans)"
|
|
162
|
+
option :list, type: :boolean, default: false,
|
|
163
|
+
desc: "List available bundled font schemes"
|
|
164
|
+
option :verbose, aliases: "-v", desc: "Verbose output", type: :boolean,
|
|
165
|
+
default: false
|
|
166
|
+
def fonts(*args)
|
|
167
|
+
run_scheme_command(:fonts, args)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
desc "colors INPUT OUTPUT", "Apply a bundled color scheme to a document"
|
|
171
|
+
long_desc <<~DESC
|
|
172
|
+
Replace the document theme's color scheme while keeping fonts and
|
|
173
|
+
formats — the equivalent of Word's Design → Colors gallery. Use
|
|
174
|
+
--list to see available color schemes.
|
|
175
|
+
|
|
176
|
+
Examples:
|
|
177
|
+
$ uniword theme colors input.docx output.docx --name emerald
|
|
178
|
+
$ uniword theme colors --list
|
|
179
|
+
DESC
|
|
180
|
+
option :name, type: :string,
|
|
181
|
+
desc: "Bundled color scheme name (e.g., emerald)"
|
|
182
|
+
option :list, type: :boolean, default: false,
|
|
183
|
+
desc: "List available bundled color schemes"
|
|
184
|
+
option :verbose, aliases: "-v", desc: "Verbose output", type: :boolean,
|
|
185
|
+
default: false
|
|
186
|
+
def colors(*args)
|
|
187
|
+
run_scheme_command(:colors, args)
|
|
188
|
+
end
|
|
189
|
+
|
|
150
190
|
desc "auto INPUT OUTPUT", "Auto-transition MS theme to Uniword equivalent"
|
|
151
191
|
long_desc <<~DESC
|
|
152
192
|
Detect the Microsoft Word theme in a document and automatically
|
|
@@ -192,8 +232,56 @@ module Uniword
|
|
|
192
232
|
handle_error(e, verbose: options[:verbose])
|
|
193
233
|
end
|
|
194
234
|
|
|
235
|
+
# Scheme-switching command configuration: loader, document method,
|
|
236
|
+
# and output label per command kind.
|
|
237
|
+
SCHEME_COMMANDS = {
|
|
238
|
+
fonts: {
|
|
239
|
+
loader: Resource::FontSchemeLoader,
|
|
240
|
+
doc_method: :apply_font_scheme,
|
|
241
|
+
label: "Font scheme",
|
|
242
|
+
},
|
|
243
|
+
colors: {
|
|
244
|
+
loader: Resource::ColorSchemeLoader,
|
|
245
|
+
doc_method: :apply_color_scheme,
|
|
246
|
+
label: "Color scheme",
|
|
247
|
+
},
|
|
248
|
+
}.freeze
|
|
249
|
+
|
|
195
250
|
private
|
|
196
251
|
|
|
252
|
+
def run_scheme_command(kind, args)
|
|
253
|
+
config = SCHEME_COMMANDS.fetch(kind)
|
|
254
|
+
if options[:list]
|
|
255
|
+
config[:loader].available_schemes.each { |s| say s }
|
|
256
|
+
return
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
require_name!(:name, config[:loader])
|
|
260
|
+
apply_scheme_to_file(config, args)
|
|
261
|
+
rescue Uniword::Error => e
|
|
262
|
+
handle_error(e)
|
|
263
|
+
rescue StandardError => e
|
|
264
|
+
handle_error(e, verbose: options[:verbose])
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def apply_scheme_to_file(config, (input_path, output_path))
|
|
268
|
+
say "Loading document #{input_path}...", :green if options[:verbose]
|
|
269
|
+
|
|
270
|
+
doc = load_document(input_path)
|
|
271
|
+
doc.method(config[:doc_method]).call(options[:name])
|
|
272
|
+
doc.save(output_path)
|
|
273
|
+
say "#{config[:label]} '#{options[:name]}' applied to #{output_path}",
|
|
274
|
+
:green
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def require_name!(key, loader)
|
|
278
|
+
return if options[key]
|
|
279
|
+
|
|
280
|
+
say "Error: specify --name for a bundled scheme " \
|
|
281
|
+
"(available: #{loader.available_schemes.join(', ')})", :red
|
|
282
|
+
exit 1
|
|
283
|
+
end
|
|
284
|
+
|
|
197
285
|
def apply_theme_to(doc)
|
|
198
286
|
if options[:name]
|
|
199
287
|
if options[:verbose]
|
data/lib/uniword/cli/toc_cli.rb
CHANGED
|
@@ -68,6 +68,9 @@ module Uniword
|
|
|
68
68
|
type: :numeric, default: 0
|
|
69
69
|
option :max_level, desc: "Maximum heading level (1-6)", type: :numeric,
|
|
70
70
|
default: 3
|
|
71
|
+
option :update_fields, type: :boolean, default: true,
|
|
72
|
+
desc: "Set w:updateFields so Word " \
|
|
73
|
+
"refreshes fields on open (default: true)"
|
|
71
74
|
option :verbose, aliases: "-v", desc: "Show verbose output",
|
|
72
75
|
type: :boolean, default: false
|
|
73
76
|
def insert(path)
|
|
@@ -84,7 +87,7 @@ module Uniword
|
|
|
84
87
|
position: options[:position],
|
|
85
88
|
max_level: options[:max_level])
|
|
86
89
|
|
|
87
|
-
doc
|
|
90
|
+
save_with_field_updates(doc, options[:output])
|
|
88
91
|
|
|
89
92
|
say "TOC inserted with #{entries.count} entries at " \
|
|
90
93
|
"position #{options[:position]}.", :green
|
|
@@ -109,6 +112,9 @@ module Uniword
|
|
|
109
112
|
type: :string
|
|
110
113
|
option :max_level, desc: "Maximum heading level (1-6)", type: :numeric,
|
|
111
114
|
default: 6
|
|
115
|
+
option :update_fields, type: :boolean, default: true,
|
|
116
|
+
desc: "Set w:updateFields so Word " \
|
|
117
|
+
"refreshes fields on open (default: true)"
|
|
112
118
|
option :verbose, aliases: "-v", desc: "Show verbose output",
|
|
113
119
|
type: :boolean, default: false
|
|
114
120
|
def update(path)
|
|
@@ -121,7 +127,7 @@ module Uniword
|
|
|
121
127
|
return
|
|
122
128
|
end
|
|
123
129
|
|
|
124
|
-
doc
|
|
130
|
+
save_with_field_updates(doc, options[:output])
|
|
125
131
|
|
|
126
132
|
say "TOC updated with #{entries.count} entries.", :green
|
|
127
133
|
say "Saved to: #{options[:output]}", :green
|
|
@@ -133,6 +139,20 @@ module Uniword
|
|
|
133
139
|
|
|
134
140
|
private
|
|
135
141
|
|
|
142
|
+
# Set w:updateFields (unless disabled) and save the document.
|
|
143
|
+
def save_with_field_updates(doc, output)
|
|
144
|
+
enable_field_updates(doc) if options[:update_fields]
|
|
145
|
+
doc.save(output)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Set w:updateFields so Word refreshes all fields (TOC, page
|
|
149
|
+
# numbers, references) when the document is opened. Word may show a
|
|
150
|
+
# one-time prompt about updating fields.
|
|
151
|
+
def enable_field_updates(doc)
|
|
152
|
+
doc.settings ||= Wordprocessingml::Settings.new
|
|
153
|
+
doc.settings.update_fields = Wordprocessingml::UpdateFields.new
|
|
154
|
+
end
|
|
155
|
+
|
|
136
156
|
# Display TOC entries in terminal format.
|
|
137
157
|
#
|
|
138
158
|
# @param entries [Array<Uniword::Toc::TocEntry>] Entries to display
|
data/lib/uniword/comment.rb
CHANGED
|
@@ -19,19 +19,6 @@ module Uniword
|
|
|
19
19
|
# @see CommentRange For comment range markers
|
|
20
20
|
# @see CommentsPart For comments collection
|
|
21
21
|
class Comment < Element
|
|
22
|
-
# OOXML namespace configuration for comments
|
|
23
|
-
xml do
|
|
24
|
-
element "comment"
|
|
25
|
-
namespace Ooxml::Namespaces::WordProcessingML
|
|
26
|
-
|
|
27
|
-
map_attribute "id", to: :comment_id
|
|
28
|
-
map_attribute "author", to: :author
|
|
29
|
-
map_attribute "date", to: :date
|
|
30
|
-
map_attribute "initials", to: :initials
|
|
31
|
-
|
|
32
|
-
map_element "p", to: :paragraphs
|
|
33
|
-
end
|
|
34
|
-
|
|
35
22
|
# Unique comment identifier (required in OOXML)
|
|
36
23
|
attribute :comment_id, :string
|
|
37
24
|
|
|
@@ -48,6 +35,19 @@ module Uniword
|
|
|
48
35
|
attribute :paragraphs, Uniword::Wordprocessingml::Paragraph, collection: true,
|
|
49
36
|
initialize_empty: true
|
|
50
37
|
|
|
38
|
+
# OOXML namespace configuration for comments
|
|
39
|
+
xml do
|
|
40
|
+
element "comment"
|
|
41
|
+
namespace Ooxml::Namespaces::WordProcessingML
|
|
42
|
+
|
|
43
|
+
map_attribute "id", to: :comment_id
|
|
44
|
+
map_attribute "author", to: :author
|
|
45
|
+
map_attribute "date", to: :date
|
|
46
|
+
map_attribute "initials", to: :initials
|
|
47
|
+
|
|
48
|
+
map_element "p", to: :paragraphs
|
|
49
|
+
end
|
|
50
|
+
|
|
51
51
|
# Initialize a new comment
|
|
52
52
|
#
|
|
53
53
|
# @param attributes [Hash] Comment attributes
|
|
@@ -129,9 +129,14 @@ module Uniword
|
|
|
129
129
|
|
|
130
130
|
# Generate a unique comment ID
|
|
131
131
|
#
|
|
132
|
-
#
|
|
132
|
+
# OOXML w:id is ST_DecimalNumber (a 32-bit integer), so the fallback
|
|
133
|
+
# ID must be decimal digits only; CommentsPart#add_comment reassigns
|
|
134
|
+
# the ID when it collides inside the part, so time+random precision
|
|
135
|
+
# is sufficient here.
|
|
136
|
+
#
|
|
137
|
+
# @return [String] A unique decimal comment ID
|
|
133
138
|
def generate_comment_id
|
|
134
|
-
"#{Time.now.
|
|
139
|
+
"#{Time.now.strftime('%H%M%S')}#{format('%03d', rand(1000))}".to_i.to_s
|
|
135
140
|
end
|
|
136
141
|
|
|
137
142
|
# Format date for OOXML
|
|
@@ -27,6 +27,9 @@ module Uniword
|
|
|
27
27
|
#
|
|
28
28
|
# @see Comment For the comment content
|
|
29
29
|
class CommentRange < Element
|
|
30
|
+
# Comment ID this range marker refers to
|
|
31
|
+
attribute :comment_id, :string
|
|
32
|
+
|
|
30
33
|
# OOXML namespace configuration
|
|
31
34
|
xml do
|
|
32
35
|
element "commentRangeStart"
|
|
@@ -35,9 +38,6 @@ module Uniword
|
|
|
35
38
|
map_attribute "id", to: :comment_id
|
|
36
39
|
end
|
|
37
40
|
|
|
38
|
-
# Comment ID this range marker refers to
|
|
39
|
-
attribute :comment_id, :string
|
|
40
|
-
|
|
41
41
|
# Type of marker (:start, :end, :reference)
|
|
42
42
|
attr_accessor :marker_type
|
|
43
43
|
|
|
@@ -22,6 +22,11 @@ module Uniword
|
|
|
22
22
|
#
|
|
23
23
|
# @see Comment For individual comment structure
|
|
24
24
|
class CommentsPart < Lutaml::Model::Serializable
|
|
25
|
+
include Enumerable
|
|
26
|
+
|
|
27
|
+
# Collection of all comments
|
|
28
|
+
attribute :comments, Comment, collection: true, initialize_empty: true
|
|
29
|
+
|
|
25
30
|
# OOXML namespace configuration for comments
|
|
26
31
|
xml do
|
|
27
32
|
element "comments"
|
|
@@ -30,19 +35,12 @@ module Uniword
|
|
|
30
35
|
map_element "comment", to: :comments
|
|
31
36
|
end
|
|
32
37
|
|
|
33
|
-
# Collection of all comments
|
|
34
|
-
attribute :comments, Comment, collection: true, initialize_empty: true
|
|
35
|
-
|
|
36
|
-
# Initialize a new comments part
|
|
37
|
-
#
|
|
38
|
-
# @param attributes [Hash] Comments part attributes
|
|
39
|
-
def initialize(attributes = {})
|
|
40
|
-
super
|
|
41
|
-
@comment_counter = 0
|
|
42
|
-
end
|
|
43
|
-
|
|
44
38
|
# Add a comment to the collection
|
|
45
39
|
#
|
|
40
|
+
# Assigns the next sequential decimal ID when the comment has no ID
|
|
41
|
+
# yet or its ID collides with one already in the collection; explicit
|
|
42
|
+
# unique IDs are preserved.
|
|
43
|
+
#
|
|
46
44
|
# @param comment [Comment] The comment to add
|
|
47
45
|
# @return [Comment] The added comment with assigned ID
|
|
48
46
|
def add_comment(comment)
|
|
@@ -51,9 +49,7 @@ module Uniword
|
|
|
51
49
|
"comment must be a Comment instance"
|
|
52
50
|
end
|
|
53
51
|
|
|
54
|
-
|
|
55
|
-
comment.comment_id = next_comment_id unless comment.comment_id && !comment.comment_id.empty?
|
|
56
|
-
|
|
52
|
+
comment.comment_id = next_comment_id if assign_id?(comment)
|
|
57
53
|
comments << comment
|
|
58
54
|
comment
|
|
59
55
|
end
|
|
@@ -91,6 +87,29 @@ module Uniword
|
|
|
91
87
|
comments.size
|
|
92
88
|
end
|
|
93
89
|
|
|
90
|
+
# Get the number of comments (Array compatibility)
|
|
91
|
+
#
|
|
92
|
+
# @return [Integer] The count of comments
|
|
93
|
+
def size
|
|
94
|
+
comments.size
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Iterate over the comments (Array compatibility)
|
|
98
|
+
#
|
|
99
|
+
# @yield [Comment] Each comment in insertion order
|
|
100
|
+
# @return [Enumerator, Array<Comment>]
|
|
101
|
+
def each(&block)
|
|
102
|
+
comments.each(&block)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Array-style access to comments (Array compatibility)
|
|
106
|
+
#
|
|
107
|
+
# @param index [Integer] Position in the collection
|
|
108
|
+
# @return [Comment, nil] The comment at the position
|
|
109
|
+
def [](index)
|
|
110
|
+
comments[index]
|
|
111
|
+
end
|
|
112
|
+
|
|
94
113
|
# Check if there are any comments
|
|
95
114
|
#
|
|
96
115
|
# @return [Boolean] true if empty
|
|
@@ -110,7 +129,6 @@ module Uniword
|
|
|
110
129
|
# @return [void]
|
|
111
130
|
def clear
|
|
112
131
|
comments.clear
|
|
113
|
-
@comment_counter = 0
|
|
114
132
|
end
|
|
115
133
|
|
|
116
134
|
# Provide detailed inspection for debugging
|
|
@@ -122,13 +140,27 @@ module Uniword
|
|
|
122
140
|
|
|
123
141
|
private
|
|
124
142
|
|
|
125
|
-
#
|
|
143
|
+
# Whether the comment needs an ID assigned by the collection: it has
|
|
144
|
+
# none, or another comment in the collection already carries it.
|
|
145
|
+
#
|
|
146
|
+
# @param comment [Comment] The comment being added
|
|
147
|
+
# @return [Boolean] true when an ID must be assigned
|
|
148
|
+
def assign_id?(comment)
|
|
149
|
+
id = comment.comment_id.to_s
|
|
150
|
+
id.empty? || !find_comment(id).nil?
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Generate the next sequential comment ID
|
|
154
|
+
#
|
|
155
|
+
# Decimal IDs already in the collection (loaded or assigned) set the
|
|
156
|
+
# high-water mark, so assigned IDs never collide with existing ones.
|
|
126
157
|
#
|
|
127
158
|
# @return [String] The next comment ID
|
|
128
159
|
def next_comment_id
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
160
|
+
max = comments.filter_map do |c|
|
|
161
|
+
Integer(c.comment_id, exception: false)
|
|
162
|
+
end.max
|
|
163
|
+
((max || 0) + 1).to_s
|
|
132
164
|
end
|
|
133
165
|
end
|
|
134
166
|
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require "yaml"
|
|
4
4
|
|
|
5
5
|
module Uniword
|
|
6
|
-
|
|
6
|
+
class Configuration
|
|
7
7
|
# Loads and manages external configuration files.
|
|
8
8
|
#
|
|
9
9
|
# Responsibility: Load configuration from external YAML files.
|
|
@@ -65,12 +65,14 @@ module Uniword
|
|
|
65
65
|
# Get a configuration value with dot notation
|
|
66
66
|
#
|
|
67
67
|
# @param config [Hash] Configuration hash
|
|
68
|
-
# @param key_path [String] Dot-separated key path
|
|
68
|
+
# @param key_path [String] Dot-separated key path
|
|
69
|
+
# (e.g., 'format_defaults.docx.default_font')
|
|
69
70
|
# @param default [Object] Default value if key not found
|
|
70
71
|
# @return [Object] Configuration value or default
|
|
71
72
|
#
|
|
72
73
|
# @example Get nested value
|
|
73
|
-
# font = ConfigurationLoader.get(config,
|
|
74
|
+
# font = ConfigurationLoader.get(config,
|
|
75
|
+
# 'format_defaults.docx.default_font')
|
|
74
76
|
def get(config, key_path, default = nil)
|
|
75
77
|
keys = key_path.split(".")
|
|
76
78
|
value = keys.reduce(config) do |hash, key|
|