Support table and table column sizing
All checks were successful
CI Pipeline / build (pull_request) Successful in 13s
All checks were successful
CI Pipeline / build (pull_request) Successful in 13s
This commit is contained in:
@@ -17,6 +17,7 @@ require_relative "notare/nodes/table_cell"
|
||||
require_relative "notare/image_dimensions"
|
||||
require_relative "notare/style"
|
||||
require_relative "notare/table_style"
|
||||
require_relative "notare/width_parser"
|
||||
require_relative "notare/xml/content_types"
|
||||
require_relative "notare/xml/relationships"
|
||||
require_relative "notare/xml/document_xml"
|
||||
|
||||
@@ -100,8 +100,8 @@ module Notare
|
||||
@current_list.add_item(item)
|
||||
end
|
||||
|
||||
def table(style: nil, &block)
|
||||
tbl = Nodes::Table.new(style: resolve_table_style(style))
|
||||
def table(style: nil, layout: nil, columns: nil, &block)
|
||||
tbl = Nodes::Table.new(style: resolve_table_style(style), layout: layout, columns: columns)
|
||||
previous_table = @current_table
|
||||
@current_table = tbl
|
||||
block.call
|
||||
@@ -118,8 +118,8 @@ module Notare
|
||||
@current_table.add_row(row)
|
||||
end
|
||||
|
||||
def td(text = nil, &block)
|
||||
cell = Nodes::TableCell.new
|
||||
def td(text = nil, width: nil, &block)
|
||||
cell = Nodes::TableCell.new(width: width)
|
||||
if block
|
||||
with_target(cell, &block)
|
||||
elsif text
|
||||
|
||||
@@ -88,13 +88,13 @@ module Notare
|
||||
def next_image_rid
|
||||
# rId1 = styles.xml (always present)
|
||||
# rId2 = numbering.xml (if lists present)
|
||||
# rId3+ = images, then hyperlinks
|
||||
# rId3+ = images and hyperlinks share the same ID space
|
||||
base = @has_lists ? 3 : 2
|
||||
"rId#{base + @images.size}"
|
||||
"rId#{base + @images.size + @hyperlinks.size}"
|
||||
end
|
||||
|
||||
def next_hyperlink_rid
|
||||
# Hyperlinks come after images
|
||||
# Images and hyperlinks share the same ID space
|
||||
base = @has_lists ? 3 : 2
|
||||
"rId#{base + @images.size + @hyperlinks.size}"
|
||||
end
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
module Notare
|
||||
module Nodes
|
||||
class Table < Base
|
||||
attr_reader :rows, :style
|
||||
attr_reader :rows, :style, :layout, :columns
|
||||
|
||||
def initialize(style: nil)
|
||||
def initialize(style: nil, layout: nil, columns: nil)
|
||||
super()
|
||||
@rows = []
|
||||
@style = style
|
||||
@layout = layout
|
||||
@columns = columns
|
||||
end
|
||||
|
||||
def add_row(row)
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
module Notare
|
||||
module Nodes
|
||||
class TableCell < Base
|
||||
attr_reader :runs
|
||||
attr_reader :runs, :width
|
||||
|
||||
def initialize
|
||||
super
|
||||
def initialize(width: nil)
|
||||
super()
|
||||
@runs = []
|
||||
@width = width
|
||||
end
|
||||
|
||||
def add_run(run)
|
||||
|
||||
31
lib/notare/width_parser.rb
Normal file
31
lib/notare/width_parser.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Notare
|
||||
module WidthParser
|
||||
TWIPS_PER_INCH = 1440
|
||||
TWIPS_PER_CM = 567
|
||||
PCT_MULTIPLIER = 50
|
||||
|
||||
ParsedWidth = Struct.new(:value, :type, keyword_init: true)
|
||||
|
||||
def self.parse(value)
|
||||
case value
|
||||
when :auto, nil
|
||||
ParsedWidth.new(value: 0, type: "auto")
|
||||
when Integer
|
||||
ParsedWidth.new(value: value, type: "dxa")
|
||||
when /\A(\d+(?:\.\d+)?)\s*in\z/i
|
||||
twips = (::Regexp.last_match(1).to_f * TWIPS_PER_INCH).to_i
|
||||
ParsedWidth.new(value: twips, type: "dxa")
|
||||
when /\A(\d+(?:\.\d+)?)\s*cm\z/i
|
||||
twips = (::Regexp.last_match(1).to_f * TWIPS_PER_CM).to_i
|
||||
ParsedWidth.new(value: twips, type: "dxa")
|
||||
when /\A(\d+(?:\.\d+)?)\s*%\z/
|
||||
pct = (::Regexp.last_match(1).to_f * PCT_MULTIPLIER).to_i
|
||||
ParsedWidth.new(value: pct, type: "pct")
|
||||
else
|
||||
raise ArgumentError, "Invalid width: #{value}. Use '2in', '5cm', '50%', :auto, or integer twips."
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -163,12 +163,12 @@ module Notare
|
||||
end
|
||||
|
||||
def render_table(xml, table)
|
||||
column_count = table.rows.first&.cells&.size || 1
|
||||
col_width = 5000 / column_count
|
||||
column_widths = compute_column_widths(table)
|
||||
|
||||
xml["w"].tbl do
|
||||
xml["w"].tblPr do
|
||||
xml["w"].tblW("w:w" => "5000", "w:type" => "pct")
|
||||
render_table_width(xml, column_widths)
|
||||
render_table_layout(xml, table.layout)
|
||||
if table.style
|
||||
xml["w"].tblStyle("w:val" => table.style.style_id)
|
||||
else
|
||||
@@ -179,25 +179,89 @@ module Notare
|
||||
end
|
||||
end
|
||||
end
|
||||
xml["w"].tblGrid do
|
||||
column_count.times do
|
||||
xml["w"].gridCol("w:w" => col_width.to_s)
|
||||
end
|
||||
render_table_grid(xml, column_widths)
|
||||
table.rows.each { |row| render_table_row(xml, row, column_widths) }
|
||||
end
|
||||
end
|
||||
|
||||
def compute_column_widths(table)
|
||||
if table.columns
|
||||
table.columns.map { |c| WidthParser.parse(c) }
|
||||
elsif table.layout == :auto
|
||||
first_row = table.rows.first
|
||||
cell_count = first_row&.cells&.size || 1
|
||||
Array.new(cell_count) { WidthParser::ParsedWidth.new(value: 0, type: "auto") }
|
||||
else
|
||||
infer_widths_from_first_row(table)
|
||||
end
|
||||
end
|
||||
|
||||
def infer_widths_from_first_row(table)
|
||||
first_row = table.rows.first
|
||||
return [WidthParser::ParsedWidth.new(value: 5000, type: "pct")] unless first_row
|
||||
|
||||
cells = first_row.cells
|
||||
has_explicit_widths = cells.any?(&:width)
|
||||
|
||||
if has_explicit_widths
|
||||
cells.map do |cell|
|
||||
cell.width ? WidthParser.parse(cell.width) : WidthParser::ParsedWidth.new(value: 0, type: "auto")
|
||||
end
|
||||
table.rows.each { |row| render_table_row(xml, row, col_width) }
|
||||
else
|
||||
col_width = 5000 / cells.size
|
||||
cells.map { WidthParser::ParsedWidth.new(value: col_width, type: "pct") }
|
||||
end
|
||||
end
|
||||
|
||||
def render_table_row(xml, row, col_width)
|
||||
def render_table_width(xml, column_widths)
|
||||
if column_widths.all? { |w| w.type == "pct" }
|
||||
total = column_widths.sum(&:value)
|
||||
xml["w"].tblW("w:w" => total.to_s, "w:type" => "pct")
|
||||
elsif column_widths.all? { |w| w.type == "dxa" }
|
||||
total = column_widths.sum(&:value)
|
||||
xml["w"].tblW("w:w" => total.to_s, "w:type" => "dxa")
|
||||
else
|
||||
xml["w"].tblW("w:w" => "0", "w:type" => "auto")
|
||||
end
|
||||
end
|
||||
|
||||
def render_table_layout(xml, layout)
|
||||
return unless layout
|
||||
|
||||
layout_type = layout == :auto ? "autofit" : "fixed"
|
||||
xml["w"].tblLayout("w:type" => layout_type)
|
||||
end
|
||||
|
||||
def render_table_grid(xml, column_widths)
|
||||
xml["w"].tblGrid do
|
||||
column_widths.each do |width|
|
||||
grid_width = case width.type
|
||||
when "pct" then pct_to_approximate_dxa(width.value)
|
||||
when "auto" then 1440 # Default 1 inch for auto columns
|
||||
else width.value
|
||||
end
|
||||
xml["w"].gridCol("w:w" => grid_width.to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Convert percentage (in fiftieths) to approximate twips
|
||||
# Assumes 6.5 inch content width = 9360 twips
|
||||
def pct_to_approximate_dxa(pct_value)
|
||||
(pct_value * 9360 / 5000.0).to_i
|
||||
end
|
||||
|
||||
def render_table_row(xml, row, column_widths)
|
||||
xml["w"].tr do
|
||||
row.cells.each { |cell| render_table_cell(xml, cell, col_width) }
|
||||
row.cells.each_with_index { |cell, idx| render_table_cell(xml, cell, column_widths[idx]) }
|
||||
end
|
||||
end
|
||||
|
||||
def render_table_cell(xml, cell, col_width)
|
||||
def render_table_cell(xml, cell, column_width)
|
||||
width = column_width || WidthParser::ParsedWidth.new(value: 0, type: "auto")
|
||||
xml["w"].tc do
|
||||
xml["w"].tcPr do
|
||||
xml["w"].tcW("w:w" => col_width.to_s, "w:type" => "pct")
|
||||
xml["w"].tcW("w:w" => width.value.to_s, "w:type" => width.type)
|
||||
end
|
||||
xml["w"].p do
|
||||
cell.runs.each { |run| render_run(xml, run) }
|
||||
|
||||
@@ -32,6 +32,8 @@ module Notare
|
||||
render_style(xml, style)
|
||||
end
|
||||
|
||||
render_table_normal_style(xml) if @table_styles.any?
|
||||
|
||||
@table_styles.each_value do |style|
|
||||
render_table_style(xml, style)
|
||||
end
|
||||
@@ -57,8 +59,12 @@ module Notare
|
||||
xml["w"].pPr do
|
||||
xml["w"].jc("w:val" => ALIGNMENT_MAP[style.align]) if style.align
|
||||
xml["w"].ind("w:left" => style.indent.to_s) if style.indent
|
||||
xml["w"].spacing("w:before" => style.spacing_before.to_s) if style.spacing_before
|
||||
xml["w"].spacing("w:after" => style.spacing_after.to_s) if style.spacing_after
|
||||
if style.spacing_before || style.spacing_after
|
||||
spacing_attrs = {}
|
||||
spacing_attrs["w:before"] = style.spacing_before.to_s if style.spacing_before
|
||||
spacing_attrs["w:after"] = style.spacing_after.to_s if style.spacing_after
|
||||
xml["w"].spacing(spacing_attrs)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -75,9 +81,24 @@ module Notare
|
||||
end
|
||||
end
|
||||
|
||||
def render_table_normal_style(xml)
|
||||
xml["w"].style("w:type" => "table", "w:default" => "1", "w:styleId" => "TableNormal") do
|
||||
xml["w"].name("w:val" => "Normal Table")
|
||||
xml["w"].tblPr do
|
||||
xml["w"].tblCellMar do
|
||||
xml["w"].top("w:w" => "0", "w:type" => "dxa")
|
||||
xml["w"].left("w:w" => "108", "w:type" => "dxa")
|
||||
xml["w"].bottom("w:w" => "0", "w:type" => "dxa")
|
||||
xml["w"].right("w:w" => "108", "w:type" => "dxa")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def render_table_style(xml, style)
|
||||
xml["w"].style("w:type" => "table", "w:styleId" => style.style_id) do
|
||||
xml["w"].name("w:val" => style.display_name)
|
||||
xml["w"].basedOn("w:val" => "TableNormal")
|
||||
|
||||
xml["w"].tblPr do
|
||||
render_table_borders(xml, style.borders) if style.borders
|
||||
|
||||
Reference in New Issue
Block a user