4 Commits

Author SHA1 Message Date
9a70d91fd5 Implement table styles
All checks were successful
CI Pipeline / build (pull_request) Successful in 14s
2025-12-03 12:14:31 +01:00
67a60c8c6e Update readme
All checks were successful
CI Pipeline / build (push) Successful in 12s
2025-12-03 11:57:02 +01:00
52d715a6de Merge pull request 'Implement nested lists' (#7) from feature/nested-lists into main
All checks were successful
CI Pipeline / build (push) Successful in 12s
Reviewed-on: #7
2025-12-02 14:00:57 +00:00
75b3a163c7 Implement nested lists
All checks were successful
CI Pipeline / build (pull_request) Successful in 13s
2025-12-02 15:00:39 +01:00
16 changed files with 793 additions and 43 deletions

View File

@@ -1,7 +1,8 @@
{
"permissions": {
"allow": [
"Bash(bundle exec rake test:*)"
"Bash(bundle exec rake test:*)",
"Bash(bundle exec rake:*)"
],
"deny": [],
"ask": []

128
README.md
View File

@@ -105,6 +105,21 @@ Notare includes built-in styles and supports custom style definitions.
#### Built-in Styles
| Style | Properties |
|-------|------------|
| `:title` | 26pt, bold, centered |
| `:subtitle` | 15pt, italic, gray (#666666) |
| `:quote` | italic, gray (#666666), indented |
| `:code` | Courier New, 10pt |
| `:heading1` | 24pt, bold |
| `:heading2` | 18pt, bold |
| `:heading3` | 14pt, bold |
| `:heading4` | 12pt, bold |
| `:heading5` | 11pt, bold, italic |
| `:heading6` | 10pt, bold, italic |
Note: `h1` through `h6` methods use the corresponding heading styles automatically.
```ruby
Notare::Document.create("output.docx") do |doc|
doc.p "This is a title", style: :title
@@ -195,6 +210,40 @@ Notare::Document.create("output.docx") do |doc|
end
```
#### Nested Lists
Lists can be nested inside list items. Mixed types (bullets inside numbered or vice versa) are supported.
```ruby
Notare::Document.create("output.docx") do |doc|
doc.ol do
doc.li "First item"
doc.li "Second item" do
doc.ul do
doc.li "Nested bullet A"
doc.li "Nested bullet B"
end
end
doc.li "Third item"
end
# Deeply nested
doc.ul do
doc.li "Level 0"
doc.li "Has children" do
doc.ul do
doc.li "Level 1"
doc.li "Goes deeper" do
doc.ul do
doc.li "Level 2"
end
end
end
end
end
end
```
### Tables
```ruby
@@ -212,6 +261,77 @@ Notare::Document.create("output.docx") do |doc|
end
```
#### Table Styles
Define reusable table styles with borders, shading, cell margins, and alignment:
```ruby
Notare::Document.create("output.docx") do |doc|
# Define a custom table style
doc.define_table_style :fancy,
borders: { style: "double", color: "0066CC", size: 6 },
shading: "E6F2FF",
cell_margins: 100,
align: :center
# Apply the style to a table
doc.table(style: :fancy) do
doc.tr do
doc.td "Product"
doc.td "Price"
end
doc.tr do
doc.td "Widget"
doc.td "$10.00"
end
end
end
```
#### Table Style Properties
| Property | Description | Example |
|----------|-------------|---------|
| `borders` | Border configuration | `{ style: "single", color: "000000", size: 4 }` |
| `shading` | Background color (hex) | `"EEEEEE"` |
| `cell_margins` | Cell padding (twips) | `100` or `{ top: 50, bottom: 50, left: 100, right: 100 }` |
| `align` | Table alignment | `:left`, `:center`, `:right` |
**Border styles:** `single`, `double`, `dotted`, `dashed`, `triple`, `none`
**Border configuration options:**
```ruby
# All borders the same
borders: { style: "single", color: "000000", size: 4 }
# Per-edge borders
borders: {
top: { style: "double", color: "FF0000", size: 8 },
bottom: { style: "single", color: "000000", size: 4 },
left: { style: "none" },
right: { style: "none" },
insideH: { style: "dotted", color: "CCCCCC", size: 2 },
insideV: { style: "dotted", color: "CCCCCC", size: 2 }
}
# No borders
borders: :none
```
#### Built-in Table Styles
| Style | Description |
|-------|-------------|
| `:grid` | Standard black single-line borders |
| `:borderless` | No borders |
```ruby
doc.table(style: :borderless) do
doc.tr { doc.td "No borders here" }
end
```
### Images
Images can be added to paragraphs, table cells, and list items. Supports PNG and JPEG formats.
@@ -400,11 +520,13 @@ end
| `link(url, text)` | Hyperlink with custom text |
| `link(url) { }` | Hyperlink with block content |
| `define_style(name, **props)` | Define a custom style |
| `ul { }` | Bullet list |
| `ol { }` | Numbered list |
| `define_table_style(name, **props)` | Define a custom table style |
| `ul { }` | Bullet list (can be nested) |
| `ol { }` | Numbered list (can be nested) |
| `li(text)` | List item with text |
| `li { }` | List item with block content |
| `table { }` | Table |
| `li(text) { }` | List item with text and nested content |
| `table(style:) { }` | Table with optional style |
| `tr { }` | Table row |
| `td(text)` | Table cell with text |
| `td { }` | Table cell with block content |

View File

@@ -18,6 +18,25 @@ Notare::Document.create(OUTPUT_FILE) do |doc|
doc.define_style :deleted_text, strike: true, color: "999999"
doc.define_style :important, highlight: "yellow", bold: true
# ============================================================================
# Custom Table Styles
# ============================================================================
doc.define_table_style :fancy_table,
borders: { style: "double", color: "0066CC", size: 6 },
shading: "E6F2FF",
cell_margins: 80,
align: :center
doc.define_table_style :minimal_table,
borders: {
top: { style: "single", color: "CCCCCC", size: 4 },
bottom: { style: "single", color: "CCCCCC", size: 4 },
left: { style: "none" },
right: { style: "none" },
insideH: { style: "dotted", color: "DDDDDD", size: 2 },
insideV: { style: "none" }
}
# ============================================================================
# Title and Introduction
# ============================================================================
@@ -151,6 +170,37 @@ Notare::Document.create(OUTPUT_FILE) do |doc|
doc.li "Step three"
end
doc.h3 "Nested Lists"
doc.ol do
doc.li "Main topic one"
doc.li "Main topic two" do
doc.ul do
doc.li "Supporting point A"
doc.li "Supporting point B" do
doc.ul do
doc.li "Detail 1"
doc.li "Detail 2"
end
end
doc.li "Supporting point C"
end
end
doc.li "Main topic three"
end
doc.p "Mixed nested lists with formatting:"
doc.ul do
doc.li do
doc.b { doc.text "Bold parent item" }
end
doc.li "Item with nested numbered list" do
doc.ol do
doc.li "First sub-step"
doc.li "Second sub-step"
end
end
end
# ============================================================================
# 8. Hyperlinks
# ============================================================================
@@ -187,6 +237,8 @@ Notare::Document.create(OUTPUT_FILE) do |doc|
# 9. Tables
# ============================================================================
doc.h2 "9. Tables"
doc.h3 "Default Table"
doc.table do
doc.tr do
doc.td { doc.b { doc.text "Feature" } }
@@ -238,6 +290,57 @@ Notare::Document.create(OUTPUT_FILE) do |doc|
doc.td { doc.text "Complete", style: :success }
doc.td "PNG and JPEG"
end
doc.tr do
doc.td "Nested Lists"
doc.td { doc.text "Complete", style: :success }
doc.td "Multi-level with mixed types"
end
end
doc.h3 "Styled Tables"
doc.p "Fancy table with double borders and shading:"
doc.table(style: :fancy_table) do
doc.tr do
doc.td { doc.b { doc.text "Product" } }
doc.td { doc.b { doc.text "Price" } }
doc.td { doc.b { doc.text "Quantity" } }
end
doc.tr do
doc.td "Widget A"
doc.td "$10.00"
doc.td "100"
end
doc.tr do
doc.td "Widget B"
doc.td "$15.00"
doc.td "50"
end
end
doc.p "Minimal table with horizontal lines only:"
doc.table(style: :minimal_table) do
doc.tr do
doc.td { doc.b { doc.text "Name" } }
doc.td { doc.b { doc.text "Role" } }
end
doc.tr do
doc.td "Alice"
doc.td "Developer"
end
doc.tr do
doc.td "Bob"
doc.td "Designer"
end
end
doc.p "Borderless table (built-in style):"
doc.table(style: :borderless) do
doc.tr do
doc.td "No"
doc.td "borders"
doc.td "here"
end
end
# ============================================================================

View File

@@ -16,6 +16,7 @@ require_relative "notare/nodes/table_row"
require_relative "notare/nodes/table_cell"
require_relative "notare/image_dimensions"
require_relative "notare/style"
require_relative "notare/table_style"
require_relative "notare/xml/content_types"
require_relative "notare/xml/relationships"
require_relative "notare/xml/document_xml"

View File

@@ -93,17 +93,15 @@ module Notare
end
def li(text = nil, &block)
item = Nodes::ListItem.new([], list_type: @current_list.type, num_id: @current_list.num_id)
if block
with_target(item, &block)
elsif text
item.add_run(Nodes::Run.new(text, **current_formatting))
end
current_type = @list_type_stack.last
item = Nodes::ListItem.new([], list_type: current_type, num_id: @current_list.num_id, level: @list_level)
item.add_run(Nodes::Run.new(text, **current_formatting)) if text
with_target(item, &block) if block
@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
@@ -134,15 +132,31 @@ module Notare
def list(type, &block)
@num_id_counter ||= 0
@num_id_counter += 1
mark_has_lists!
@list_level ||= 0
@list_type_stack ||= []
list_node = Nodes::List.new(type: type, num_id: @num_id_counter)
previous_list = @current_list
@current_list = list_node
block.call
@current_list = previous_list
@nodes << list_node
nested = !previous_list.nil?
if nested
# Nested list: reuse parent list, push new type, increment level
@list_level += 1
@list_type_stack.push(type)
block.call
@list_type_stack.pop
@list_level -= 1
else
# Top-level list: new List node
@num_id_counter += 1
mark_has_lists!
list_node = Nodes::List.new(type: type, num_id: @num_id_counter)
@list_type_stack.push(type)
@current_list = list_node
block.call
@current_list = previous_list
@list_type_stack.pop
@nodes << list_node
end
end
def with_format(format, &block)
@@ -184,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

View File

@@ -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

View File

@@ -3,13 +3,14 @@
module Notare
module Nodes
class ListItem < Base
attr_reader :runs, :list_type, :num_id
attr_reader :runs, :list_type, :num_id, :level
def initialize(runs = [], list_type:, num_id:)
def initialize(runs = [], list_type:, num_id:, level: 0)
super()
@runs = runs
@list_type = list_type
@num_id = num_id
@level = level
end
def add_run(run)

View File

@@ -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)

View File

@@ -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
View 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

View File

@@ -1,5 +1,5 @@
# frozen_string_literal: true
module Notare
VERSION = "0.0.2"
VERSION = "0.0.3"
end

View File

@@ -69,7 +69,7 @@ module Notare
xml["w"].p do
xml["w"].pPr do
xml["w"].numPr do
xml["w"].ilvl("w:val" => "0")
xml["w"].ilvl("w:val" => item.level.to_s)
xml["w"].numId("w:val" => item.num_id.to_s)
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

View File

@@ -4,6 +4,8 @@ module Notare
module Xml
class Numbering
NAMESPACE = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
BULLET_CHARS = ["", "", ""].freeze
NUMBER_FORMATS = %w[decimal lowerLetter lowerRoman].freeze
def initialize(lists)
@lists = lists
@@ -28,13 +30,16 @@ module Notare
def render_abstract_num(xml, list)
xml["w"].abstractNum("w:abstractNumId" => list.num_id.to_s) do
xml["w"].lvl("w:ilvl" => "0") do
xml["w"].start("w:val" => "1")
xml["w"].numFmt("w:val" => num_format(list.type))
xml["w"].lvlText("w:val" => lvl_text(list.type))
xml["w"].lvlJc("w:val" => "left")
xml["w"].pPr do
xml["w"].ind("w:left" => "720", "w:hanging" => "360")
9.times do |level|
xml["w"].lvl("w:ilvl" => level.to_s) do
xml["w"].start("w:val" => "1")
xml["w"].numFmt("w:val" => num_format_for_level(list.type, level))
xml["w"].lvlText("w:val" => lvl_text_for_level(list.type, level))
xml["w"].lvlJc("w:val" => "left")
xml["w"].pPr do
left = 720 * (level + 1)
xml["w"].ind("w:left" => left.to_s, "w:hanging" => "360")
end
end
end
end
@@ -46,12 +51,20 @@ module Notare
end
end
def num_format(type)
type == :bullet ? "bullet" : "decimal"
def num_format_for_level(type, level)
if type == :bullet
"bullet"
else
NUMBER_FORMATS[level % NUMBER_FORMATS.length]
end
end
def lvl_text(type)
type == :bullet ? "" : "%1."
def lvl_text_for_level(type, level)
if type == :bullet
BULLET_CHARS[level % BULLET_CHARS.length]
else
"%#{level + 1}."
end
end
end
end

View File

@@ -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

View File

@@ -144,4 +144,98 @@ class ListTest < Minitest::Test
refute xml_files.key?("word/numbering.xml"), "numbering.xml should not exist without lists"
end
#
# Nested List Tests
#
def test_nested_bullet_list
xml = create_doc_and_read_xml do |doc|
doc.ul do
doc.li "Parent item"
doc.li "Item with nested list" do
doc.ul do
doc.li "Nested item 1"
doc.li "Nested item 2"
end
end
doc.li "Another parent item"
end
end
assert_includes xml, "Parent item"
assert_includes xml, "Nested item 1"
assert_includes xml, "Nested item 2"
assert_includes xml, "Another parent item"
# Check for level 0 and level 1
assert_includes xml, 'w:ilvl w:val="0"'
assert_includes xml, 'w:ilvl w:val="1"'
end
def test_nested_numbered_list
xml = create_doc_and_read_xml do |doc|
doc.ol do
doc.li "First"
doc.li "Second with nested" do
doc.ol do
doc.li "Nested 1"
doc.li "Nested 2"
end
end
doc.li "Third"
end
end
assert_includes xml, "First"
assert_includes xml, "Nested 1"
assert_includes xml, "Third"
assert_includes xml, 'w:ilvl w:val="0"'
assert_includes xml, 'w:ilvl w:val="1"'
end
def test_mixed_nested_list
xml_files = create_doc_and_read_all_xml do |doc|
doc.ol do
doc.li "Numbered item 1"
doc.li "Numbered item 2" do
doc.ul do
doc.li "Bullet inside numbered"
end
end
doc.li "Numbered item 3"
end
end
doc_xml = xml_files["word/document.xml"]
assert_includes doc_xml, "Numbered item 1"
assert_includes doc_xml, "Bullet inside numbered"
assert_includes doc_xml, "Numbered item 3"
assert_includes doc_xml, 'w:ilvl w:val="0"'
assert_includes doc_xml, 'w:ilvl w:val="1"'
end
def test_deeply_nested_list
xml = create_doc_and_read_xml do |doc|
doc.ul do
doc.li "Level 0"
doc.li "Has nested" do
doc.ul do
doc.li "Level 1"
doc.li "Has deeper nested" do
doc.ul do
doc.li "Level 2"
end
end
end
end
end
end
assert_includes xml, "Level 0"
assert_includes xml, "Level 1"
assert_includes xml, "Level 2"
assert_includes xml, 'w:ilvl w:val="0"'
assert_includes xml, 'w:ilvl w:val="1"'
assert_includes xml, 'w:ilvl w:val="2"'
end
end

224
test/table_style_test.rb Normal file
View File

@@ -0,0 +1,224 @@
# frozen_string_literal: true
require "test_helper"
class TableStyleTest < Minitest::Test
include NotareTestHelpers
# --- TableStyle class tests ---
def test_table_style_id_generation
style = Notare::TableStyle.new(:my_table_style)
assert_equal "MyTableStyle", style.style_id
end
def test_table_style_display_name
style = Notare::TableStyle.new(:my_table_style)
assert_equal "My Table Style", style.display_name
end
def test_invalid_border_style_raises_error
assert_raises(ArgumentError) do
Notare::TableStyle.new(:bad, borders: { style: "invalid" })
end
end
def test_invalid_color_raises_error
assert_raises(ArgumentError) do
Notare::TableStyle.new(:bad, shading: "invalid")
end
end
def test_color_normalizes_hash_prefix
style = Notare::TableStyle.new(:test, shading: "#ff0000")
assert_equal "FF0000", style.shading
end
def test_invalid_alignment_raises_error
assert_raises(ArgumentError) do
Notare::TableStyle.new(:bad, align: :invalid)
end
end
# --- Document registration tests ---
def test_define_table_style
xml_files = create_doc_and_read_all_xml do |doc|
doc.define_table_style :custom, borders: { style: "double", color: "FF0000", size: 8 }
doc.table(style: :custom) do
doc.tr { doc.td "Test" }
end
end
styles_xml = xml_files["word/styles.xml"]
assert_includes styles_xml, 'w:styleId="Custom"'
assert_includes styles_xml, 'w:type="table"'
assert_includes styles_xml, 'w:val="double"'
assert_includes styles_xml, 'w:color="FF0000"'
end
def test_unknown_table_style_raises_error
assert_raises(ArgumentError) do
Tempfile.create(["test", ".docx"]) do |file|
Notare::Document.create(file.path) do |doc|
doc.table(style: :nonexistent) { doc.tr { doc.td "Test" } }
end
end
end
end
# --- Style application tests ---
def test_table_with_style_reference
xml = create_doc_and_read_xml do |doc|
doc.define_table_style :bordered, borders: { style: "single", color: "000000", size: 4 }
doc.table(style: :bordered) do
doc.tr { doc.td "Cell" }
end
end
assert_includes xml, '<w:tblStyle w:val="Bordered"'
end
def test_table_without_style_uses_default_borders
xml = create_doc_and_read_xml do |doc|
doc.table do
doc.tr { doc.td "Cell" }
end
end
assert_includes xml, "<w:tblBorders>"
refute_includes xml, "<w:tblStyle"
end
def test_borderless_table_style
xml_files = create_doc_and_read_all_xml do |doc|
doc.define_table_style :no_borders, borders: :none
doc.table(style: :no_borders) do
doc.tr { doc.td "Cell" }
end
end
styles_xml = xml_files["word/styles.xml"]
assert_includes styles_xml, 'w:val="nil"'
end
def test_table_style_with_shading
xml_files = create_doc_and_read_all_xml do |doc|
doc.define_table_style :shaded, shading: "EEEEEE"
doc.table(style: :shaded) do
doc.tr { doc.td "Cell" }
end
end
styles_xml = xml_files["word/styles.xml"]
assert_includes styles_xml, 'w:fill="EEEEEE"'
end
def test_table_style_with_cell_margins
xml_files = create_doc_and_read_all_xml do |doc|
doc.define_table_style :padded, cell_margins: 100
doc.table(style: :padded) do
doc.tr { doc.td "Cell" }
end
end
styles_xml = xml_files["word/styles.xml"]
assert_includes styles_xml, "<w:tblCellMar>"
assert_includes styles_xml, 'w:w="100"'
end
def test_table_style_with_alignment
xml_files = create_doc_and_read_all_xml do |doc|
doc.define_table_style :centered, align: :center
doc.table(style: :centered) do
doc.tr { doc.td "Cell" }
end
end
styles_xml = xml_files["word/styles.xml"]
assert_includes styles_xml, '<w:jc w:val="center"'
end
def test_combined_table_style_properties
xml_files = create_doc_and_read_all_xml do |doc|
doc.define_table_style :fancy,
borders: { style: "double", color: "0000FF", size: 8 },
shading: "F0F0F0",
cell_margins: { top: 50, bottom: 50, left: 100, right: 100 },
align: :center
doc.table(style: :fancy) do
doc.tr { doc.td "Fancy" }
end
end
styles_xml = xml_files["word/styles.xml"]
assert_includes styles_xml, 'w:styleId="Fancy"'
assert_includes styles_xml, 'w:val="double"'
assert_includes styles_xml, 'w:fill="F0F0F0"'
assert_includes styles_xml, '<w:jc w:val="center"'
assert_includes styles_xml, "<w:tblCellMar>"
end
def test_per_edge_borders
xml_files = create_doc_and_read_all_xml do |doc|
doc.define_table_style :mixed_borders,
borders: {
top: { style: "double", color: "FF0000", size: 8 },
bottom: { style: "single", color: "000000", size: 4 },
left: { style: "none" },
right: { style: "none" },
insideH: { style: "dotted", color: "CCCCCC", size: 2 },
insideV: { style: "dotted", color: "CCCCCC", size: 2 }
}
doc.table(style: :mixed_borders) do
doc.tr { doc.td "Cell" }
end
end
styles_xml = xml_files["word/styles.xml"]
assert_includes styles_xml, '<w:top w:val="double"'
assert_includes styles_xml, '<w:bottom w:val="single"'
assert_includes styles_xml, '<w:left w:val="nil"'
end
# --- Built-in styles tests ---
def test_built_in_grid_style_exists
xml_files = create_doc_and_read_all_xml do |doc|
doc.table(style: :grid) do
doc.tr { doc.td "Test" }
end
end
styles_xml = xml_files["word/styles.xml"]
assert_includes styles_xml, 'w:styleId="Grid"'
end
def test_built_in_borderless_style_exists
xml_files = create_doc_and_read_all_xml do |doc|
doc.table(style: :borderless) do
doc.tr { doc.td "Test" }
end
end
styles_xml = xml_files["word/styles.xml"]
assert_includes styles_xml, 'w:styleId="Borderless"'
end
def test_cell_margins_as_hash
xml_files = create_doc_and_read_all_xml do |doc|
doc.define_table_style :asymmetric_padding,
cell_margins: { top: 100, bottom: 200, left: 150, right: 150 }
doc.table(style: :asymmetric_padding) do
doc.tr { doc.td "Cell" }
end
end
styles_xml = xml_files["word/styles.xml"]
assert_includes styles_xml, "<w:tblCellMar>"
assert_match(/<w:top[^>]*w:w="100"/, styles_xml)
assert_match(/<w:bottom[^>]*w:w="200"/, styles_xml)
end
end