notare 0.0.3 → 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 +4 -4
- data/README.md +159 -3
- data/lib/notare/builder.rb +11 -4
- data/lib/notare/document.rb +22 -4
- data/lib/notare/nodes/table.rb +6 -3
- data/lib/notare/nodes/table_cell.rb +4 -3
- data/lib/notare/package.rb +1 -1
- data/lib/notare/table_style.rb +83 -0
- data/lib/notare/version.rb +1 -1
- data/lib/notare/width_parser.rb +31 -0
- data/lib/notare/xml/document_xml.rb +83 -15
- data/lib/notare/xml/styles_xml.rb +88 -3
- data/lib/notare.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 959b59c2f2dc30ff265115057c1fd304f3b7747adfe16c9818e6de9e5c564fc1
|
|
4
|
+
data.tar.gz: 6c3da88bce319ed81fdb519308d6854c00f6a846343e37178e6bafab2b1fefbb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '00558f142269e3927cdb5aab15271a92533a48ff5ff2cf19c349e8757ccd9da77af9d27a249bc1a2551c910b8578a8bc9a354622cea7c20424183b7230bc06b9'
|
|
7
|
+
data.tar.gz: 2f179ca2e773afe314151b61fb9de4e48a6b0037bba3ce50397636b9f2a26790f5e0399c11f5d807c4f3a3090f562e094d0e02da40b4a77719f1bd4c3ea7ee8a
|
data/README.md
CHANGED
|
@@ -105,6 +105,21 @@ Notare includes built-in styles and supports custom style definitions.
|
|
|
105
105
|
|
|
106
106
|
#### Built-in Styles
|
|
107
107
|
|
|
108
|
+
| Style | Properties |
|
|
109
|
+
|-------|------------|
|
|
110
|
+
| `:title` | 26pt, bold, centered |
|
|
111
|
+
| `:subtitle` | 15pt, italic, gray (#666666) |
|
|
112
|
+
| `:quote` | italic, gray (#666666), indented |
|
|
113
|
+
| `:code` | Courier New, 10pt |
|
|
114
|
+
| `:heading1` | 24pt, bold |
|
|
115
|
+
| `:heading2` | 18pt, bold |
|
|
116
|
+
| `:heading3` | 14pt, bold |
|
|
117
|
+
| `:heading4` | 12pt, bold |
|
|
118
|
+
| `:heading5` | 11pt, bold, italic |
|
|
119
|
+
| `:heading6` | 10pt, bold, italic |
|
|
120
|
+
|
|
121
|
+
Note: `h1` through `h6` methods use the corresponding heading styles automatically.
|
|
122
|
+
|
|
108
123
|
```ruby
|
|
109
124
|
Notare::Document.create("output.docx") do |doc|
|
|
110
125
|
doc.p "This is a title", style: :title
|
|
@@ -246,6 +261,146 @@ Notare::Document.create("output.docx") do |doc|
|
|
|
246
261
|
end
|
|
247
262
|
```
|
|
248
263
|
|
|
264
|
+
#### Table Styles
|
|
265
|
+
|
|
266
|
+
Define reusable table styles with borders, shading, cell margins, and alignment:
|
|
267
|
+
|
|
268
|
+
```ruby
|
|
269
|
+
Notare::Document.create("output.docx") do |doc|
|
|
270
|
+
# Define a custom table style
|
|
271
|
+
doc.define_table_style :fancy,
|
|
272
|
+
borders: { style: "double", color: "0066CC", size: 6 },
|
|
273
|
+
shading: "E6F2FF",
|
|
274
|
+
cell_margins: 100,
|
|
275
|
+
align: :center
|
|
276
|
+
|
|
277
|
+
# Apply the style to a table
|
|
278
|
+
doc.table(style: :fancy) do
|
|
279
|
+
doc.tr do
|
|
280
|
+
doc.td "Product"
|
|
281
|
+
doc.td "Price"
|
|
282
|
+
end
|
|
283
|
+
doc.tr do
|
|
284
|
+
doc.td "Widget"
|
|
285
|
+
doc.td "$10.00"
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
#### Table Style Properties
|
|
292
|
+
|
|
293
|
+
| Property | Description | Example |
|
|
294
|
+
|----------|-------------|---------|
|
|
295
|
+
| `borders` | Border configuration | `{ style: "single", color: "000000", size: 4 }` |
|
|
296
|
+
| `shading` | Background color (hex) | `"EEEEEE"` |
|
|
297
|
+
| `cell_margins` | Cell padding (twips) | `100` or `{ top: 50, bottom: 50, left: 100, right: 100 }` |
|
|
298
|
+
| `align` | Table alignment | `:left`, `:center`, `:right` |
|
|
299
|
+
|
|
300
|
+
**Border styles:** `single`, `double`, `dotted`, `dashed`, `triple`, `none`
|
|
301
|
+
|
|
302
|
+
**Border configuration options:**
|
|
303
|
+
|
|
304
|
+
```ruby
|
|
305
|
+
# All borders the same
|
|
306
|
+
borders: { style: "single", color: "000000", size: 4 }
|
|
307
|
+
|
|
308
|
+
# Per-edge borders
|
|
309
|
+
borders: {
|
|
310
|
+
top: { style: "double", color: "FF0000", size: 8 },
|
|
311
|
+
bottom: { style: "single", color: "000000", size: 4 },
|
|
312
|
+
left: { style: "none" },
|
|
313
|
+
right: { style: "none" },
|
|
314
|
+
insideH: { style: "dotted", color: "CCCCCC", size: 2 },
|
|
315
|
+
insideV: { style: "dotted", color: "CCCCCC", size: 2 }
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
# No borders
|
|
319
|
+
borders: :none
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
#### Built-in Table Styles
|
|
323
|
+
|
|
324
|
+
| Style | Description |
|
|
325
|
+
|-------|-------------|
|
|
326
|
+
| `:grid` | Standard black single-line borders |
|
|
327
|
+
| `:borderless` | No borders |
|
|
328
|
+
|
|
329
|
+
```ruby
|
|
330
|
+
doc.table(style: :borderless) do
|
|
331
|
+
doc.tr { doc.td "No borders here" }
|
|
332
|
+
end
|
|
333
|
+
```
|
|
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
|
+
|
|
249
404
|
### Images
|
|
250
405
|
|
|
251
406
|
Images can be added to paragraphs, table cells, and list items. Supports PNG and JPEG formats.
|
|
@@ -434,15 +589,16 @@ end
|
|
|
434
589
|
| `link(url, text)` | Hyperlink with custom text |
|
|
435
590
|
| `link(url) { }` | Hyperlink with block content |
|
|
436
591
|
| `define_style(name, **props)` | Define a custom style |
|
|
592
|
+
| `define_table_style(name, **props)` | Define a custom table style |
|
|
437
593
|
| `ul { }` | Bullet list (can be nested) |
|
|
438
594
|
| `ol { }` | Numbered list (can be nested) |
|
|
439
595
|
| `li(text)` | List item with text |
|
|
440
596
|
| `li { }` | List item with block content |
|
|
441
597
|
| `li(text) { }` | List item with text and nested content |
|
|
442
|
-
| `table { }` | Table |
|
|
598
|
+
| `table(style:, layout:, columns:) { }` | Table with optional style, layout (`:auto`/`:fixed`), and column widths |
|
|
443
599
|
| `tr { }` | Table row |
|
|
444
|
-
| `td(text)` | Table cell with text |
|
|
445
|
-
| `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 |
|
|
446
602
|
| `image(path, width:, height:)` | Insert image (PNG/JPEG). Dimensions: `"2in"`, `"5cm"`, `"100px"`, or integer pixels |
|
|
447
603
|
|
|
448
604
|
## Development
|
data/lib/notare/builder.rb
CHANGED
|
@@ -100,8 +100,8 @@ module Notare
|
|
|
100
100
|
@current_list.add_item(item)
|
|
101
101
|
end
|
|
102
102
|
|
|
103
|
-
def table(&block)
|
|
104
|
-
tbl = Nodes::Table.new
|
|
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
|
|
@@ -198,5 +198,12 @@ module Notare
|
|
|
198
198
|
|
|
199
199
|
style(style_or_name) || raise(ArgumentError, "Unknown style: #{style_or_name}")
|
|
200
200
|
end
|
|
201
|
+
|
|
202
|
+
def resolve_table_style(style_or_name)
|
|
203
|
+
return nil if style_or_name.nil?
|
|
204
|
+
return style_or_name if style_or_name.is_a?(TableStyle)
|
|
205
|
+
|
|
206
|
+
table_style(style_or_name) || raise(ArgumentError, "Unknown table style: #{style_or_name}")
|
|
207
|
+
end
|
|
201
208
|
end
|
|
202
209
|
end
|
data/lib/notare/document.rb
CHANGED
|
@@ -4,7 +4,7 @@ module Notare
|
|
|
4
4
|
class Document
|
|
5
5
|
include Builder
|
|
6
6
|
|
|
7
|
-
attr_reader :nodes, :styles, :hyperlinks
|
|
7
|
+
attr_reader :nodes, :styles, :table_styles, :hyperlinks
|
|
8
8
|
|
|
9
9
|
def self.create(path, &block)
|
|
10
10
|
doc = new
|
|
@@ -25,7 +25,9 @@ module Notare
|
|
|
25
25
|
@images = {}
|
|
26
26
|
@hyperlinks = []
|
|
27
27
|
@styles = {}
|
|
28
|
+
@table_styles = {}
|
|
28
29
|
register_built_in_styles
|
|
30
|
+
register_built_in_table_styles
|
|
29
31
|
end
|
|
30
32
|
|
|
31
33
|
def define_style(name, **properties)
|
|
@@ -36,6 +38,14 @@ module Notare
|
|
|
36
38
|
@styles[name]
|
|
37
39
|
end
|
|
38
40
|
|
|
41
|
+
def define_table_style(name, **properties)
|
|
42
|
+
@table_styles[name] = TableStyle.new(name, **properties)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def table_style(name)
|
|
46
|
+
@table_styles[name]
|
|
47
|
+
end
|
|
48
|
+
|
|
39
49
|
def save(path)
|
|
40
50
|
Package.new(self).save(path)
|
|
41
51
|
end
|
|
@@ -78,13 +88,13 @@ module Notare
|
|
|
78
88
|
def next_image_rid
|
|
79
89
|
# rId1 = styles.xml (always present)
|
|
80
90
|
# rId2 = numbering.xml (if lists present)
|
|
81
|
-
# rId3+ = images
|
|
91
|
+
# rId3+ = images and hyperlinks share the same ID space
|
|
82
92
|
base = @has_lists ? 3 : 2
|
|
83
|
-
"rId#{base + @images.size}"
|
|
93
|
+
"rId#{base + @images.size + @hyperlinks.size}"
|
|
84
94
|
end
|
|
85
95
|
|
|
86
96
|
def next_hyperlink_rid
|
|
87
|
-
#
|
|
97
|
+
# Images and hyperlinks share the same ID space
|
|
88
98
|
base = @has_lists ? 3 : 2
|
|
89
99
|
"rId#{base + @images.size + @hyperlinks.size}"
|
|
90
100
|
end
|
|
@@ -104,5 +114,13 @@ module Notare
|
|
|
104
114
|
define_style :quote, italic: true, color: "666666", indent: 720
|
|
105
115
|
define_style :code, font: "Courier New", size: 10
|
|
106
116
|
end
|
|
117
|
+
|
|
118
|
+
def register_built_in_table_styles
|
|
119
|
+
define_table_style :grid,
|
|
120
|
+
borders: { style: "single", color: "000000", size: 4 }
|
|
121
|
+
|
|
122
|
+
define_table_style :borderless,
|
|
123
|
+
borders: :none
|
|
124
|
+
end
|
|
107
125
|
end
|
|
108
126
|
end
|
data/lib/notare/nodes/table.rb
CHANGED
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
module Notare
|
|
4
4
|
module Nodes
|
|
5
5
|
class Table < Base
|
|
6
|
-
attr_reader :rows
|
|
6
|
+
attr_reader :rows, :style, :layout, :columns
|
|
7
7
|
|
|
8
|
-
def initialize
|
|
9
|
-
super
|
|
8
|
+
def initialize(style: nil, layout: nil, columns: nil)
|
|
9
|
+
super()
|
|
10
10
|
@rows = []
|
|
11
|
+
@style = style
|
|
12
|
+
@layout = layout
|
|
13
|
+
@columns = columns
|
|
11
14
|
end
|
|
12
15
|
|
|
13
16
|
def add_row(row)
|
data/lib/notare/package.rb
CHANGED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Notare
|
|
4
|
+
class TableStyle
|
|
5
|
+
attr_reader :name, :borders, :shading, :cell_margins, :align
|
|
6
|
+
|
|
7
|
+
BORDER_STYLES = %w[single double dotted dashed triple none nil].freeze
|
|
8
|
+
BORDER_POSITIONS = %i[top bottom left right insideH insideV].freeze
|
|
9
|
+
ALIGNMENTS = %i[left center right].freeze
|
|
10
|
+
|
|
11
|
+
def initialize(name, borders: nil, shading: nil, cell_margins: nil, align: nil)
|
|
12
|
+
@name = name
|
|
13
|
+
@borders = normalize_borders(borders)
|
|
14
|
+
@shading = normalize_color(shading)
|
|
15
|
+
@cell_margins = normalize_cell_margins(cell_margins)
|
|
16
|
+
@align = validate_align(align)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def style_id
|
|
20
|
+
name.to_s.split("_").map(&:capitalize).join
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def display_name
|
|
24
|
+
name.to_s.split("_").map(&:capitalize).join(" ")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def normalize_borders(borders)
|
|
30
|
+
return nil if borders.nil?
|
|
31
|
+
return :none if borders == :none
|
|
32
|
+
|
|
33
|
+
# Check if it's a per-edge configuration
|
|
34
|
+
if borders.keys.any? { |k| BORDER_POSITIONS.include?(k) }
|
|
35
|
+
borders.transform_values { |v| normalize_single_border(v) }
|
|
36
|
+
else
|
|
37
|
+
# Single border config applied to all edges
|
|
38
|
+
normalize_single_border(borders)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def normalize_single_border(border)
|
|
43
|
+
return :none if border == :none || border[:style] == "none"
|
|
44
|
+
|
|
45
|
+
style = border[:style] || "single"
|
|
46
|
+
unless BORDER_STYLES.include?(style)
|
|
47
|
+
raise ArgumentError, "Invalid border style: #{style}. Use #{BORDER_STYLES.join(", ")}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
{
|
|
51
|
+
style: style,
|
|
52
|
+
color: normalize_color(border[:color]) || "000000",
|
|
53
|
+
size: border[:size] || 4
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def normalize_color(color)
|
|
58
|
+
return nil if color.nil?
|
|
59
|
+
|
|
60
|
+
hex = color.to_s.sub(/^#/, "").upcase
|
|
61
|
+
return hex if hex.match?(/\A[0-9A-F]{6}\z/)
|
|
62
|
+
|
|
63
|
+
raise ArgumentError, "Invalid color: #{color}. Use 6-digit hex (e.g., 'FF0000')"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def normalize_cell_margins(margins)
|
|
67
|
+
return nil if margins.nil?
|
|
68
|
+
|
|
69
|
+
if margins.is_a?(Hash)
|
|
70
|
+
margins.slice(:top, :bottom, :left, :right)
|
|
71
|
+
else
|
|
72
|
+
margins.to_i
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def validate_align(align)
|
|
77
|
+
return nil if align.nil?
|
|
78
|
+
return align if ALIGNMENTS.include?(align)
|
|
79
|
+
|
|
80
|
+
raise ArgumentError, "Invalid alignment: #{align}. Use #{ALIGNMENTS.join(", ")}"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
data/lib/notare/version.rb
CHANGED
|
@@ -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,37 +163,105 @@ module Notare
|
|
|
163
163
|
end
|
|
164
164
|
|
|
165
165
|
def render_table(xml, table)
|
|
166
|
-
|
|
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
|
|
172
|
-
xml
|
|
173
|
-
|
|
174
|
-
|
|
170
|
+
render_table_width(xml, column_widths)
|
|
171
|
+
render_table_layout(xml, table.layout)
|
|
172
|
+
if table.style
|
|
173
|
+
xml["w"].tblStyle("w:val" => table.style.style_id)
|
|
174
|
+
else
|
|
175
|
+
xml["w"].tblBorders do
|
|
176
|
+
%w[top left bottom right insideH insideV].each do |border|
|
|
177
|
+
xml["w"].send(border, "w:val" => "single", "w:sz" => "4", "w:space" => "0", "w:color" => "000000")
|
|
178
|
+
end
|
|
175
179
|
end
|
|
176
180
|
end
|
|
177
181
|
end
|
|
178
|
-
xml
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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")
|
|
209
|
+
end
|
|
210
|
+
else
|
|
211
|
+
col_width = 5000 / cells.size
|
|
212
|
+
cells.map { WidthParser::ParsedWidth.new(value: col_width, type: "pct") }
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
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)
|
|
182
244
|
end
|
|
183
|
-
table.rows.each { |row| render_table_row(xml, row, col_width) }
|
|
184
245
|
end
|
|
185
246
|
end
|
|
186
247
|
|
|
187
|
-
|
|
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)
|
|
188
255
|
xml["w"].tr do
|
|
189
|
-
row.cells.
|
|
256
|
+
row.cells.each_with_index { |cell, idx| render_table_cell(xml, cell, column_widths[idx]) }
|
|
190
257
|
end
|
|
191
258
|
end
|
|
192
259
|
|
|
193
|
-
def render_table_cell(xml, cell,
|
|
260
|
+
def render_table_cell(xml, cell, column_width)
|
|
261
|
+
width = column_width || WidthParser::ParsedWidth.new(value: 0, type: "auto")
|
|
194
262
|
xml["w"].tc do
|
|
195
263
|
xml["w"].tcPr do
|
|
196
|
-
xml["w"].tcW("w:w" =>
|
|
264
|
+
xml["w"].tcW("w:w" => width.value.to_s, "w:type" => width.type)
|
|
197
265
|
end
|
|
198
266
|
xml["w"].p do
|
|
199
267
|
cell.runs.each { |run| render_run(xml, run) }
|
|
@@ -12,8 +12,15 @@ module Notare
|
|
|
12
12
|
justify: "both"
|
|
13
13
|
}.freeze
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
TABLE_ALIGNMENT_MAP = {
|
|
16
|
+
left: "left",
|
|
17
|
+
center: "center",
|
|
18
|
+
right: "right"
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
def initialize(styles, table_styles = {})
|
|
16
22
|
@styles = styles
|
|
23
|
+
@table_styles = table_styles
|
|
17
24
|
end
|
|
18
25
|
|
|
19
26
|
def to_xml
|
|
@@ -24,6 +31,12 @@ module Notare
|
|
|
24
31
|
@styles.each_value do |style|
|
|
25
32
|
render_style(xml, style)
|
|
26
33
|
end
|
|
34
|
+
|
|
35
|
+
render_table_normal_style(xml) if @table_styles.any?
|
|
36
|
+
|
|
37
|
+
@table_styles.each_value do |style|
|
|
38
|
+
render_table_style(xml, style)
|
|
39
|
+
end
|
|
27
40
|
end
|
|
28
41
|
end
|
|
29
42
|
builder.to_xml
|
|
@@ -46,8 +59,12 @@ module Notare
|
|
|
46
59
|
xml["w"].pPr do
|
|
47
60
|
xml["w"].jc("w:val" => ALIGNMENT_MAP[style.align]) if style.align
|
|
48
61
|
xml["w"].ind("w:left" => style.indent.to_s) if style.indent
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
51
68
|
end
|
|
52
69
|
end
|
|
53
70
|
|
|
@@ -63,6 +80,74 @@ module Notare
|
|
|
63
80
|
xml["w"].highlight("w:val" => style.highlight) if style.highlight
|
|
64
81
|
end
|
|
65
82
|
end
|
|
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
|
+
|
|
98
|
+
def render_table_style(xml, style)
|
|
99
|
+
xml["w"].style("w:type" => "table", "w:styleId" => style.style_id) do
|
|
100
|
+
xml["w"].name("w:val" => style.display_name)
|
|
101
|
+
xml["w"].basedOn("w:val" => "TableNormal")
|
|
102
|
+
|
|
103
|
+
xml["w"].tblPr do
|
|
104
|
+
render_table_borders(xml, style.borders) if style.borders
|
|
105
|
+
render_table_shading(xml, style.shading) if style.shading
|
|
106
|
+
render_table_cell_margins(xml, style.cell_margins) if style.cell_margins
|
|
107
|
+
xml["w"].jc("w:val" => TABLE_ALIGNMENT_MAP[style.align]) if style.align
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def render_table_borders(xml, borders)
|
|
113
|
+
xml["w"].tblBorders do
|
|
114
|
+
%i[top left bottom right insideH insideV].each do |pos|
|
|
115
|
+
border = borders == :none ? :none : (borders[pos] || borders)
|
|
116
|
+
render_single_border(xml, pos, border)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def render_single_border(xml, position, border)
|
|
122
|
+
if border == :none
|
|
123
|
+
xml["w"].send(position, "w:val" => "nil")
|
|
124
|
+
else
|
|
125
|
+
xml["w"].send(position,
|
|
126
|
+
"w:val" => border[:style],
|
|
127
|
+
"w:sz" => border[:size].to_s,
|
|
128
|
+
"w:space" => "0",
|
|
129
|
+
"w:color" => border[:color])
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def render_table_shading(xml, color)
|
|
134
|
+
xml["w"].shd("w:val" => "clear", "w:color" => "auto", "w:fill" => color)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def render_table_cell_margins(xml, margins)
|
|
138
|
+
xml["w"].tblCellMar do
|
|
139
|
+
if margins.is_a?(Hash)
|
|
140
|
+
xml["w"].top("w:w" => margins[:top].to_s, "w:type" => "dxa") if margins[:top]
|
|
141
|
+
xml["w"].left("w:w" => margins[:left].to_s, "w:type" => "dxa") if margins[:left]
|
|
142
|
+
xml["w"].bottom("w:w" => margins[:bottom].to_s, "w:type" => "dxa") if margins[:bottom]
|
|
143
|
+
xml["w"].right("w:w" => margins[:right].to_s, "w:type" => "dxa") if margins[:right]
|
|
144
|
+
else
|
|
145
|
+
%i[top left bottom right].each do |side|
|
|
146
|
+
xml["w"].send(side, "w:w" => margins.to_s, "w:type" => "dxa")
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
66
151
|
end
|
|
67
152
|
end
|
|
68
153
|
end
|
data/lib/notare.rb
CHANGED
|
@@ -16,6 +16,8 @@ require_relative "notare/nodes/table_row"
|
|
|
16
16
|
require_relative "notare/nodes/table_cell"
|
|
17
17
|
require_relative "notare/image_dimensions"
|
|
18
18
|
require_relative "notare/style"
|
|
19
|
+
require_relative "notare/table_style"
|
|
20
|
+
require_relative "notare/width_parser"
|
|
19
21
|
require_relative "notare/xml/content_types"
|
|
20
22
|
require_relative "notare/xml/relationships"
|
|
21
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
|
+
version: 0.0.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mathias
|
|
@@ -133,7 +133,9 @@ files:
|
|
|
133
133
|
- lib/notare/nodes/table_row.rb
|
|
134
134
|
- lib/notare/package.rb
|
|
135
135
|
- lib/notare/style.rb
|
|
136
|
+
- lib/notare/table_style.rb
|
|
136
137
|
- lib/notare/version.rb
|
|
138
|
+
- lib/notare/width_parser.rb
|
|
137
139
|
- lib/notare/xml/content_types.rb
|
|
138
140
|
- lib/notare/xml/document_xml.rb
|
|
139
141
|
- lib/notare/xml/numbering.rb
|