Implement table styles
All checks were successful
CI Pipeline / build (pull_request) Successful in 14s
All checks were successful
CI Pipeline / build (pull_request) Successful in 14s
This commit is contained in:
@@ -100,8 +100,8 @@ module Notare
|
||||
@current_list.add_item(item)
|
||||
end
|
||||
|
||||
def table(&block)
|
||||
tbl = Nodes::Table.new
|
||||
def table(style: nil, &block)
|
||||
tbl = Nodes::Table.new(style: resolve_table_style(style))
|
||||
previous_table = @current_table
|
||||
@current_table = tbl
|
||||
block.call
|
||||
@@ -198,5 +198,12 @@ module Notare
|
||||
|
||||
style(style_or_name) || raise(ArgumentError, "Unknown style: #{style_or_name}")
|
||||
end
|
||||
|
||||
def resolve_table_style(style_or_name)
|
||||
return nil if style_or_name.nil?
|
||||
return style_or_name if style_or_name.is_a?(TableStyle)
|
||||
|
||||
table_style(style_or_name) || raise(ArgumentError, "Unknown table style: #{style_or_name}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,7 +4,7 @@ module Notare
|
||||
class Document
|
||||
include Builder
|
||||
|
||||
attr_reader :nodes, :styles, :hyperlinks
|
||||
attr_reader :nodes, :styles, :table_styles, :hyperlinks
|
||||
|
||||
def self.create(path, &block)
|
||||
doc = new
|
||||
@@ -25,7 +25,9 @@ module Notare
|
||||
@images = {}
|
||||
@hyperlinks = []
|
||||
@styles = {}
|
||||
@table_styles = {}
|
||||
register_built_in_styles
|
||||
register_built_in_table_styles
|
||||
end
|
||||
|
||||
def define_style(name, **properties)
|
||||
@@ -36,6 +38,14 @@ module Notare
|
||||
@styles[name]
|
||||
end
|
||||
|
||||
def define_table_style(name, **properties)
|
||||
@table_styles[name] = TableStyle.new(name, **properties)
|
||||
end
|
||||
|
||||
def table_style(name)
|
||||
@table_styles[name]
|
||||
end
|
||||
|
||||
def save(path)
|
||||
Package.new(self).save(path)
|
||||
end
|
||||
@@ -104,5 +114,13 @@ module Notare
|
||||
define_style :quote, italic: true, color: "666666", indent: 720
|
||||
define_style :code, font: "Courier New", size: 10
|
||||
end
|
||||
|
||||
def register_built_in_table_styles
|
||||
define_table_style :grid,
|
||||
borders: { style: "single", color: "000000", size: 4 }
|
||||
|
||||
define_table_style :borderless,
|
||||
borders: :none
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
module Notare
|
||||
module Nodes
|
||||
class Table < Base
|
||||
attr_reader :rows
|
||||
attr_reader :rows, :style
|
||||
|
||||
def initialize
|
||||
super
|
||||
def initialize(style: nil)
|
||||
super()
|
||||
@rows = []
|
||||
@style = style
|
||||
end
|
||||
|
||||
def add_row(row)
|
||||
|
||||
@@ -59,7 +59,7 @@ module Notare
|
||||
end
|
||||
|
||||
def styles_xml
|
||||
Xml::StylesXml.new(@document.styles).to_xml
|
||||
Xml::StylesXml.new(@document.styles, @document.table_styles).to_xml
|
||||
end
|
||||
|
||||
def numbering_xml
|
||||
|
||||
83
lib/notare/table_style.rb
Normal file
83
lib/notare/table_style.rb
Normal file
@@ -0,0 +1,83 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Notare
|
||||
class TableStyle
|
||||
attr_reader :name, :borders, :shading, :cell_margins, :align
|
||||
|
||||
BORDER_STYLES = %w[single double dotted dashed triple none nil].freeze
|
||||
BORDER_POSITIONS = %i[top bottom left right insideH insideV].freeze
|
||||
ALIGNMENTS = %i[left center right].freeze
|
||||
|
||||
def initialize(name, borders: nil, shading: nil, cell_margins: nil, align: nil)
|
||||
@name = name
|
||||
@borders = normalize_borders(borders)
|
||||
@shading = normalize_color(shading)
|
||||
@cell_margins = normalize_cell_margins(cell_margins)
|
||||
@align = validate_align(align)
|
||||
end
|
||||
|
||||
def style_id
|
||||
name.to_s.split("_").map(&:capitalize).join
|
||||
end
|
||||
|
||||
def display_name
|
||||
name.to_s.split("_").map(&:capitalize).join(" ")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def normalize_borders(borders)
|
||||
return nil if borders.nil?
|
||||
return :none if borders == :none
|
||||
|
||||
# Check if it's a per-edge configuration
|
||||
if borders.keys.any? { |k| BORDER_POSITIONS.include?(k) }
|
||||
borders.transform_values { |v| normalize_single_border(v) }
|
||||
else
|
||||
# Single border config applied to all edges
|
||||
normalize_single_border(borders)
|
||||
end
|
||||
end
|
||||
|
||||
def normalize_single_border(border)
|
||||
return :none if border == :none || border[:style] == "none"
|
||||
|
||||
style = border[:style] || "single"
|
||||
unless BORDER_STYLES.include?(style)
|
||||
raise ArgumentError, "Invalid border style: #{style}. Use #{BORDER_STYLES.join(", ")}"
|
||||
end
|
||||
|
||||
{
|
||||
style: style,
|
||||
color: normalize_color(border[:color]) || "000000",
|
||||
size: border[:size] || 4
|
||||
}
|
||||
end
|
||||
|
||||
def normalize_color(color)
|
||||
return nil if color.nil?
|
||||
|
||||
hex = color.to_s.sub(/^#/, "").upcase
|
||||
return hex if hex.match?(/\A[0-9A-F]{6}\z/)
|
||||
|
||||
raise ArgumentError, "Invalid color: #{color}. Use 6-digit hex (e.g., 'FF0000')"
|
||||
end
|
||||
|
||||
def normalize_cell_margins(margins)
|
||||
return nil if margins.nil?
|
||||
|
||||
if margins.is_a?(Hash)
|
||||
margins.slice(:top, :bottom, :left, :right)
|
||||
else
|
||||
margins.to_i
|
||||
end
|
||||
end
|
||||
|
||||
def validate_align(align)
|
||||
return nil if align.nil?
|
||||
return align if ALIGNMENTS.include?(align)
|
||||
|
||||
raise ArgumentError, "Invalid alignment: #{align}. Use #{ALIGNMENTS.join(", ")}"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -169,9 +169,13 @@ module Notare
|
||||
xml["w"].tbl do
|
||||
xml["w"].tblPr do
|
||||
xml["w"].tblW("w:w" => "5000", "w:type" => "pct")
|
||||
xml["w"].tblBorders do
|
||||
%w[top left bottom right insideH insideV].each do |border|
|
||||
xml["w"].send(border, "w:val" => "single", "w:sz" => "4", "w:space" => "0", "w:color" => "000000")
|
||||
if table.style
|
||||
xml["w"].tblStyle("w:val" => table.style.style_id)
|
||||
else
|
||||
xml["w"].tblBorders do
|
||||
%w[top left bottom right insideH insideV].each do |border|
|
||||
xml["w"].send(border, "w:val" => "single", "w:sz" => "4", "w:space" => "0", "w:color" => "000000")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,8 +12,15 @@ module Notare
|
||||
justify: "both"
|
||||
}.freeze
|
||||
|
||||
def initialize(styles)
|
||||
TABLE_ALIGNMENT_MAP = {
|
||||
left: "left",
|
||||
center: "center",
|
||||
right: "right"
|
||||
}.freeze
|
||||
|
||||
def initialize(styles, table_styles = {})
|
||||
@styles = styles
|
||||
@table_styles = table_styles
|
||||
end
|
||||
|
||||
def to_xml
|
||||
@@ -24,6 +31,10 @@ module Notare
|
||||
@styles.each_value do |style|
|
||||
render_style(xml, style)
|
||||
end
|
||||
|
||||
@table_styles.each_value do |style|
|
||||
render_table_style(xml, style)
|
||||
end
|
||||
end
|
||||
end
|
||||
builder.to_xml
|
||||
@@ -63,6 +74,59 @@ module Notare
|
||||
xml["w"].highlight("w:val" => style.highlight) if style.highlight
|
||||
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"].tblPr do
|
||||
render_table_borders(xml, style.borders) if style.borders
|
||||
render_table_shading(xml, style.shading) if style.shading
|
||||
render_table_cell_margins(xml, style.cell_margins) if style.cell_margins
|
||||
xml["w"].jc("w:val" => TABLE_ALIGNMENT_MAP[style.align]) if style.align
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def render_table_borders(xml, borders)
|
||||
xml["w"].tblBorders do
|
||||
%i[top left bottom right insideH insideV].each do |pos|
|
||||
border = borders == :none ? :none : (borders[pos] || borders)
|
||||
render_single_border(xml, pos, border)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def render_single_border(xml, position, border)
|
||||
if border == :none
|
||||
xml["w"].send(position, "w:val" => "nil")
|
||||
else
|
||||
xml["w"].send(position,
|
||||
"w:val" => border[:style],
|
||||
"w:sz" => border[:size].to_s,
|
||||
"w:space" => "0",
|
||||
"w:color" => border[:color])
|
||||
end
|
||||
end
|
||||
|
||||
def render_table_shading(xml, color)
|
||||
xml["w"].shd("w:val" => "clear", "w:color" => "auto", "w:fill" => color)
|
||||
end
|
||||
|
||||
def render_table_cell_margins(xml, margins)
|
||||
xml["w"].tblCellMar do
|
||||
if margins.is_a?(Hash)
|
||||
xml["w"].top("w:w" => margins[:top].to_s, "w:type" => "dxa") if margins[:top]
|
||||
xml["w"].left("w:w" => margins[:left].to_s, "w:type" => "dxa") if margins[:left]
|
||||
xml["w"].bottom("w:w" => margins[:bottom].to_s, "w:type" => "dxa") if margins[:bottom]
|
||||
xml["w"].right("w:w" => margins[:right].to_s, "w:type" => "dxa") if margins[:right]
|
||||
else
|
||||
%i[top left bottom right].each do |side|
|
||||
xml["w"].send(side, "w:w" => margins.to_s, "w:type" => "dxa")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user