Compare commits
2 Commits
feature/ad
...
feature/ne
| Author | SHA1 | Date | |
|---|---|---|---|
| 75b3a163c7 | |||
| 6a54f9f8da |
39
README.md
39
README.md
@@ -195,6 +195,40 @@ Notare::Document.create("output.docx") do |doc|
|
|||||||
end
|
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
|
### Tables
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
@@ -400,10 +434,11 @@ end
|
|||||||
| `link(url, text)` | Hyperlink with custom text |
|
| `link(url, text)` | Hyperlink with custom text |
|
||||||
| `link(url) { }` | Hyperlink with block content |
|
| `link(url) { }` | Hyperlink with block content |
|
||||||
| `define_style(name, **props)` | Define a custom style |
|
| `define_style(name, **props)` | Define a custom style |
|
||||||
| `ul { }` | Bullet list |
|
| `ul { }` | Bullet list (can be nested) |
|
||||||
| `ol { }` | Numbered list |
|
| `ol { }` | Numbered list (can be nested) |
|
||||||
| `li(text)` | List item with text |
|
| `li(text)` | List item with text |
|
||||||
| `li { }` | List item with block content |
|
| `li { }` | List item with block content |
|
||||||
|
| `li(text) { }` | List item with text and nested content |
|
||||||
| `table { }` | Table |
|
| `table { }` | Table |
|
||||||
| `tr { }` | Table row |
|
| `tr { }` | Table row |
|
||||||
| `td(text)` | Table cell with text |
|
| `td(text)` | Table cell with text |
|
||||||
|
|||||||
@@ -151,6 +151,37 @@ Notare::Document.create(OUTPUT_FILE) do |doc|
|
|||||||
doc.li "Step three"
|
doc.li "Step three"
|
||||||
end
|
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
|
# 8. Hyperlinks
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -238,6 +269,11 @@ Notare::Document.create(OUTPUT_FILE) do |doc|
|
|||||||
doc.td { doc.text "Complete", style: :success }
|
doc.td { doc.text "Complete", style: :success }
|
||||||
doc.td "PNG and JPEG"
|
doc.td "PNG and JPEG"
|
||||||
end
|
end
|
||||||
|
doc.tr do
|
||||||
|
doc.td "Nested Lists"
|
||||||
|
doc.td { doc.text "Complete", style: :success }
|
||||||
|
doc.td "Multi-level with mixed types"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
@@ -93,12 +93,10 @@ module Notare
|
|||||||
end
|
end
|
||||||
|
|
||||||
def li(text = nil, &block)
|
def li(text = nil, &block)
|
||||||
item = Nodes::ListItem.new([], list_type: @current_list.type, num_id: @current_list.num_id)
|
current_type = @list_type_stack.last
|
||||||
if block
|
item = Nodes::ListItem.new([], list_type: current_type, num_id: @current_list.num_id, level: @list_level)
|
||||||
with_target(item, &block)
|
item.add_run(Nodes::Run.new(text, **current_formatting)) if text
|
||||||
elsif text
|
with_target(item, &block) if block
|
||||||
item.add_run(Nodes::Run.new(text, **current_formatting))
|
|
||||||
end
|
|
||||||
@current_list.add_item(item)
|
@current_list.add_item(item)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -134,15 +132,31 @@ module Notare
|
|||||||
|
|
||||||
def list(type, &block)
|
def list(type, &block)
|
||||||
@num_id_counter ||= 0
|
@num_id_counter ||= 0
|
||||||
@num_id_counter += 1
|
@list_level ||= 0
|
||||||
mark_has_lists!
|
@list_type_stack ||= []
|
||||||
|
|
||||||
list_node = Nodes::List.new(type: type, num_id: @num_id_counter)
|
|
||||||
previous_list = @current_list
|
previous_list = @current_list
|
||||||
@current_list = list_node
|
nested = !previous_list.nil?
|
||||||
block.call
|
|
||||||
@current_list = previous_list
|
if nested
|
||||||
@nodes << list_node
|
# 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
|
end
|
||||||
|
|
||||||
def with_format(format, &block)
|
def with_format(format, &block)
|
||||||
|
|||||||
@@ -3,13 +3,14 @@
|
|||||||
module Notare
|
module Notare
|
||||||
module Nodes
|
module Nodes
|
||||||
class ListItem < Base
|
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()
|
super()
|
||||||
@runs = runs
|
@runs = runs
|
||||||
@list_type = list_type
|
@list_type = list_type
|
||||||
@num_id = num_id
|
@num_id = num_id
|
||||||
|
@level = level
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_run(run)
|
def add_run(run)
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ module Notare
|
|||||||
xml["w"].p do
|
xml["w"].p do
|
||||||
xml["w"].pPr do
|
xml["w"].pPr do
|
||||||
xml["w"].numPr 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)
|
xml["w"].numId("w:val" => item.num_id.to_s)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ module Notare
|
|||||||
module Xml
|
module Xml
|
||||||
class Numbering
|
class Numbering
|
||||||
NAMESPACE = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
NAMESPACE = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
||||||
|
BULLET_CHARS = ["•", "○", "■"].freeze
|
||||||
|
NUMBER_FORMATS = %w[decimal lowerLetter lowerRoman].freeze
|
||||||
|
|
||||||
def initialize(lists)
|
def initialize(lists)
|
||||||
@lists = lists
|
@lists = lists
|
||||||
@@ -28,13 +30,16 @@ module Notare
|
|||||||
|
|
||||||
def render_abstract_num(xml, list)
|
def render_abstract_num(xml, list)
|
||||||
xml["w"].abstractNum("w:abstractNumId" => list.num_id.to_s) do
|
xml["w"].abstractNum("w:abstractNumId" => list.num_id.to_s) do
|
||||||
xml["w"].lvl("w:ilvl" => "0") do
|
9.times do |level|
|
||||||
xml["w"].start("w:val" => "1")
|
xml["w"].lvl("w:ilvl" => level.to_s) do
|
||||||
xml["w"].numFmt("w:val" => num_format(list.type))
|
xml["w"].start("w:val" => "1")
|
||||||
xml["w"].lvlText("w:val" => lvl_text(list.type))
|
xml["w"].numFmt("w:val" => num_format_for_level(list.type, level))
|
||||||
xml["w"].lvlJc("w:val" => "left")
|
xml["w"].lvlText("w:val" => lvl_text_for_level(list.type, level))
|
||||||
xml["w"].pPr do
|
xml["w"].lvlJc("w:val" => "left")
|
||||||
xml["w"].ind("w:left" => "720", "w:hanging" => "360")
|
xml["w"].pPr do
|
||||||
|
left = 720 * (level + 1)
|
||||||
|
xml["w"].ind("w:left" => left.to_s, "w:hanging" => "360")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -46,12 +51,20 @@ module Notare
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def num_format(type)
|
def num_format_for_level(type, level)
|
||||||
type == :bullet ? "bullet" : "decimal"
|
if type == :bullet
|
||||||
|
"bullet"
|
||||||
|
else
|
||||||
|
NUMBER_FORMATS[level % NUMBER_FORMATS.length]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def lvl_text(type)
|
def lvl_text_for_level(type, level)
|
||||||
type == :bullet ? "•" : "%1."
|
if type == :bullet
|
||||||
|
BULLET_CHARS[level % BULLET_CHARS.length]
|
||||||
|
else
|
||||||
|
"%#{level + 1}."
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -144,4 +144,98 @@ class ListTest < Minitest::Test
|
|||||||
|
|
||||||
refute xml_files.key?("word/numbering.xml"), "numbering.xml should not exist without lists"
|
refute xml_files.key?("word/numbering.xml"), "numbering.xml should not exist without lists"
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user