Class: Uniword::Builder::DocumentBuilder

Inherits:
BaseBuilder show all
Defined in:
lib/uniword/builder/document_builder.rb

Overview

Builds and configures DocumentRoot objects.

Top-level builder for creating Word documents.

Examples:

Create a new document

doc = DocumentBuilder.new
doc.paragraph { |p| p << 'Hello World' }
doc.heading('Title', level: 1)
doc.save('output.docx')

Load and modify a document

doc = DocumentBuilder.from_file('template.docx')
doc.paragraph { |p| p << 'New content' }
doc.save('modified.docx')

Complete document

doc = DocumentBuilder.new
doc.title('Report').author('Author')
doc.theme('atlas')
doc.toc
doc.heading('Introduction', level: 1)
doc.paragraph { |p| p << 'Content...' }
doc.bullet_list { |l| l.item('First'); l.item('Second') }
doc.page_break
doc.footer { |f| f << Builder.page_number_field }
doc.save('report.docx')

Instance Attribute Summary collapse

Attributes inherited from BaseBuilder

#model

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from BaseBuilder

#build, from_model

Constructor Details

#initialize(model = nil, allocator: nil) ⇒ DocumentBuilder

Every DocumentBuilder carries an IdAllocator — the single ID authority — so build-time allocations (images, hyperlinks, paragraph ids) and the save path draw from one registry. Reuses an explicit allocator or one already on the model; otherwise populates one seeded from the model's current relationships (loaded documents keep their rIds; fresh models start empty).



45
46
47
48
49
50
51
# File 'lib/uniword/builder/document_builder.rb', line 45

def initialize(model = nil, allocator: nil)
  super(model)
  @allocator = allocator || @model.allocator ||
    Docx::IdAllocator.populate_from_package(@model)
  @model.allocator = @allocator
  @footnote_builder = FootnoteBuilder.new(self, allocator: @allocator)
end

Instance Attribute Details

#allocatorObject (readonly)

Returns the value of attribute allocator.



32
33
34
# File 'lib/uniword/builder/document_builder.rb', line 32

def allocator
  @allocator
end

Class Method Details

.default_model_classObject



34
35
36
# File 'lib/uniword/builder/document_builder.rb', line 34

def self.default_model_class
  Wordprocessingml::DocumentRoot
end

.from_file(path) ⇒ DocumentBuilder

Load a document from file for manipulation

Parameters:

  • path (String)

    Path to .docx file

Returns:



57
58
59
# File 'lib/uniword/builder/document_builder.rb', line 57

def self.from_file(path)
  new(Uniword.load(path))
end

.from_template(path) ⇒ DocumentBuilder

Load a DOCX as a template, reset its body to a clean state, and seed the IdAllocator from the template's existing relationships.

Use this when you want the template's OOXML scaffolding (settings, fonts, styles, theme, content_types, namespace declarations) but will replace the document body with new content. This is the recommended path to producing Word-valid output: starting from a known-good DOCX preserves mc:Ignorable prefixes, rsid values, and other structural guarantees that Word expects.

Resets performed:

  • Body content cleared (paragraphs, tables, SDTs, bookmarks, element_order, section_properties)
  • User footnotes/endnotes cleared (separator/continuation kept)
  • custom_properties and custom_xml_items cleared
  • Stale image and customXml relationships removed
  • IdAllocator seeded from document_rels and package_rels

Parameters:

  • path (String)

    Path to .docx template file

Returns:

Raises:

  • (ArgumentError)

    if path does not exist



82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/uniword/builder/document_builder.rb', line 82

def self.from_template(path)
  raise ArgumentError, "Template not found: #{path}" unless File.exist?(path)

  root = Uniword.load(path)
  reset_template_body(root)
  clear_user_notes(root)
  root.custom_properties = nil
  root.custom_xml_items = nil
  remove_stale_relationships(root)
  seed_allocator(root)
  new(root, allocator: root.allocator)
end

Instance Method Details

#<<(element) ⇒ self

Append a top-level element (paragraph or table)

Parameters:

Returns:

  • (self)


149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/uniword/builder/document_builder.rb', line 149

def <<(element)
  case element
  when Wordprocessingml::Paragraph
    @model.body.paragraphs << element
    @model.body.append_to_element_order("p")
  when Wordprocessingml::Table
    @model.body.tables << element
    @model.body.append_to_element_order("tbl")
  when ParagraphBuilder
    @model.body.paragraphs << element.build
    @model.body.append_to_element_order("p")
  when TableBuilder
    @model.body.tables << element.build
    @model.body.append_to_element_order("tbl")
  else
    raise ArgumentError, "Cannot add #{element.class} to document"
  end
  self
end

#apply_styleset(name, strategy: :keep_existing) ⇒ self

Apply a bundled styleset to the document

Parameters:

  • name (String)

    Styleset name (e.g., 'formal', 'modern', 'elegant')

  • strategy (Symbol) (defaults to: :keep_existing)

    Conflict resolution (:keep_existing, :replace, :rename)

Returns:

  • (self)


400
401
402
403
# File 'lib/uniword/builder/document_builder.rb', line 400

def apply_styleset(name, strategy: :keep_existing)
  @model.apply_styleset(name, strategy: strategy)
  self
end

#author(value) ⇒ Object



365
366
367
368
# File 'lib/uniword/builder/document_builder.rb', line 365

def author(value)
  @model.core_properties.creator = value
  self
end

#bibliography(style: "APA") {|BibliographyBuilder| ... } ⇒ BibliographyBuilder

Create and configure bibliography sources

Parameters:

  • style (String) (defaults to: "APA")

    Citation style (default 'APA')

Yields:

Returns:



556
557
558
559
560
561
# File 'lib/uniword/builder/document_builder.rb', line 556

def bibliography(style: "APA", &block)
  bib = BibliographyBuilder.new(style: style)
  block.call(bib) if block_given?
  bib.attach(self)
  bib
end

#bibliography_placeholderself

Insert a bibliography placeholder (SDT content control)

Returns:

  • (self)


566
567
568
569
570
571
572
# File 'lib/uniword/builder/document_builder.rb', line 566

def bibliography_placeholder
  sdt = SdtBuilder.bibliography.build
  para = Wordprocessingml::Paragraph.new
  para.sdts << sdt
  self << para
  self
end

#bookmark(name) {|ParagraphBuilder| ... } ⇒ ParagraphBuilder

Create a bookmark wrapping the next content

Parameters:

  • name (String)

    Bookmark name

Yields:

Returns:



302
303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'lib/uniword/builder/document_builder.rb', line 302

def bookmark(name, &block)
  id = @allocator ? @allocator.alloc_bookmark_id : begin
    @bookmark_counter ||= 0
    @bookmark_counter += 1
    @bookmark_counter.to_s
  end

  para = ParagraphBuilder.new(allocator: @allocator)
  para << Wordprocessingml::BookmarkStart.new(id: id, name: name)
  block.call(para) if block_given?
  para << Wordprocessingml::BookmarkEnd.new(id: id)
  self << para
  para
end

#bullet_list {|ListBuilder| ... } ⇒ ListBuilder

Shorthand: create a bullet list

Yields:

Returns:



293
294
295
# File 'lib/uniword/builder/document_builder.rb', line 293

def bullet_list(&)
  list(type: :bullet, &)
end

#chart(type: :bar, width: nil, height: nil) {|ChartBuilder| ... } ⇒ ChartBuilder

Insert a chart into the document

Parameters:

  • type (Symbol) (defaults to: :bar)

    Chart type (:bar, :line, :pie, default :bar)

  • width (Integer) (defaults to: nil)

    Width in EMU (default 5486400 ≈ 6 inches)

  • height (Integer) (defaults to: nil)

    Height in EMU (default 3200400 ≈ 3.5 inches)

Yields:

Returns:



581
582
583
584
585
586
587
588
589
590
591
592
593
# File 'lib/uniword/builder/document_builder.rb', line 581

def chart(type: :bar, width: nil, height: nil, &block)
  cb = ChartBuilder.new(chart_type: type)
  cb.dimensions(width: width, height: height) if width || height
  block.call(cb) if block_given?

  drawing = cb.build_drawing(self)
  run = Wordprocessingml::Run.new
  run.drawings << drawing
  para = Wordprocessingml::Paragraph.new
  para.runs << run
  self << para
  cb
end

#comment(author:, text: nil, initials: nil, on: nil) {|CommentBuilder| ... } ⇒ Comment

Create a comment, register it in the document's comments collection (serialized to word/comments.xml on save), and anchor it around the most recently added body paragraph with commentRangeStart/commentRangeEnd markers and a CommentReference run. With no preceding paragraph the comment stays unanchored.

Create a comment and store it in the document's comments collection.

Without on:, the comment is anchored around the yielded paragraph (or the document's last paragraph when no block is given). With on:, the comment is anchored around an existing paragraph — anywhere in the document, including table cells.

Parameters:

  • author (String)

    Comment author name

  • text (String, nil) (defaults to: nil)

    Comment text

  • initials (String, nil) (defaults to: nil)

    Author initials

  • author (String)

    Comment author name

  • text (String, nil) (defaults to: nil)

    Comment text

  • initials (String, nil) (defaults to: nil)

    Author initials

  • on (Wordprocessingml::Paragraph, Integer, nil) (defaults to: nil)

    Anchor target: a paragraph object, or an index into body paragraphs

Yields:

Returns:

  • (Comment)

    The created Comment model

  • (Comment)

    The created Comment model

Raises:

  • (ArgumentError)

    when on is neither a Paragraph nor an index of an existing body paragraph



515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
# File 'lib/uniword/builder/document_builder.rb', line 515

def comment(author:, text: nil, initials: nil, on: nil, &block)
  comment_id = @allocator ? @allocator.alloc_comment_id : begin
    @comment_counter ||= 0
    @comment_counter += 1
    @comment_counter.to_s
  end
  cb = CommentBuilder.new(
    author: author,
    comment_id: comment_id,
    initials: initials
  )
  cb << text if text
  block.call(cb) if block_given?
  comment_obj = cb.build
  comments_part.add_comment(comment_obj)
  CommentAnchorer.anchor(comment_anchor_target(on),
                         comment_obj.comment_id)
  ensure_comment_reference_style
  comment_obj
end

#content_control(tag: nil, alias_name: nil, placeholder_text: nil) ⇒ SdtBuilder

Create a text content control paragraph

Parameters:

  • tag (String, nil) (defaults to: nil)

    Developer tag

  • alias_name (String, nil) (defaults to: nil)

    Display name

  • placeholder_text (String, nil) (defaults to: nil)

    Placeholder text

Returns:



542
543
544
545
546
547
548
549
# File 'lib/uniword/builder/document_builder.rb', line 542

def content_control(tag: nil, alias_name: nil, placeholder_text: nil)
  sdt = SdtBuilder.text(
    tag: tag, alias_name: alias_name,
    placeholder_text: placeholder_text
  )
  @model.body.paragraphs.last&.sdts&.<<(sdt.build)
  sdt
end

#created(value) ⇒ Object



385
386
387
388
# File 'lib/uniword/builder/document_builder.rb', line 385

def created(value)
  @model.core_properties.created = value
  self
end

#date_field(format: "M/d/yyyy") ⇒ self

Insert a date paragraph

Parameters:

  • format (String) (defaults to: "M/d/yyyy")

    Date format (default 'M/d/yyyy')

Returns:

  • (self)


615
616
617
618
# File 'lib/uniword/builder/document_builder.rb', line 615

def date_field(format: "M/d/yyyy")
  self << Builder.date_field(format: format)
  self
end

#define_style(name, base_on: "Normal") {|StyleBuilder| ... } ⇒ StyleBuilder

Define a paragraph style

Parameters:

  • name (String)

    Style name

  • base_on (String) (defaults to: "Normal")

    Base style (default 'Normal')

Yields:

Returns:



353
354
355
356
357
358
# File 'lib/uniword/builder/document_builder.rb', line 353

def define_style(name, base_on: "Normal", &block)
  style = StyleBuilder.new(name, base_on: base_on)
  block.call(style) if block_given?
  @model.styles_configuration.add_style(style.build)
  style
end

#description(value) ⇒ Object



370
371
372
373
# File 'lib/uniword/builder/document_builder.rb', line 370

def description(value)
  @model.core_properties.description = value
  self
end

#endnote(text = nil) {|ParagraphBuilder| ... } ⇒ Wordprocessingml::Run

Create an endnote and return a Run with an endnoteReference.

Parameters:

  • text (String) (defaults to: nil)

    Endnote text

Yields:

Returns:



331
332
333
# File 'lib/uniword/builder/document_builder.rb', line 331

def endnote(text = nil, &)
  @footnote_builder.endnote(text, &)
end

#floating_image(path, width: nil, height: nil, alt_text: nil, align: nil, vertical_align: nil, wrap: :square, behind_text: false) ⇒ self

Insert a floating image paragraph

Parameters:

  • path (String)

    Path to image file

  • width (Integer, nil) (defaults to: nil)

    Width in EMU

  • height (Integer, nil) (defaults to: nil)

    Height in EMU

  • alt_text (String, nil) (defaults to: nil)

    Alternative text

  • align (Symbol, nil) (defaults to: nil)

    Horizontal alignment (:left, :center, :right)

  • vertical_align (Symbol, nil) (defaults to: nil)

    Vertical alignment (:top, :middle, :bottom)

  • wrap (Symbol) (defaults to: :square)

    Text wrapping (:square, :none, :top_and_bottom)

  • behind_text (Boolean) (defaults to: false)

    Place image behind text

Returns:

  • (self)


448
449
450
451
452
453
454
455
456
457
458
# File 'lib/uniword/builder/document_builder.rb', line 448

def floating_image(path, width: nil, height: nil, alt_text: nil,
                   align: nil, vertical_align: nil, wrap: :square,
                   behind_text: false)
  para = Wordprocessingml::Paragraph.new
  para.runs << ImageBuilder.create_floating_run(
    self, path, width: width, height: height, alt_text: alt_text,
                align: align, wrap: wrap, behind_text: behind_text
  )
  self << para
  self
end

Configure a footer

Parameters:

  • type (String) (defaults to: "default")

    Footer type ('default', 'first', 'even')

Yields:

Returns:



251
252
253
254
255
256
# File 'lib/uniword/builder/document_builder.rb', line 251

def footer(type: "default", &block)
  hf = HeaderFooterBuilder.new(:footer, type: type, allocator: @allocator)
  block.call(hf) if block_given?
  (@model.footers ||= {})[type] = hf.build
  hf
end

#footnote(text = nil) {|ParagraphBuilder| ... } ⇒ Wordprocessingml::Run

Create a footnote and return a Run with a footnoteReference.

Parameters:

  • text (String) (defaults to: nil)

    Footnote text

Yields:

Returns:



322
323
324
# File 'lib/uniword/builder/document_builder.rb', line 322

def footnote(text = nil, &)
  @footnote_builder.footnote(text, &)
end

#header(type: "default") {|HeaderFooterBuilder| ... } ⇒ HeaderFooterBuilder

Configure a header

Parameters:

  • type (String) (defaults to: "default")

    Header type ('default', 'first', 'even')

Yields:

Returns:



239
240
241
242
243
244
# File 'lib/uniword/builder/document_builder.rb', line 239

def header(type: "default", &block)
  hf = HeaderFooterBuilder.new(:header, type: type, allocator: @allocator)
  block.call(hf) if block_given?
  (@model.headers ||= {})[type] = hf.build
  hf
end

#heading(text, level: 1) {|ParagraphBuilder| ... } ⇒ ParagraphBuilder

Create and add a heading paragraph

Parameters:

  • text (String)

    Heading text

  • level (Integer) (defaults to: 1)

    Heading level (1-9, default 1)

Yields:

Returns:



188
189
190
191
192
193
194
# File 'lib/uniword/builder/document_builder.rb', line 188

def heading(text, level: 1)
  para = ParagraphBuilder.new(allocator: @allocator)
  para.style = "Heading#{level}"
  para << text
  self << para
  para
end

#horizontal_rule(style: "single", color: "auto", size: 6) ⇒ self

Insert a horizontal rule (paragraph with bottom border)

Parameters:

  • style (String) (defaults to: "single")

    Border style (default 'single')

  • color (String) (defaults to: "auto")

    Border color (default 'auto')

  • size (Integer) (defaults to: 6)

    Border size in eighths of a point (default 6)

Returns:

  • (self)


411
412
413
414
415
416
417
418
419
# File 'lib/uniword/builder/document_builder.rb', line 411

def horizontal_rule(style: "single", color: "auto", size: 6)
  para = ParagraphBuilder.new(allocator: @allocator)
  para.borders(
    bottom: { style: style, color: color, size: size }
  )
  para.spacing(after: 0)
  self << para
  self
end

#image(path, width: nil, height: nil, alt_text: nil) ⇒ self

Insert an inline image paragraph

Parameters:

  • path (String)

    Path to image file

  • width (Integer, nil) (defaults to: nil)

    Width in EMU (914400 = 1 inch)

  • height (Integer, nil) (defaults to: nil)

    Height in EMU

  • alt_text (String, nil) (defaults to: nil)

    Alternative text

Returns:

  • (self)


428
429
430
431
432
433
434
435
# File 'lib/uniword/builder/document_builder.rb', line 428

def image(path, width: nil, height: nil, alt_text: nil)
  para = Wordprocessingml::Paragraph.new
  para.runs << ImageBuilder.create_run(
    self, path, width: width, height: height, alt_text: alt_text
  )
  self << para
  self
end

#keywords(value) ⇒ Object



380
381
382
383
# File 'lib/uniword/builder/document_builder.rb', line 380

def keywords(value)
  @model.core_properties.keywords = value
  self
end

#list(type: :bullet) {|ListBuilder| ... } ⇒ ListBuilder

Create a list (bulleted or numbered)

Parameters:

  • type (Symbol) (defaults to: :bullet)

    List type (:bullet, :decimal, :roman, :letter)

Yields:

Returns:



275
276
277
278
279
# File 'lib/uniword/builder/document_builder.rb', line 275

def list(type: :bullet, &block)
  lb = ListBuilder.new(self, type: type)
  block.call(lb) if block_given?
  lb
end

#modified(value) ⇒ Object



390
391
392
393
# File 'lib/uniword/builder/document_builder.rb', line 390

def modified(value)
  @model.core_properties.modified = value
  self
end

#numbered_list {|ListBuilder| ... } ⇒ ListBuilder

Shorthand: create a numbered list

Yields:

Returns:



285
286
287
# File 'lib/uniword/builder/document_builder.rb', line 285

def numbered_list(&)
  list(type: :decimal, &)
end

#page_breakself

Insert a page break

Returns:

  • (self)


199
200
201
202
203
204
# File 'lib/uniword/builder/document_builder.rb', line 199

def page_break
  self << Wordprocessingml::Paragraph.new(
    runs: [Builder.page_break]
  )
  self
end

#page_numberself

Insert a page number paragraph

Returns:

  • (self)


598
599
600
601
# File 'lib/uniword/builder/document_builder.rb', line 598

def page_number
  self << Builder.page_number_field
  self
end

#paragraph(text = nil) {|ParagraphBuilder| ... } ⇒ ParagraphBuilder

Create and add a paragraph to the document

Parameters:

  • text (String, nil) (defaults to: nil)

    Optional text content

Yields:

Returns:



174
175
176
177
178
179
180
# File 'lib/uniword/builder/document_builder.rb', line 174

def paragraph(text = nil, &block)
  para = ParagraphBuilder.new(allocator: @allocator)
  para << text if text
  block.call(para) if block_given?
  self << para
  para
end

#save(path) ⇒ Object

Save document to file

Parameters:

  • path (String)

    Output file path



632
633
634
# File 'lib/uniword/builder/document_builder.rb', line 632

def save(path)
  @model.to_file(path)
end

#section(type: "nextPage") {|SectionBuilder| ... } ⇒ SectionBuilder

Configure section properties

Parameters:

  • type (String) (defaults to: "nextPage")

    Section break type ('nextPage', 'continuous', 'evenPage', 'oddPage')

Yields:

Returns:



222
223
224
225
226
227
228
229
230
231
232
# File 'lib/uniword/builder/document_builder.rb', line 222

def section(type: "nextPage", &block)
  sec = SectionBuilder.new
  sec.type = type
  block.call(sec) if block_given?
  @model.body.section_properties ||= sec.build
  # Register section-level headers/footers so their sectPr
  # references resolve to real parts at save time.
  sec.header_models.each { |t, hf| (@model.headers ||= {})[t] = hf }
  sec.footer_models.each { |t, hf| (@model.footers ||= {})[t] = hf }
  sec
end

#subject(value) ⇒ Object



375
376
377
378
# File 'lib/uniword/builder/document_builder.rb', line 375

def subject(value)
  @model.core_properties.subject = value
  self
end

#table {|TableBuilder| ... } ⇒ TableBuilder

Create and add a table to the document

Yields:

Returns:



210
211
212
213
214
215
# File 'lib/uniword/builder/document_builder.rb', line 210

def table(&block)
  tbl = TableBuilder.new
  block.call(tbl) if block_given?
  self << tbl
  tbl
end

#theme(name = nil) {|ThemeBuilder| ... } ⇒ ThemeBuilder

Apply or configure a document theme

Parameters:

  • name (String, nil) (defaults to: nil)

    Theme name to apply

Yields:

Returns:



340
341
342
343
344
345
# File 'lib/uniword/builder/document_builder.rb', line 340

def theme(name = nil, &block)
  tb = ThemeBuilder.new(self)
  tb.apply(name) if name
  block.call(tb) if block_given?
  tb
end

#time_field(format: "h:mm:ss am/pm") ⇒ self

Insert a time paragraph

Parameters:

  • format (String) (defaults to: "h:mm:ss am/pm")

    Time format (default 'h:mm:ss am/pm')

Returns:

  • (self)


624
625
626
627
# File 'lib/uniword/builder/document_builder.rb', line 624

def time_field(format: "h:mm:ss am/pm")
  self << Builder.time_field(format: format)
  self
end

#title(value) ⇒ Object



360
361
362
363
# File 'lib/uniword/builder/document_builder.rb', line 360

def title(value)
  @model.core_properties.title = value
  self
end

#toc(title: "Table of Contents", styles: nil) ⇒ self

Insert a Table of Contents

Parameters:

  • title (String) (defaults to: "Table of Contents")

    TOC title (default 'Table of Contents')

  • styles (Array<String>, nil) (defaults to: nil)

    Heading styles to include

Returns:

  • (self)


263
264
265
266
267
268
# File 'lib/uniword/builder/document_builder.rb', line 263

def toc(title: "Table of Contents", styles: nil)
  TocBuilder.build(title: title, styles: styles).each do |para|
    self << para
  end
  self
end

#total_pagesself

Insert a total pages paragraph

Returns:

  • (self)


606
607
608
609
# File 'lib/uniword/builder/document_builder.rb', line 606

def total_pages
  self << Builder.total_pages_field
  self
end

#watermark(text, font: "Calibri", size: 60, color: nil, opacity: "0.3", angle: -45)) ⇒ self

Add a watermark to the document header

Parameters:

  • text (String, nil)

    Watermark text (nil to clear)

  • font (String) (defaults to: "Calibri")

    Font name (default 'Calibri')

  • size (Integer) (defaults to: 60)

    Font size in points (default 60)

  • color (String) (defaults to: nil)

    Fill color hex (default 'D0D0D0')

  • opacity (String) (defaults to: "0.3")

    Opacity '0.0' to '1.0' (default '0.3')

  • angle (Integer) (defaults to: -45))

    Rotation angle in degrees (default -45)

Returns:

  • (self)


469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
# File 'lib/uniword/builder/document_builder.rb', line 469

def watermark(text, font: "Calibri", size: 60, color: nil,
              opacity: "0.3", angle: -45)
  if text.nil?
    (@model.headers ||= {}).delete("default")
    return self
  end

  para = WatermarkBuilder.build_paragraph(
    text, font: font, size: size, color: color,
          opacity: opacity, angle: angle
  )

  header = Wordprocessingml::Header.new
  header.paragraphs << para
  (@model.headers ||= {})["default"] = header
  self
end