notare 0.0.4 → 0.0.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1dd4e9ab394b198d4a9ddd59eb40fac35c865159e527fe14a90113bba1071fd0
4
- data.tar.gz: e10d211085e797c80850ab3433b8802270df96bc01cd3d2ac348da6f17c675ec
3
+ metadata.gz: 959b59c2f2dc30ff265115057c1fd304f3b7747adfe16c9818e6de9e5c564fc1
4
+ data.tar.gz: 6c3da88bce319ed81fdb519308d6854c00f6a846343e37178e6bafab2b1fefbb
5
5
  SHA512:
6
- metadata.gz: ba2c27ab7d6ad5a6f5b5239f54504e8352a5fee52374685f7e09d61284781ba93e9353a0c75f07a3e047571cc55d598c3764beb3c1d08663e54d11119e515fa5
7
- data.tar.gz: b42bc57c20a639882a8912b1da7759b2c131443ce840f3d901a1a29a70a86e4f8238ce0b9d38ed688e0332fb99b77f7c5edcebffc611302273ede404c2a72197
6
+ metadata.gz: '00558f142269e3927cdb5aab15271a92533a48ff5ff2cf19c349e8757ccd9da77af9d27a249bc1a2551c910b8578a8bc9a354622cea7c20424183b7230bc06b9'
7
+ data.tar.gz: 2f179ca2e773afe314151b61fb9de4e48a6b0037bba3ce50397636b9f2a26790f5e0399c11f5d807c4f3a3090f562e094d0e02da40b4a77719f1bd4c3ea7ee8a
data/README.md CHANGED
@@ -332,6 +332,75 @@ doc.table(style: :borderless) do
332
332
  end
333
333
  ```
334
334
 
335
+ #### Column Sizing
336
+
337
+ Control table column widths with layout modes and explicit sizing.
338
+
339
+ **Auto-layout** - columns adjust to fit content:
340
+
341
+ ```ruby
342
+ doc.table(layout: :auto) do
343
+ doc.tr do
344
+ doc.td "Short"
345
+ doc.td "This column expands to fit longer content"
346
+ end
347
+ end
348
+ ```
349
+
350
+ **Fixed column widths** - specify widths for all columns:
351
+
352
+ ```ruby
353
+ # Inches
354
+ doc.table(columns: %w[2in 3in 1.5in]) do
355
+ doc.tr do
356
+ doc.td "2 inches"
357
+ doc.td "3 inches"
358
+ doc.td "1.5 inches"
359
+ end
360
+ end
361
+
362
+ # Centimeters
363
+ doc.table(columns: %w[5cm 10cm]) do
364
+ doc.tr { doc.td "5cm"; doc.td "10cm" }
365
+ end
366
+
367
+ # Percentages
368
+ doc.table(columns: %w[25% 50% 25%]) do
369
+ doc.tr { doc.td "Quarter"; doc.td "Half"; doc.td "Quarter" }
370
+ end
371
+ ```
372
+
373
+ **Per-cell widths** - set width on individual cells:
374
+
375
+ ```ruby
376
+ doc.table do
377
+ doc.tr do
378
+ doc.td("Narrow", width: "1in")
379
+ doc.td("Wide", width: "4in")
380
+ end
381
+ end
382
+ ```
383
+
384
+ **Combined layout and columns:**
385
+
386
+ ```ruby
387
+ doc.table(layout: :fixed, columns: %w[2in 2in 2in]) do
388
+ doc.tr do
389
+ doc.td "A"
390
+ doc.td "B"
391
+ doc.td "C"
392
+ end
393
+ end
394
+ ```
395
+
396
+ **Width formats:**
397
+
398
+ | Format | Example | Description |
399
+ |--------|---------|-------------|
400
+ | Inches | `"2in"` | Fixed width in inches |
401
+ | Centimeters | `"5cm"` | Fixed width in centimeters |
402
+ | Percentage | `"50%"` | Percentage of table width |
403
+
335
404
  ### Images
336
405
 
337
406
  Images can be added to paragraphs, table cells, and list items. Supports PNG and JPEG formats.
@@ -526,10 +595,10 @@ end
526
595
  | `li(text)` | List item with text |
527
596
  | `li { }` | List item with block content |
528
597
  | `li(text) { }` | List item with text and nested content |
529
- | `table(style:) { }` | Table with optional style |
598
+ | `table(style:, layout:, columns:) { }` | Table with optional style, layout (`:auto`/`:fixed`), and column widths |
530
599
  | `tr { }` | Table row |
531
- | `td(text)` | Table cell with text |
532
- | `td { }` | Table cell with block content |
600
+ | `td(text, width:)` | Table cell with text and optional width |
601
+ | `td(width:) { }` | Table cell with block content and optional width |
533
602
  | `image(path, width:, height:)` | Insert image (PNG/JPEG). Dimensions: `"2in"`, `"5cm"`, `"100px"`, or integer pixels |
534
603
 
535
604
  ## Development
@@ -100,8 +100,8 @@ module Notare
100
100
  @current_list.add_item(item)
101
101
  end
102
102
 
103
- def table(style: nil, &block)
104
- tbl = Nodes::Table.new(style: resolve_table_style(style))
103
+ def table(style: nil, layout: nil, columns: nil, &block)
104
+ tbl = Nodes::Table.new(style: resolve_table_style(style), layout: layout, columns: columns)
105
105
  previous_table = @current_table
106
106
  @current_table = tbl
107
107
  block.call
@@ -118,8 +118,8 @@ module Notare
118
118
  @current_table.add_row(row)
119
119
  end
120
120
 
121
- def td(text = nil, &block)
122
- cell = Nodes::TableCell.new
121
+ def td(text = nil, width: nil, &block)
122
+ cell = Nodes::TableCell.new(width: width)
123
123
  if block
124
124
  with_target(cell, &block)
125
125
  elsif text
@@ -88,13 +88,13 @@ module Notare
88
88
  def next_image_rid
89
89
  # rId1 = styles.xml (always present)
90
90
  # rId2 = numbering.xml (if lists present)
91
- # rId3+ = images, then hyperlinks
91
+ # rId3+ = images and hyperlinks share the same ID space
92
92
  base = @has_lists ? 3 : 2
93
- "rId#{base + @images.size}"
93
+ "rId#{base + @images.size + @hyperlinks.size}"
94
94
  end
95
95
 
96
96
  def next_hyperlink_rid
97
- # Hyperlinks come after images
97
+ # Images and hyperlinks share the same ID space
98
98
  base = @has_lists ? 3 : 2
99
99
  "rId#{base + @images.size + @hyperlinks.size}"
100
100
  end
@@ -3,12 +3,14 @@
3
3
  module Notare
4
4
  module Nodes
5
5
  class Table < Base
6
- attr_reader :rows, :style
6
+ attr_reader :rows, :style, :layout, :columns
7
7
 
8
- def initialize(style: nil)
8
+ def initialize(style: nil, layout: nil, columns: nil)
9
9
  super()
10
10
  @rows = []
11
11
  @style = style
12
+ @layout = layout
13
+ @columns = columns
12
14
  end
13
15
 
14
16
  def add_row(row)
@@ -3,11 +3,12 @@
3
3
  module Notare
4
4
  module Nodes
5
5
  class TableCell < Base
6
- attr_reader :runs
6
+ attr_reader :runs, :width
7
7
 
8
- def initialize
9
- super
8
+ def initialize(width: nil)
9
+ super()
10
10
  @runs = []
11
+ @width = width
11
12
  end
12
13
 
13
14
  def add_run(run)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Notare
4
- VERSION = "0.0.4"
4
+ VERSION = "0.0.5"
5
5
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Notare
4
+ module WidthParser
5
+ TWIPS_PER_INCH = 1440
6
+ TWIPS_PER_CM = 567
7
+ PCT_MULTIPLIER = 50
8
+
9
+ ParsedWidth = Struct.new(:value, :type, keyword_init: true)
10
+
11
+ def self.parse(value)
12
+ case value
13
+ when :auto, nil
14
+ ParsedWidth.new(value: 0, type: "auto")
15
+ when Integer
16
+ ParsedWidth.new(value: value, type: "dxa")
17
+ when /\A(\d+(?:\.\d+)?)\s*in\z/i
18
+ twips = (::Regexp.last_match(1).to_f * TWIPS_PER_INCH).to_i
19
+ ParsedWidth.new(value: twips, type: "dxa")
20
+ when /\A(\d+(?:\.\d+)?)\s*cm\z/i
21
+ twips = (::Regexp.last_match(1).to_f * TWIPS_PER_CM).to_i
22
+ ParsedWidth.new(value: twips, type: "dxa")
23
+ when /\A(\d+(?:\.\d+)?)\s*%\z/
24
+ pct = (::Regexp.last_match(1).to_f * PCT_MULTIPLIER).to_i
25
+ ParsedWidth.new(value: pct, type: "pct")
26
+ else
27
+ raise ArgumentError, "Invalid width: #{value}. Use '2in', '5cm', '50%', :auto, or integer twips."
28
+ end
29
+ end
30
+ end
31
+ end
@@ -163,12 +163,12 @@ module Notare
163
163
  end
164
164
 
165
165
  def render_table(xml, table)
166
- column_count = table.rows.first&.cells&.size || 1
167
- col_width = 5000 / column_count
166
+ column_widths = compute_column_widths(table)
168
167
 
169
168
  xml["w"].tbl do
170
169
  xml["w"].tblPr do
171
- xml["w"].tblW("w:w" => "5000", "w:type" => "pct")
170
+ render_table_width(xml, column_widths)
171
+ render_table_layout(xml, table.layout)
172
172
  if table.style
173
173
  xml["w"].tblStyle("w:val" => table.style.style_id)
174
174
  else
@@ -179,25 +179,89 @@ module Notare
179
179
  end
180
180
  end
181
181
  end
182
- xml["w"].tblGrid do
183
- column_count.times do
184
- xml["w"].gridCol("w:w" => col_width.to_s)
185
- end
182
+ render_table_grid(xml, column_widths)
183
+ table.rows.each { |row| render_table_row(xml, row, column_widths) }
184
+ end
185
+ end
186
+
187
+ def compute_column_widths(table)
188
+ if table.columns
189
+ table.columns.map { |c| WidthParser.parse(c) }
190
+ elsif table.layout == :auto
191
+ first_row = table.rows.first
192
+ cell_count = first_row&.cells&.size || 1
193
+ Array.new(cell_count) { WidthParser::ParsedWidth.new(value: 0, type: "auto") }
194
+ else
195
+ infer_widths_from_first_row(table)
196
+ end
197
+ end
198
+
199
+ def infer_widths_from_first_row(table)
200
+ first_row = table.rows.first
201
+ return [WidthParser::ParsedWidth.new(value: 5000, type: "pct")] unless first_row
202
+
203
+ cells = first_row.cells
204
+ has_explicit_widths = cells.any?(&:width)
205
+
206
+ if has_explicit_widths
207
+ cells.map do |cell|
208
+ cell.width ? WidthParser.parse(cell.width) : WidthParser::ParsedWidth.new(value: 0, type: "auto")
186
209
  end
187
- table.rows.each { |row| render_table_row(xml, row, col_width) }
210
+ else
211
+ col_width = 5000 / cells.size
212
+ cells.map { WidthParser::ParsedWidth.new(value: col_width, type: "pct") }
188
213
  end
189
214
  end
190
215
 
191
- def render_table_row(xml, row, col_width)
216
+ def render_table_width(xml, column_widths)
217
+ if column_widths.all? { |w| w.type == "pct" }
218
+ total = column_widths.sum(&:value)
219
+ xml["w"].tblW("w:w" => total.to_s, "w:type" => "pct")
220
+ elsif column_widths.all? { |w| w.type == "dxa" }
221
+ total = column_widths.sum(&:value)
222
+ xml["w"].tblW("w:w" => total.to_s, "w:type" => "dxa")
223
+ else
224
+ xml["w"].tblW("w:w" => "0", "w:type" => "auto")
225
+ end
226
+ end
227
+
228
+ def render_table_layout(xml, layout)
229
+ return unless layout
230
+
231
+ layout_type = layout == :auto ? "autofit" : "fixed"
232
+ xml["w"].tblLayout("w:type" => layout_type)
233
+ end
234
+
235
+ def render_table_grid(xml, column_widths)
236
+ xml["w"].tblGrid do
237
+ column_widths.each do |width|
238
+ grid_width = case width.type
239
+ when "pct" then pct_to_approximate_dxa(width.value)
240
+ when "auto" then 1440 # Default 1 inch for auto columns
241
+ else width.value
242
+ end
243
+ xml["w"].gridCol("w:w" => grid_width.to_s)
244
+ end
245
+ end
246
+ end
247
+
248
+ # Convert percentage (in fiftieths) to approximate twips
249
+ # Assumes 6.5 inch content width = 9360 twips
250
+ def pct_to_approximate_dxa(pct_value)
251
+ (pct_value * 9360 / 5000.0).to_i
252
+ end
253
+
254
+ def render_table_row(xml, row, column_widths)
192
255
  xml["w"].tr do
193
- row.cells.each { |cell| render_table_cell(xml, cell, col_width) }
256
+ row.cells.each_with_index { |cell, idx| render_table_cell(xml, cell, column_widths[idx]) }
194
257
  end
195
258
  end
196
259
 
197
- def render_table_cell(xml, cell, col_width)
260
+ def render_table_cell(xml, cell, column_width)
261
+ width = column_width || WidthParser::ParsedWidth.new(value: 0, type: "auto")
198
262
  xml["w"].tc do
199
263
  xml["w"].tcPr do
200
- xml["w"].tcW("w:w" => col_width.to_s, "w:type" => "pct")
264
+ xml["w"].tcW("w:w" => width.value.to_s, "w:type" => width.type)
201
265
  end
202
266
  xml["w"].p do
203
267
  cell.runs.each { |run| render_run(xml, run) }
@@ -32,6 +32,8 @@ module Notare
32
32
  render_style(xml, style)
33
33
  end
34
34
 
35
+ render_table_normal_style(xml) if @table_styles.any?
36
+
35
37
  @table_styles.each_value do |style|
36
38
  render_table_style(xml, style)
37
39
  end
@@ -57,8 +59,12 @@ module Notare
57
59
  xml["w"].pPr do
58
60
  xml["w"].jc("w:val" => ALIGNMENT_MAP[style.align]) if style.align
59
61
  xml["w"].ind("w:left" => style.indent.to_s) if style.indent
60
- xml["w"].spacing("w:before" => style.spacing_before.to_s) if style.spacing_before
61
- xml["w"].spacing("w:after" => style.spacing_after.to_s) if style.spacing_after
62
+ if style.spacing_before || style.spacing_after
63
+ spacing_attrs = {}
64
+ spacing_attrs["w:before"] = style.spacing_before.to_s if style.spacing_before
65
+ spacing_attrs["w:after"] = style.spacing_after.to_s if style.spacing_after
66
+ xml["w"].spacing(spacing_attrs)
67
+ end
62
68
  end
63
69
  end
64
70
 
@@ -75,9 +81,24 @@ module Notare
75
81
  end
76
82
  end
77
83
 
84
+ def render_table_normal_style(xml)
85
+ xml["w"].style("w:type" => "table", "w:default" => "1", "w:styleId" => "TableNormal") do
86
+ xml["w"].name("w:val" => "Normal Table")
87
+ xml["w"].tblPr do
88
+ xml["w"].tblCellMar do
89
+ xml["w"].top("w:w" => "0", "w:type" => "dxa")
90
+ xml["w"].left("w:w" => "108", "w:type" => "dxa")
91
+ xml["w"].bottom("w:w" => "0", "w:type" => "dxa")
92
+ xml["w"].right("w:w" => "108", "w:type" => "dxa")
93
+ end
94
+ end
95
+ end
96
+ end
97
+
78
98
  def render_table_style(xml, style)
79
99
  xml["w"].style("w:type" => "table", "w:styleId" => style.style_id) do
80
100
  xml["w"].name("w:val" => style.display_name)
101
+ xml["w"].basedOn("w:val" => "TableNormal")
81
102
 
82
103
  xml["w"].tblPr do
83
104
  render_table_borders(xml, style.borders) if style.borders
data/lib/notare.rb CHANGED
@@ -17,6 +17,7 @@ require_relative "notare/nodes/table_cell"
17
17
  require_relative "notare/image_dimensions"
18
18
  require_relative "notare/style"
19
19
  require_relative "notare/table_style"
20
+ require_relative "notare/width_parser"
20
21
  require_relative "notare/xml/content_types"
21
22
  require_relative "notare/xml/relationships"
22
23
  require_relative "notare/xml/document_xml"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: notare
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mathias
@@ -135,6 +135,7 @@ files:
135
135
  - lib/notare/style.rb
136
136
  - lib/notare/table_style.rb
137
137
  - lib/notare/version.rb
138
+ - lib/notare/width_parser.rb
138
139
  - lib/notare/xml/content_types.rb
139
140
  - lib/notare/xml/document_xml.rb
140
141
  - lib/notare/xml/numbering.rb