uniword 1.0.10 → 1.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/config/font_metadata.yml +56 -0
- data/lib/uniword/accessibility/rules/descriptive_headings_rule.rb +1 -1
- data/lib/uniword/assembly/toc.rb +4 -14
- data/lib/uniword/assembly/variable_substitutor.rb +3 -4
- data/lib/uniword/batch/stages/normalize_styles_stage.rb +4 -2
- data/lib/uniword/builder/base_builder.rb +25 -0
- data/lib/uniword/builder/chart_builder.rb +33 -14
- data/lib/uniword/builder/deterministic_id.rb +17 -0
- data/lib/uniword/builder/document_builder.rb +129 -34
- data/lib/uniword/builder/footnote_builder.rb +22 -23
- data/lib/uniword/builder/header_footer_builder.rb +10 -10
- data/lib/uniword/builder/image_builder.rb +24 -16
- data/lib/uniword/builder/list_builder.rb +2 -1
- data/lib/uniword/builder/paragraph_builder.rb +95 -20
- data/lib/uniword/builder/run_utils.rb +97 -0
- data/lib/uniword/builder/section_builder.rb +40 -67
- data/lib/uniword/builder/table_builder.rb +32 -13
- data/lib/uniword/builder/table_cell_builder.rb +19 -0
- data/lib/uniword/builder.rb +27 -19
- data/lib/uniword/cli/diff_cli.rb +0 -1
- data/lib/uniword/cli/generate_cli.rb +0 -1
- data/lib/uniword/cli/headers_cli.rb +0 -1
- data/lib/uniword/cli/images_cli.rb +0 -1
- data/lib/uniword/cli/main.rb +6 -7
- data/lib/uniword/cli/protect_cli.rb +0 -1
- data/lib/uniword/cli/resources_cli.rb +0 -1
- data/lib/uniword/cli/review_cli.rb +0 -1
- data/lib/uniword/cli/spellcheck_cli.rb +0 -1
- data/lib/uniword/cli/styleset_cli.rb +0 -1
- data/lib/uniword/cli/template_cli.rb +0 -1
- data/lib/uniword/cli/theme_cli.rb +0 -1
- data/lib/uniword/cli/toc_cli.rb +0 -1
- data/lib/uniword/cli/watermark_cli.rb +0 -1
- data/lib/uniword/cli.rb +6 -17
- data/lib/uniword/diff/document_differ.rb +23 -5
- data/lib/uniword/diff/package_differ.rb +0 -1
- data/lib/uniword/diff.rb +4 -0
- data/lib/uniword/document_factory.rb +16 -24
- data/lib/uniword/docx/document_statistics.rb +42 -54
- data/lib/uniword/docx/id_allocator.rb +143 -0
- data/lib/uniword/docx/package.rb +42 -6
- data/lib/uniword/docx/package_defaults.rb +16 -25
- data/lib/uniword/docx/package_serialization.rb +131 -127
- data/lib/uniword/docx/reconciler/body.rb +197 -0
- data/lib/uniword/docx/reconciler/fix_codes.rb +74 -0
- data/lib/uniword/docx/reconciler/helpers.rb +337 -0
- data/lib/uniword/docx/reconciler/notes.rb +301 -0
- data/lib/uniword/docx/reconciler/package_structure.rb +322 -0
- data/lib/uniword/docx/reconciler/parts.rb +484 -0
- data/lib/uniword/docx/reconciler/referential_integrity.rb +449 -0
- data/lib/uniword/docx/reconciler/tables.rb +162 -0
- data/lib/uniword/docx/reconciler/theme.rb +141 -0
- data/lib/uniword/docx/reconciler.rb +44 -1069
- data/lib/uniword/docx.rb +3 -0
- data/lib/uniword/drawingml/color_scheme.rb +38 -5
- data/lib/uniword/hyperlink.rb +27 -26
- data/lib/uniword/infrastructure/zip_packager.rb +41 -38
- data/lib/uniword/lazy_loader.rb +36 -62
- data/lib/uniword/math.rb +3 -86
- data/lib/uniword/mhtml/document.rb +1 -3
- data/lib/uniword/model_attribute_access.rb +27 -0
- data/lib/uniword/ooxml/namespaces.rb +62 -2
- data/lib/uniword/ooxml/relationships/package_relationships.rb +7 -0
- data/lib/uniword/presentationml/slide_id.rb +0 -1
- data/lib/uniword/presentationml/slide_master_id.rb +0 -1
- data/lib/uniword/properties/bold.rb +0 -2
- data/lib/uniword/properties/boolean_formatting.rb +0 -2
- data/lib/uniword/properties/borders.rb +1 -1
- data/lib/uniword/properties/italic.rb +0 -2
- data/lib/uniword/properties/numbering_properties.rb +1 -1
- data/lib/uniword/properties/style_reference.rb +13 -0
- data/lib/uniword/quality/rules/heading_hierarchy_rule.rb +1 -1
- data/lib/uniword/quality/rules/style_consistency_rule.rb +1 -1
- data/lib/uniword/review/review_manager.rb +0 -2
- data/lib/uniword/template/helpers/loop_helper.rb +2 -2
- data/lib/uniword/template/variable_resolver.rb +14 -1
- data/lib/uniword/theme/theme_xml_parser.rb +1 -1
- data/lib/uniword/toc/toc_generator.rb +3 -14
- data/lib/uniword/transformation/mhtml_element_renderer.rb +94 -37
- data/lib/uniword/transformation/mhtml_metadata_builder.rb +1 -1
- data/lib/uniword/transformation/mhtml_style_builder.rb +9 -2
- data/lib/uniword/transformation/ooxml_to_html_converter.rb +3 -5
- data/lib/uniword/transformation/ooxml_to_mhtml_converter.rb +7 -15
- data/lib/uniword/transformation/yaml_css_generator.rb +815 -0
- data/lib/uniword/transformation.rb +2 -0
- data/lib/uniword/validation/checkers/internal_link_checker.rb +2 -3
- data/lib/uniword/version.rb +1 -1
- data/lib/uniword/visitor/text_extractor.rb +2 -1
- data/lib/uniword/watermark/manager.rb +2 -2
- data/lib/uniword/wordprocessingml/allow_png.rb +14 -0
- data/lib/uniword/wordprocessingml/body.rb +53 -18
- data/lib/uniword/wordprocessingml/body_div.rb +17 -0
- data/lib/uniword/wordprocessingml/div_border.rb +23 -0
- data/lib/uniword/wordprocessingml/div_borders.rb +24 -0
- data/lib/uniword/wordprocessingml/divs_child.rb +17 -0
- data/lib/uniword/wordprocessingml/document_root.rb +6 -3
- data/lib/uniword/wordprocessingml/endnote.rb +1 -1
- data/lib/uniword/wordprocessingml/endnotes.rb +4 -0
- data/lib/uniword/wordprocessingml/footer.rb +4 -0
- data/lib/uniword/wordprocessingml/footer_reference.rb +0 -1
- data/lib/uniword/wordprocessingml/footnote.rb +1 -1
- data/lib/uniword/wordprocessingml/footnotes.rb +4 -0
- data/lib/uniword/wordprocessingml/header.rb +4 -0
- data/lib/uniword/wordprocessingml/header_reference.rb +0 -1
- data/lib/uniword/wordprocessingml/hyperlink.rb +2 -2
- data/lib/uniword/wordprocessingml/instr_text.rb +20 -5
- data/lib/uniword/wordprocessingml/level.rb +2 -0
- data/lib/uniword/wordprocessingml/mar_bottom.rb +17 -0
- data/lib/uniword/wordprocessingml/mar_left.rb +17 -0
- data/lib/uniword/wordprocessingml/mar_right.rb +17 -0
- data/lib/uniword/wordprocessingml/mar_top.rb +17 -0
- data/lib/uniword/wordprocessingml/num.rb +4 -0
- data/lib/uniword/wordprocessingml/numbering.rb +1 -22
- data/lib/uniword/wordprocessingml/optimize_for_browser.rb +14 -0
- data/lib/uniword/wordprocessingml/page_defaults.rb +39 -0
- data/lib/uniword/wordprocessingml/paragraph.rb +16 -13
- data/lib/uniword/wordprocessingml/paragraph_properties.rb +12 -8
- data/lib/uniword/wordprocessingml/run.rb +32 -36
- data/lib/uniword/wordprocessingml/run_properties/merging.rb +45 -0
- data/lib/uniword/wordprocessingml/run_properties.rb +8 -5
- data/lib/uniword/wordprocessingml/section_properties.rb +3 -3
- data/lib/uniword/wordprocessingml/separator_char.rb +21 -0
- data/lib/uniword/wordprocessingml/settings.rb +19 -17
- data/lib/uniword/wordprocessingml/styles/style_definition.rb +1 -1
- data/lib/uniword/wordprocessingml/styles/style_library.rb +16 -0
- data/lib/uniword/wordprocessingml/table_defaults.rb +44 -0
- data/lib/uniword/wordprocessingml/text.rb +8 -2
- data/lib/uniword/wordprocessingml/web_div.rb +41 -0
- data/lib/uniword/wordprocessingml/web_divs.rb +18 -0
- data/lib/uniword/wordprocessingml/web_encoding.rb +17 -0
- data/lib/uniword/wordprocessingml/web_settings.rb +5 -21
- data/lib/uniword/wordprocessingml/zoom.rb +2 -0
- data/lib/uniword/wordprocessingml.rb +23 -0
- data/lib/uniword.rb +27 -0
- metadata +47 -70
- data/lib/uniword/math/accent.rb +0 -25
- data/lib/uniword/math/accent_properties.rb +0 -25
- data/lib/uniword/math/argument_properties.rb +0 -22
- data/lib/uniword/math/bar.rb +0 -25
- data/lib/uniword/math/bar_properties.rb +0 -25
- data/lib/uniword/math/begin_char.rb +0 -22
- data/lib/uniword/math/border_box.rb +0 -25
- data/lib/uniword/math/border_box_properties.rb +0 -39
- data/lib/uniword/math/box.rb +0 -25
- data/lib/uniword/math/box_properties.rb +0 -33
- data/lib/uniword/math/char.rb +0 -22
- data/lib/uniword/math/control_properties.rb +0 -27
- data/lib/uniword/math/degree.rb +0 -26
- data/lib/uniword/math/delimiter.rb +0 -25
- data/lib/uniword/math/delimiter_properties.rb +0 -33
- data/lib/uniword/math/denominator.rb +0 -26
- data/lib/uniword/math/element.rb +0 -61
- data/lib/uniword/math/end_char.rb +0 -22
- data/lib/uniword/math/equation_array.rb +0 -25
- data/lib/uniword/math/equation_array_properties.rb +0 -33
- data/lib/uniword/math/fraction.rb +0 -27
- data/lib/uniword/math/fraction_properties.rb +0 -25
- data/lib/uniword/math/function.rb +0 -27
- data/lib/uniword/math/function_name.rb +0 -26
- data/lib/uniword/math/function_properties.rb +0 -23
- data/lib/uniword/math/group_char.rb +0 -25
- data/lib/uniword/math/group_char_properties.rb +0 -29
- data/lib/uniword/math/lim.rb +0 -26
- data/lib/uniword/math/lower_limit.rb +0 -27
- data/lib/uniword/math/lower_limit_properties.rb +0 -23
- data/lib/uniword/math/math_break.rb +0 -22
- data/lib/uniword/math/math_font.rb +0 -22
- data/lib/uniword/math/math_properties.rb +0 -53
- data/lib/uniword/math/math_run.rb +0 -42
- data/lib/uniword/math/math_run_properties.rb +0 -37
- data/lib/uniword/math/math_simple_int_val.rb +0 -24
- data/lib/uniword/math/math_simple_val.rb +0 -24
- data/lib/uniword/math/math_style.rb +0 -22
- data/lib/uniword/math/math_text.rb +0 -23
- data/lib/uniword/math/matrix.rb +0 -25
- data/lib/uniword/math/matrix_column.rb +0 -23
- data/lib/uniword/math/matrix_column_properties.rb +0 -24
- data/lib/uniword/math/matrix_columns.rb +0 -23
- data/lib/uniword/math/matrix_properties.rb +0 -37
- data/lib/uniword/math/matrix_row.rb +0 -23
- data/lib/uniword/math/nary.rb +0 -29
- data/lib/uniword/math/nary_properties.rb +0 -33
- data/lib/uniword/math/numerator.rb +0 -26
- data/lib/uniword/math/o_math.rb +0 -65
- data/lib/uniword/math/o_math_para.rb +0 -25
- data/lib/uniword/math/o_math_para_properties.rb +0 -22
- data/lib/uniword/math/phantom.rb +0 -25
- data/lib/uniword/math/phantom_properties.rb +0 -33
- data/lib/uniword/math/pre_sub_superscript.rb +0 -29
- data/lib/uniword/math/pre_sub_superscript_properties.rb +0 -23
- data/lib/uniword/math/radical.rb +0 -27
- data/lib/uniword/math/radical_properties.rb +0 -25
- data/lib/uniword/math/separator_char.rb +0 -22
- data/lib/uniword/math/sub.rb +0 -26
- data/lib/uniword/math/sub_superscript.rb +0 -29
- data/lib/uniword/math/sub_superscript_properties.rb +0 -25
- data/lib/uniword/math/subscript.rb +0 -27
- data/lib/uniword/math/subscript_properties.rb +0 -23
- data/lib/uniword/math/sup.rb +0 -26
- data/lib/uniword/math/superscript.rb +0 -27
- data/lib/uniword/math/superscript_properties.rb +0 -23
- data/lib/uniword/math/upper_limit.rb +0 -27
- data/lib/uniword/math/upper_limit_properties.rb +0 -23
|
@@ -5,25 +5,16 @@ module Uniword
|
|
|
5
5
|
# Builds footnotes and endnotes for documents.
|
|
6
6
|
#
|
|
7
7
|
# Manages footnote/endnote creation, ID assignment, and wiring
|
|
8
|
-
# references into the document body.
|
|
9
|
-
#
|
|
10
|
-
# @example Add footnotes to a document
|
|
11
|
-
# doc = DocumentBuilder.new
|
|
12
|
-
# doc.paragraph { |p| p << 'See the '; p << doc.footnote('A note') }
|
|
13
|
-
#
|
|
14
|
-
# @example Add endnotes
|
|
15
|
-
# doc.paragraph { |p| p << 'See '; p << doc.endnote('An endnote') }
|
|
8
|
+
# references into the document body. Uses IdAllocator for all ID
|
|
9
|
+
# assignment when available.
|
|
16
10
|
class FootnoteBuilder
|
|
17
|
-
def initialize(document)
|
|
11
|
+
def initialize(document, allocator: nil)
|
|
18
12
|
@document = document
|
|
13
|
+
@allocator = allocator
|
|
19
14
|
end
|
|
20
15
|
|
|
21
16
|
# Create a footnote and return a Run with a footnoteReference.
|
|
22
17
|
#
|
|
23
|
-
# The footnote body is stored in the document's footnotes collection.
|
|
24
|
-
# The returned Run contains a <w:footnoteReference> element that
|
|
25
|
-
# Word uses to link to the footnote body.
|
|
26
|
-
#
|
|
27
18
|
# @param text [String] Footnote text content
|
|
28
19
|
# @yield [ParagraphBuilder] Builder for rich footnote content
|
|
29
20
|
# @return [Wordprocessingml::Run] Run with footnote reference
|
|
@@ -61,24 +52,32 @@ module Uniword
|
|
|
61
52
|
end
|
|
62
53
|
|
|
63
54
|
def next_footnote_id
|
|
64
|
-
@
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
55
|
+
if @allocator
|
|
56
|
+
@allocator.alloc_footnote_id
|
|
57
|
+
else
|
|
58
|
+
@footnote_counter ||= 1
|
|
59
|
+
id = @footnote_counter
|
|
60
|
+
@footnote_counter += 1
|
|
61
|
+
id
|
|
62
|
+
end
|
|
68
63
|
end
|
|
69
64
|
|
|
70
65
|
def next_endnote_id
|
|
71
|
-
@
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
66
|
+
if @allocator
|
|
67
|
+
@allocator.alloc_endnote_id
|
|
68
|
+
else
|
|
69
|
+
@endnote_counter ||= 1
|
|
70
|
+
id = @endnote_counter
|
|
71
|
+
@endnote_counter += 1
|
|
72
|
+
id
|
|
73
|
+
end
|
|
75
74
|
end
|
|
76
75
|
|
|
77
76
|
def create_footnote_entry(id, text, &block)
|
|
78
77
|
# OOXML spec: regular footnotes must NOT have w:type.
|
|
79
78
|
# Only separator/continuationSeparator use w:type.
|
|
80
79
|
entry = Wordprocessingml::Footnote.new(id: id.to_s)
|
|
81
|
-
para = ParagraphBuilder.new
|
|
80
|
+
para = ParagraphBuilder.new(allocator: @allocator)
|
|
82
81
|
if block_given?
|
|
83
82
|
block.call(para)
|
|
84
83
|
elsif text
|
|
@@ -90,7 +89,7 @@ module Uniword
|
|
|
90
89
|
|
|
91
90
|
def create_endnote_entry(id, text, &block)
|
|
92
91
|
entry = Wordprocessingml::Endnote.new(id: id.to_s)
|
|
93
|
-
para = ParagraphBuilder.new
|
|
92
|
+
para = ParagraphBuilder.new(allocator: @allocator)
|
|
94
93
|
if block_given?
|
|
95
94
|
block.call(para)
|
|
96
95
|
elsif text
|
|
@@ -15,10 +15,11 @@ module Uniword
|
|
|
15
15
|
# f << Builder.page_number_field
|
|
16
16
|
# end
|
|
17
17
|
class HeaderFooterBuilder
|
|
18
|
-
attr_reader :model, :kind
|
|
18
|
+
attr_reader :model, :kind, :allocator
|
|
19
19
|
|
|
20
|
-
def initialize(kind, type: "default")
|
|
20
|
+
def initialize(kind, type: "default", allocator: nil)
|
|
21
21
|
@kind = kind
|
|
22
|
+
@allocator = allocator
|
|
22
23
|
@model = if kind == :header
|
|
23
24
|
Wordprocessingml::Header.new
|
|
24
25
|
else
|
|
@@ -33,15 +34,14 @@ module Uniword
|
|
|
33
34
|
def <<(element)
|
|
34
35
|
case element
|
|
35
36
|
when String
|
|
36
|
-
para =
|
|
37
|
-
para
|
|
38
|
-
@model.paragraphs << para
|
|
37
|
+
para = ParagraphBuilder.new(allocator: @allocator)
|
|
38
|
+
para << element
|
|
39
|
+
@model.paragraphs << para.build
|
|
39
40
|
when Wordprocessingml::Run
|
|
40
|
-
# Auto-wrap Run in a paragraph, appending to the last one if possible
|
|
41
41
|
if @model.paragraphs.empty?
|
|
42
|
-
para =
|
|
43
|
-
para
|
|
44
|
-
@model.paragraphs << para
|
|
42
|
+
para = ParagraphBuilder.new(allocator: @allocator)
|
|
43
|
+
para << element
|
|
44
|
+
@model.paragraphs << para.build
|
|
45
45
|
else
|
|
46
46
|
@model.paragraphs.last.runs << element
|
|
47
47
|
end
|
|
@@ -63,7 +63,7 @@ module Uniword
|
|
|
63
63
|
# @yield [ParagraphBuilder] Builder for paragraph configuration
|
|
64
64
|
# @return [ParagraphBuilder]
|
|
65
65
|
def paragraph(text = nil, &block)
|
|
66
|
-
para = ParagraphBuilder.new
|
|
66
|
+
para = ParagraphBuilder.new(allocator: @allocator)
|
|
67
67
|
para << text if text
|
|
68
68
|
yield(para) if block
|
|
69
69
|
@model.paragraphs << para.build
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "digest"
|
|
4
4
|
require "stringio"
|
|
5
5
|
|
|
6
6
|
module Uniword
|
|
@@ -27,20 +27,26 @@ module Uniword
|
|
|
27
27
|
# Picture namespace URI for GraphicData
|
|
28
28
|
PIC_URI = "http://schemas.openxmlformats.org/drawingml/2006/picture"
|
|
29
29
|
|
|
30
|
+
IMAGE_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"
|
|
31
|
+
|
|
30
32
|
# Register an image part on the document for DOCX packaging.
|
|
31
33
|
#
|
|
32
34
|
# @param document [DocumentBuilder, DocumentRoot] Target document
|
|
33
35
|
# @param path [String] Path to image file
|
|
34
|
-
# @return [String] Relationship ID (e.g., '
|
|
36
|
+
# @return [String] Relationship ID (e.g., 'rId5')
|
|
35
37
|
def self.register_image(document, path)
|
|
36
38
|
root = document.is_a?(Uniword::Builder::DocumentBuilder) ? document.model : document
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
alloc = document.is_a?(Uniword::Builder::DocumentBuilder) ? document.allocator : nil
|
|
40
|
+
|
|
41
|
+
target = "media/#{File.basename(path)}"
|
|
42
|
+
r_id = if alloc
|
|
43
|
+
alloc.alloc_rid(target: target, type: IMAGE_REL_TYPE)
|
|
44
|
+
elsif root
|
|
45
|
+
root.image_parts ||= {}
|
|
46
|
+
"rId#{root.image_parts.size + 1}"
|
|
47
|
+
else
|
|
48
|
+
"rId#{deterministic_id('img_rid', path)}"
|
|
49
|
+
end
|
|
44
50
|
|
|
45
51
|
content_type = case File.extname(path).downcase
|
|
46
52
|
when ".png" then "image/png"
|
|
@@ -53,6 +59,7 @@ module Uniword
|
|
|
53
59
|
end
|
|
54
60
|
|
|
55
61
|
if root
|
|
62
|
+
root.image_parts ||= {}
|
|
56
63
|
root.image_parts[r_id] = {
|
|
57
64
|
path: path,
|
|
58
65
|
data: File.binread(path),
|
|
@@ -133,11 +140,11 @@ alt_text: nil)
|
|
|
133
140
|
|
|
134
141
|
drawing = Wordprocessingml::Drawing.new
|
|
135
142
|
|
|
136
|
-
inline = WpDrawing::Inline.new
|
|
143
|
+
inline = WpDrawing::Inline.new(dist_t: 0, dist_b: 0, dist_l: 0, dist_r: 0)
|
|
137
144
|
inline.extent = WpDrawing::Extent.new(cx: w, cy: h)
|
|
138
|
-
inline.effect_extent = WpDrawing::EffectExtent.new
|
|
145
|
+
inline.effect_extent = WpDrawing::EffectExtent.new(l: 0, t: 0, r: 0, b: 0)
|
|
139
146
|
inline.doc_properties = WpDrawing::DocProperties.new(
|
|
140
|
-
id:
|
|
147
|
+
id: deterministic_id("inline", path),
|
|
141
148
|
name: File.basename(path, ".*"),
|
|
142
149
|
)
|
|
143
150
|
inline.graphic = build_graphic(r_id, w, h)
|
|
@@ -217,7 +224,7 @@ alt_text: nil)
|
|
|
217
224
|
end
|
|
218
225
|
|
|
219
226
|
anchor.doc_properties = WpDrawing::DocProperties.new(
|
|
220
|
-
id:
|
|
227
|
+
id: deterministic_id("anchor", path),
|
|
221
228
|
name: File.basename(path, ".*"),
|
|
222
229
|
)
|
|
223
230
|
anchor.graphic = build_graphic(r_id, w, h)
|
|
@@ -266,7 +273,7 @@ alt_text: nil)
|
|
|
266
273
|
end
|
|
267
274
|
|
|
268
275
|
class << self
|
|
269
|
-
|
|
276
|
+
include DeterministicId
|
|
270
277
|
|
|
271
278
|
# Build the Graphic > GraphicData > Picture chain
|
|
272
279
|
#
|
|
@@ -295,8 +302,8 @@ alt_text: nil)
|
|
|
295
302
|
# Non-visual properties
|
|
296
303
|
pic.nv_pic_pr = Picture::NonVisualPictureProperties.new
|
|
297
304
|
pic.nv_pic_pr.c_nv_pr = Drawingml::NonVisualDrawingProperties.new(
|
|
298
|
-
id:
|
|
299
|
-
name: "Picture
|
|
305
|
+
id: deterministic_id("pic", r_id),
|
|
306
|
+
name: "Picture",
|
|
300
307
|
)
|
|
301
308
|
pic.nv_pic_pr.c_nv_pic_pr = Picture::NonVisualPictureDrawingProperties.new
|
|
302
309
|
|
|
@@ -312,6 +319,7 @@ alt_text: nil)
|
|
|
312
319
|
pic.sp_pr.xfrm.off = Drawingml::Offset.new(x: 0, y: 0)
|
|
313
320
|
pic.sp_pr.xfrm.ext = Drawingml::Extents.new(cx: width, cy: height)
|
|
314
321
|
pic.sp_pr.prst_geom = Drawingml::PresetGeometry.new(prst: "rect")
|
|
322
|
+
pic.sp_pr.prst_geom.av_lst = Drawingml::AdjustValueList.new
|
|
315
323
|
|
|
316
324
|
pic
|
|
317
325
|
end
|
|
@@ -40,7 +40,8 @@ module Uniword
|
|
|
40
40
|
# @yield [ParagraphBuilder] Builder for item content
|
|
41
41
|
# @return [ParagraphBuilder] The paragraph builder
|
|
42
42
|
def item(text = nil, level: 0, &block)
|
|
43
|
-
|
|
43
|
+
alloc = @document.is_a?(DocumentBuilder) ? @document.allocator : nil
|
|
44
|
+
para = ParagraphBuilder.new(allocator: alloc)
|
|
44
45
|
para.numbering(@num_id, level)
|
|
45
46
|
para << text if text
|
|
46
47
|
yield(para) if block
|
|
@@ -1,31 +1,32 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
3
5
|
module Uniword
|
|
4
6
|
module Builder
|
|
5
|
-
# Builds and configures Paragraph objects.
|
|
6
|
-
#
|
|
7
|
-
# Uses << operator to append child elements with smart type routing.
|
|
8
|
-
#
|
|
9
|
-
# @example Create a simple paragraph
|
|
10
|
-
# para = ParagraphBuilder.new
|
|
11
|
-
# para << 'Hello World'
|
|
12
|
-
# para.build
|
|
13
|
-
#
|
|
14
|
-
# @example Styled paragraph with mixed content
|
|
15
|
-
# para = ParagraphBuilder.new
|
|
16
|
-
# para.style = 'Heading1'
|
|
17
|
-
# para << Builder.text('Title', bold: true, size: 24)
|
|
18
|
-
# para << Builder.hyperlink('https://example.com', 'link')
|
|
19
|
-
# para << Builder.tab_stop(position: 7200)
|
|
20
|
-
# para.build
|
|
21
7
|
class ParagraphBuilder < BaseBuilder
|
|
22
8
|
include HasBorders
|
|
23
9
|
include HasShading
|
|
24
10
|
|
|
11
|
+
@sequence = 0
|
|
12
|
+
@mutex = Mutex.new
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
attr_accessor :sequence, :mutex
|
|
16
|
+
end
|
|
17
|
+
|
|
25
18
|
def self.default_model_class
|
|
26
19
|
Wordprocessingml::Paragraph
|
|
27
20
|
end
|
|
28
21
|
|
|
22
|
+
attr_reader :allocator
|
|
23
|
+
|
|
24
|
+
def initialize(model = nil, allocator: nil)
|
|
25
|
+
@allocator = allocator
|
|
26
|
+
super(model)
|
|
27
|
+
assign_tracking_attributes
|
|
28
|
+
end
|
|
29
|
+
|
|
29
30
|
# Append a child element. Routes by type:
|
|
30
31
|
# - String -> creates a Run
|
|
31
32
|
# - Run -> appends to runs
|
|
@@ -39,23 +40,36 @@ module Uniword
|
|
|
39
40
|
def <<(element)
|
|
40
41
|
case element
|
|
41
42
|
when String
|
|
42
|
-
|
|
43
|
+
append_string(element)
|
|
44
|
+
when Omml::Models::CTOMath, Omml::Models::OMath
|
|
45
|
+
@model.o_maths << element
|
|
46
|
+
track_element_order("oMath")
|
|
47
|
+
when Omml::Models::OMathPara
|
|
48
|
+
@model.o_math_paras << element
|
|
49
|
+
track_element_order("oMathPara")
|
|
43
50
|
when Wordprocessingml::Run
|
|
44
|
-
|
|
51
|
+
append_run(element)
|
|
45
52
|
when Wordprocessingml::Hyperlink
|
|
46
53
|
@model.hyperlinks << element
|
|
54
|
+
track_element_order("hyperlink")
|
|
47
55
|
when Properties::TabStop
|
|
48
56
|
ensure_properties
|
|
49
57
|
@model.properties.tabs ||= Properties::Tabs.new
|
|
50
58
|
@model.properties.tabs << element
|
|
51
59
|
when Wordprocessingml::BookmarkStart
|
|
52
60
|
@model.bookmark_starts << element
|
|
61
|
+
track_element_order("bookmarkStart")
|
|
53
62
|
when Wordprocessingml::BookmarkEnd
|
|
54
63
|
@model.bookmark_ends << element
|
|
64
|
+
track_element_order("bookmarkEnd")
|
|
55
65
|
when Wordprocessingml::StructuredDocumentTag
|
|
56
66
|
@model.sdts << element
|
|
67
|
+
track_element_order("sdt")
|
|
68
|
+
when Wordprocessingml::SimpleField
|
|
69
|
+
@model.simple_fields << element
|
|
70
|
+
track_element_order("fldSimple")
|
|
57
71
|
when RunBuilder
|
|
58
|
-
|
|
72
|
+
append_run(element.build)
|
|
59
73
|
else
|
|
60
74
|
raise ArgumentError, "Cannot add #{element.class} to paragraph"
|
|
61
75
|
end
|
|
@@ -63,10 +77,14 @@ module Uniword
|
|
|
63
77
|
end
|
|
64
78
|
|
|
65
79
|
def style=(name)
|
|
66
|
-
ensure_properties.style =
|
|
80
|
+
ensure_properties.style = name
|
|
67
81
|
self
|
|
68
82
|
end
|
|
69
83
|
|
|
84
|
+
def style
|
|
85
|
+
@model.style
|
|
86
|
+
end
|
|
87
|
+
|
|
70
88
|
def align=(value)
|
|
71
89
|
ensure_properties.alignment = Properties::Alignment.new(value: value.to_s)
|
|
72
90
|
self
|
|
@@ -146,8 +164,65 @@ module Uniword
|
|
|
146
164
|
|
|
147
165
|
private
|
|
148
166
|
|
|
167
|
+
def assign_tracking_attributes
|
|
168
|
+
if @allocator
|
|
169
|
+
@model.rsid_r = @allocator.alloc_rsid
|
|
170
|
+
@model.para_id = Wordprocessingml::W14ParaId.new(@allocator.alloc_para_id)
|
|
171
|
+
else
|
|
172
|
+
idx = self.class.mutex.synchronize do
|
|
173
|
+
self.class.sequence += 1
|
|
174
|
+
end
|
|
175
|
+
hash = Digest::SHA256.hexdigest("para:#{idx}:#{Process.pid}")
|
|
176
|
+
@model.rsid_r = hash[0, 8]
|
|
177
|
+
@model.para_id = Wordprocessingml::W14ParaId.new(hash[8, 8])
|
|
178
|
+
end
|
|
179
|
+
@model.rsid_r_default = "00000000"
|
|
180
|
+
@model.text_id = Wordprocessingml::W14TextId.new("77777777")
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def append_run(run)
|
|
184
|
+
return if RunUtils.empty_run?(run)
|
|
185
|
+
|
|
186
|
+
last = @model.runs.last
|
|
187
|
+
if last && RunUtils.mergeable?(last, run)
|
|
188
|
+
RunUtils.merge_text(last, run)
|
|
189
|
+
else
|
|
190
|
+
@model.runs << run
|
|
191
|
+
track_element_order("r")
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def append_string(str)
|
|
196
|
+
return if str.nil? || str.empty?
|
|
197
|
+
|
|
198
|
+
unless str.include?("\n")
|
|
199
|
+
@model.runs << Wordprocessingml::Run.new(text: str)
|
|
200
|
+
track_element_order("r")
|
|
201
|
+
return
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
segments = str.split("\n", -1)
|
|
205
|
+
segments.each_with_index do |seg, i|
|
|
206
|
+
unless seg.empty?
|
|
207
|
+
@model.runs << Wordprocessingml::Run.new(text: seg)
|
|
208
|
+
track_element_order("r")
|
|
209
|
+
end
|
|
210
|
+
if i < segments.size - 1
|
|
211
|
+
br_run = Wordprocessingml::Run.new
|
|
212
|
+
br_run.break = Wordprocessingml::Break.new
|
|
213
|
+
@model.runs << br_run
|
|
214
|
+
track_element_order("r")
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def properties_tag
|
|
220
|
+
"pPr"
|
|
221
|
+
end
|
|
222
|
+
|
|
149
223
|
def ensure_properties
|
|
150
224
|
@model.properties ||= Wordprocessingml::ParagraphProperties.new
|
|
225
|
+
ensure_properties_in_order
|
|
151
226
|
@model.properties
|
|
152
227
|
end
|
|
153
228
|
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uniword
|
|
4
|
+
module Builder
|
|
5
|
+
# Shared run utilities used by both ParagraphBuilder (build-time) and
|
|
6
|
+
# Reconciler::Helpers (reconcile-time).
|
|
7
|
+
#
|
|
8
|
+
# Provides canonical implementations of run classification and merging.
|
|
9
|
+
# Both modules delegate here to ensure identical behavior.
|
|
10
|
+
module RunUtils
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# Whether a run contains no renderable content.
|
|
14
|
+
#
|
|
15
|
+
# A run is empty only if it has no text content and no structural
|
|
16
|
+
# elements (breaks, tabs, drawings, field chars, etc.).
|
|
17
|
+
def empty_run?(run)
|
|
18
|
+
return false if Array(run.break).any?
|
|
19
|
+
return false if run.tab
|
|
20
|
+
return false if run.drawings&.any?
|
|
21
|
+
return false if run.pictures&.any?
|
|
22
|
+
return false if run.alternate_content
|
|
23
|
+
return false if run.footnote_reference
|
|
24
|
+
return false if run.endnote_reference
|
|
25
|
+
return false if run.field_char
|
|
26
|
+
return false if run.instr_text
|
|
27
|
+
return false if run.position_tab
|
|
28
|
+
return false if run.del_text
|
|
29
|
+
return false if run.no_break_hyphen
|
|
30
|
+
return false if run.sym
|
|
31
|
+
return false if run.last_rendered_page_break
|
|
32
|
+
return false if run.separator_char
|
|
33
|
+
return false if run.continuation_separator_char
|
|
34
|
+
|
|
35
|
+
run.text_string.empty?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Whether a run contains only text (no structural elements).
|
|
39
|
+
#
|
|
40
|
+
# Runs with drawings, breaks, tabs, field chars, etc. are NOT
|
|
41
|
+
# text-only and should never be merged with other runs.
|
|
42
|
+
def text_only_run?(run)
|
|
43
|
+
return false if run.drawings&.any?
|
|
44
|
+
return false if run.pictures&.any?
|
|
45
|
+
return false if run.alternate_content
|
|
46
|
+
return false if run.footnote_reference
|
|
47
|
+
return false if run.endnote_reference
|
|
48
|
+
return false if run.field_char
|
|
49
|
+
return false if run.instr_text
|
|
50
|
+
return false if run.sym
|
|
51
|
+
return false if Array(run.break).any?
|
|
52
|
+
return false if run.tab
|
|
53
|
+
return false if run.position_tab
|
|
54
|
+
return false if run.no_break_hyphen
|
|
55
|
+
return false if run.del_text
|
|
56
|
+
|
|
57
|
+
true
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Whether two runs have identical formatting (RunProperties).
|
|
61
|
+
def properties_match?(a, b)
|
|
62
|
+
rpr_a = a.properties
|
|
63
|
+
rpr_b = b.properties
|
|
64
|
+
return true if rpr_a.nil? && rpr_b.nil?
|
|
65
|
+
return false if rpr_a.nil? || rpr_b.nil?
|
|
66
|
+
|
|
67
|
+
rpr_a.to_xml == rpr_b.to_xml
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Whether two text-only runs can be merged.
|
|
71
|
+
#
|
|
72
|
+
# Runs can be merged if both are text-only with identical formatting.
|
|
73
|
+
def mergeable?(existing, incoming)
|
|
74
|
+
return false unless text_only_run?(existing) && text_only_run?(incoming)
|
|
75
|
+
return false if incoming.text_string.empty?
|
|
76
|
+
|
|
77
|
+
properties_match?(existing, incoming)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Merge source run's text into target run.
|
|
81
|
+
#
|
|
82
|
+
# Handles xml:space="preserve" for whitespace-sensitive content.
|
|
83
|
+
def merge_text(target, source)
|
|
84
|
+
if Array(source.text).any?
|
|
85
|
+
existing = target.text_string
|
|
86
|
+
appended = source.text_string
|
|
87
|
+
combined = existing + appended
|
|
88
|
+
new_text = Wordprocessingml::Text.new(content: combined)
|
|
89
|
+
if Wordprocessingml::Text.preserve_whitespace?(combined)
|
|
90
|
+
new_text.xml_space = "preserve"
|
|
91
|
+
end
|
|
92
|
+
target.text = new_text
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -21,18 +21,19 @@ module Uniword
|
|
|
21
21
|
Wordprocessingml::SectionProperties
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
+
def initialize(model = nil)
|
|
25
|
+
super
|
|
26
|
+
ensure_section_defaults
|
|
27
|
+
end
|
|
28
|
+
|
|
24
29
|
def type=(value)
|
|
25
30
|
@model.type = value
|
|
26
31
|
self
|
|
27
32
|
end
|
|
28
33
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
# @param height [Integer] Page height in twips (default US Letter: 15840)
|
|
33
|
-
# @param orientation [String] 'portrait' or 'landscape'
|
|
34
|
-
# @return [self]
|
|
35
|
-
def page_size(width: 12_240, height: 15_840, orientation: "portrait")
|
|
34
|
+
def page_size(width: Wordprocessingml::PageDefaults::LETTER_WIDTH,
|
|
35
|
+
height: Wordprocessingml::PageDefaults::LETTER_HEIGHT,
|
|
36
|
+
orientation: "portrait")
|
|
36
37
|
@model.page_size ||= Wordprocessingml::PageSize.new
|
|
37
38
|
@model.page_size.width = width
|
|
38
39
|
@model.page_size.height = height
|
|
@@ -40,16 +41,6 @@ module Uniword
|
|
|
40
41
|
self
|
|
41
42
|
end
|
|
42
43
|
|
|
43
|
-
# Set page margins
|
|
44
|
-
#
|
|
45
|
-
# @param top [Integer] Top margin in twips (default 1440 = 1 inch)
|
|
46
|
-
# @param bottom [Integer] Bottom margin in twips
|
|
47
|
-
# @param left [Integer] Left margin in twips
|
|
48
|
-
# @param right [Integer] Right margin in twips
|
|
49
|
-
# @param header [Integer] Header distance from edge in twips
|
|
50
|
-
# @param footer [Integer] Footer distance from edge in twips
|
|
51
|
-
# @param gutter [Integer] Gutter margin in twips
|
|
52
|
-
# @return [self]
|
|
53
44
|
def margins(top: 1440, bottom: 1440, left: 1440, right: 1440,
|
|
54
45
|
header: 720, footer: 720, gutter: 0)
|
|
55
46
|
@model.page_margins ||= Wordprocessingml::PageMargins.new
|
|
@@ -63,42 +54,6 @@ module Uniword
|
|
|
63
54
|
self
|
|
64
55
|
end
|
|
65
56
|
|
|
66
|
-
# Set number of columns
|
|
67
|
-
#
|
|
68
|
-
# @param count [Integer] Number of columns
|
|
69
|
-
# @param spacing [Integer] Space between columns in twips
|
|
70
|
-
# @return [self]
|
|
71
|
-
def columns(count: 1, spacing: 720)
|
|
72
|
-
@model.columns ||= Wordprocessingml::Columns.new
|
|
73
|
-
@model.columns.num = count
|
|
74
|
-
@model.columns.space = spacing
|
|
75
|
-
self
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def page_borders(**_options)
|
|
79
|
-
@model.page_borders ||= Wordprocessingml::PageBorders.new
|
|
80
|
-
self
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# Set line numbering
|
|
84
|
-
#
|
|
85
|
-
# @param count_by [Integer] Line numbering interval
|
|
86
|
-
# @param start [Integer] Starting line number
|
|
87
|
-
# @param restart [String] Restart setting ('continuous', 'newPage', 'newSection')
|
|
88
|
-
# @return [self]
|
|
89
|
-
def line_numbering(count_by: 1, start: 1, restart: "continuous")
|
|
90
|
-
@model.line_numbering ||= Wordprocessingml::LineNumbering.new
|
|
91
|
-
@model.line_numbering.count_by = count_by
|
|
92
|
-
@model.line_numbering.start = start
|
|
93
|
-
@model.line_numbering.restart = restart
|
|
94
|
-
self
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# Set page numbering for this section
|
|
98
|
-
#
|
|
99
|
-
# @param start [Integer, nil] Starting page number
|
|
100
|
-
# @param format [String, nil] Number format ('decimal', 'lowerRoman', 'upperRoman')
|
|
101
|
-
# @return [self]
|
|
102
57
|
def page_numbering(start: nil, format: nil)
|
|
103
58
|
@model.page_numbering ||= Wordprocessingml::PageNumbering.new
|
|
104
59
|
@model.page_numbering.start = start if start
|
|
@@ -106,37 +61,55 @@ module Uniword
|
|
|
106
61
|
self
|
|
107
62
|
end
|
|
108
63
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
64
|
+
def columns(count: 1, spacing: 720)
|
|
65
|
+
@model.columns ||= Wordprocessingml::Columns.new
|
|
66
|
+
@model.columns.num = count
|
|
67
|
+
@model.columns.space = spacing
|
|
68
|
+
self
|
|
69
|
+
end
|
|
70
|
+
|
|
114
71
|
def header(type: "default", &block)
|
|
115
72
|
hf = HeaderFooterBuilder.new(:header, type: type)
|
|
116
|
-
|
|
117
|
-
|
|
73
|
+
block&.call(hf)
|
|
118
74
|
ref = Wordprocessingml::HeaderReference.new(
|
|
119
75
|
type: type, r_id: "rIdHdr#{type}",
|
|
120
76
|
)
|
|
121
77
|
@model.header_references << ref
|
|
78
|
+
@header_builders ||= {}
|
|
79
|
+
@header_builders[type] = hf
|
|
122
80
|
hf
|
|
123
81
|
end
|
|
124
82
|
|
|
125
|
-
# Configure a footer for this section
|
|
126
|
-
#
|
|
127
|
-
# @param type [String] Footer type ('default', 'first', 'even')
|
|
128
|
-
# @yield [HeaderFooterBuilder] Builder for footer content
|
|
129
|
-
# @return [HeaderFooterBuilder] The header/footer builder
|
|
130
83
|
def footer(type: "default", &block)
|
|
131
84
|
hf = HeaderFooterBuilder.new(:footer, type: type)
|
|
132
|
-
|
|
133
|
-
|
|
85
|
+
block&.call(hf)
|
|
134
86
|
ref = Wordprocessingml::FooterReference.new(
|
|
135
87
|
type: type, r_id: "rIdFtr#{type}",
|
|
136
88
|
)
|
|
137
89
|
@model.footer_references << ref
|
|
90
|
+
@footer_builders ||= {}
|
|
91
|
+
@footer_builders[type] = hf
|
|
138
92
|
hf
|
|
139
93
|
end
|
|
94
|
+
|
|
95
|
+
# Built header content models keyed by type.
|
|
96
|
+
def header_models
|
|
97
|
+
(@header_builders || {}).transform_values(&:build)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Built footer content models keyed by type.
|
|
101
|
+
def footer_models
|
|
102
|
+
(@footer_builders || {}).transform_values(&:build)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def ensure_section_defaults
|
|
108
|
+
page_size
|
|
109
|
+
margins
|
|
110
|
+
columns
|
|
111
|
+
@model.doc_grid ||= Wordprocessingml::PageDefaults.default_doc_grid
|
|
112
|
+
end
|
|
140
113
|
end
|
|
141
114
|
end
|
|
142
115
|
end
|