diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 0000000..aad0fcb
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,9 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(bundle exec rake test:*)"
+ ],
+ "deny": [],
+ "ask": []
+ }
+}
diff --git a/README.md b/README.md
index 99eaff5..20d7ca5 100644
--- a/README.md
+++ b/README.md
@@ -58,6 +58,8 @@ Notare::Document.create("output.docx") do |doc|
doc.i { doc.text "italic" }
doc.text " and "
doc.u { doc.text "underlined" }
+ doc.text " and "
+ doc.s { doc.text "strikethrough" }
end
# Nested formatting (bold + italic)
@@ -66,6 +68,13 @@ Notare::Document.create("output.docx") do |doc|
doc.i { doc.text "bold and italic" }
end
end
+
+ # Show edits (strikethrough old, bold new)
+ doc.p do
+ doc.s { doc.text "old text" }
+ doc.text " "
+ doc.b { doc.text "new text" }
+ end
end
```
@@ -146,10 +155,14 @@ end
- `bold: true/false`
- `italic: true/false`
- `underline: true/false`
+- `strike: true/false` - strikethrough
+- `highlight: "yellow"` - text highlight (see colors below)
- `color: "FF0000"` (hex RGB)
- `size: 14` (points)
- `font: "Arial"` (font family)
+**Highlight colors:** `black`, `blue`, `cyan`, `darkBlue`, `darkCyan`, `darkGray`, `darkGreen`, `darkMagenta`, `darkRed`, `darkYellow`, `green`, `lightGray`, `magenta`, `red`, `white`, `yellow`
+
**Paragraph properties:**
- `align: :left / :center / :right / :justify`
- `indent: 720` (twips, 1 inch = 1440 twips)
@@ -246,6 +259,91 @@ Notare::Document.create("output.docx") do |doc|
end
```
+### Line Breaks
+
+Use `br` for soft line breaks within a paragraph (text continues in the same paragraph but on a new line):
+
+```ruby
+Notare::Document.create("output.docx") do |doc|
+ doc.p do
+ doc.text "Line one"
+ doc.br
+ doc.text "Line two (same paragraph)"
+ doc.br
+ doc.text "Line three"
+ end
+
+ # Useful for addresses
+ doc.p do
+ doc.b { doc.text "Address:" }
+ doc.br
+ doc.text "123 Main Street"
+ doc.br
+ doc.text "Anytown, ST 12345"
+ end
+end
+```
+
+### Page Breaks
+
+Use `page_break` to force content to start on a new page:
+
+```ruby
+Notare::Document.create("output.docx") do |doc|
+ doc.h1 "Chapter 1"
+ doc.p "Content of chapter 1..."
+
+ doc.page_break
+
+ doc.h1 "Chapter 2"
+ doc.p "This starts on a new page."
+end
+```
+
+### Hyperlinks
+
+Add clickable links with `link`:
+
+```ruby
+Notare::Document.create("output.docx") do |doc|
+ # Link with custom text
+ doc.p do
+ doc.text "Visit "
+ doc.link "https://example.com", "our website"
+ doc.text " for more info."
+ end
+
+ # Link showing the URL as text
+ doc.p do
+ doc.text "URL: "
+ doc.link "https://example.com"
+ end
+
+ # Link with formatted content
+ doc.p do
+ doc.link "https://github.com" do
+ doc.b { doc.text "GitHub" }
+ end
+ end
+
+ # Links in lists
+ doc.ul do
+ doc.li do
+ doc.link "https://ruby-lang.org", "Ruby"
+ end
+ doc.li do
+ doc.link "https://rubyonrails.org", "Rails"
+ end
+ end
+
+ # Email links
+ doc.p do
+ doc.text "Contact: "
+ doc.link "mailto:hello@example.com", "hello@example.com"
+ end
+end
+```
+
### Complete Example
```ruby
@@ -296,6 +394,11 @@ end
| `b { }` | Bold formatting |
| `i { }` | Italic formatting |
| `u { }` | Underline formatting |
+| `s { }` | Strikethrough formatting |
+| `br` | Line break (soft break within paragraph) |
+| `page_break` | Page break (force new page) |
+| `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 |
diff --git a/examples/full_demo.rb b/examples/full_demo.rb
index 1307d6c..4502a24 100644
--- a/examples/full_demo.rb
+++ b/examples/full_demo.rb
@@ -15,6 +15,8 @@ Notare::Document.create(OUTPUT_FILE) do |doc|
doc.define_style :highlight, bold: true, color: "FF6600"
doc.define_style :success, color: "228B22", italic: true
doc.define_style :centered_large, align: :center, size: 16, bold: true
+ doc.define_style :deleted_text, strike: true, color: "999999"
+ doc.define_style :important, highlight: "yellow", bold: true
# ============================================================================
# Title and Introduction
@@ -33,6 +35,8 @@ Notare::Document.create(OUTPUT_FILE) do |doc|
doc.i { doc.text "italic" }
doc.text ", "
doc.u { doc.text "underlined" }
+ doc.text ", "
+ doc.s { doc.text "strikethrough" }
doc.text ", and "
doc.b do
doc.i do
@@ -42,6 +46,13 @@ Notare::Document.create(OUTPUT_FILE) do |doc|
doc.text " formatting."
end
+ doc.p do
+ doc.text "Showing edits: "
+ doc.s { doc.text "old text" }
+ doc.text " "
+ doc.b { doc.text "new text" }
+ end
+
# ============================================================================
# 2. Headings
# ============================================================================
@@ -73,11 +84,54 @@ Notare::Document.create(OUTPUT_FILE) do |doc|
doc.text " in one paragraph."
end
doc.p "Centered and large text", style: :centered_large
+ doc.p "This was removed from the document", style: :deleted_text
+ doc.p "This is critically important!", style: :important
# ============================================================================
- # 5. Lists
+ # 5. Text Highlighting
# ============================================================================
- doc.h2 "5. Lists"
+ doc.h2 "5. Text Highlighting"
+ doc.p do
+ doc.text "You can highlight text in "
+ doc.text "yellow", style: :important
+ doc.text " or use styles with various highlight colors."
+ end
+ doc.define_style :highlight_cyan, highlight: "cyan"
+ doc.define_style :highlight_green, highlight: "green"
+ doc.define_style :highlight_magenta, highlight: "magenta"
+ doc.p do
+ doc.text "Multiple colors: "
+ doc.text "cyan", style: :highlight_cyan
+ doc.text " "
+ doc.text "green", style: :highlight_green
+ doc.text " "
+ doc.text "magenta", style: :highlight_magenta
+ end
+
+ # ============================================================================
+ # 6. Line Breaks
+ # ============================================================================
+ doc.h2 "6. Line Breaks"
+ doc.p do
+ doc.text "This is the first line."
+ doc.br
+ doc.text "This is the second line (soft break)."
+ doc.br
+ doc.text "This is the third line."
+ end
+
+ doc.p do
+ doc.b { doc.text "Address:" }
+ doc.br
+ doc.text "123 Main Street"
+ doc.br
+ doc.text "Anytown, ST 12345"
+ end
+
+ # ============================================================================
+ # 7. Lists
+ # ============================================================================
+ doc.h2 "7. Lists"
doc.h3 "Bullet List"
doc.ul do
@@ -98,9 +152,41 @@ Notare::Document.create(OUTPUT_FILE) do |doc|
end
# ============================================================================
- # 6. Tables
+ # 8. Hyperlinks
# ============================================================================
- doc.h2 "6. Tables"
+ doc.h2 "8. Hyperlinks"
+ doc.p do
+ doc.text "Visit "
+ doc.link "https://www.example.com", "Example.com"
+ doc.text " for more information."
+ end
+
+ doc.p do
+ doc.text "Check out "
+ doc.link "https://github.com" do
+ doc.b { doc.text "GitHub" }
+ end
+ doc.text " for code hosting."
+ end
+
+ doc.p do
+ doc.text "Or just paste the URL: "
+ doc.link "https://www.ruby-lang.org"
+ end
+
+ doc.ul do
+ doc.li do
+ doc.link "https://rubyonrails.org", "Ruby on Rails"
+ end
+ doc.li do
+ doc.link "https://rubygems.org", "RubyGems"
+ end
+ end
+
+ # ============================================================================
+ # 9. Tables
+ # ============================================================================
+ doc.h2 "9. Tables"
doc.table do
doc.tr do
doc.td { doc.b { doc.text "Feature" } }
@@ -115,7 +201,27 @@ Notare::Document.create(OUTPUT_FILE) do |doc|
doc.tr do
doc.td "Formatting"
doc.td { doc.text "Complete", style: :success }
- doc.td "Bold, italic, underline"
+ doc.td "Bold, italic, underline, strikethrough"
+ end
+ doc.tr do
+ doc.td "Highlighting"
+ doc.td { doc.text "Complete", style: :success }
+ doc.td "16 highlight colors"
+ end
+ doc.tr do
+ doc.td "Line Breaks"
+ doc.td { doc.text "Complete", style: :success }
+ doc.td "Soft breaks within paragraphs"
+ end
+ doc.tr do
+ doc.td "Page Breaks"
+ doc.td { doc.text "Complete", style: :success }
+ doc.td "Force new pages"
+ end
+ doc.tr do
+ doc.td "Hyperlinks"
+ doc.td { doc.text "Complete", style: :success }
+ doc.td "Clickable links"
end
doc.tr do
doc.td "Headings"
@@ -135,9 +241,9 @@ Notare::Document.create(OUTPUT_FILE) do |doc|
end
# ============================================================================
- # 7. Images
+ # 10. Images
# ============================================================================
- doc.h2 "7. Images"
+ doc.h2 "10. Images"
doc.p "Image with explicit dimensions:"
doc.p do
@@ -162,9 +268,19 @@ Notare::Document.create(OUTPUT_FILE) do |doc|
end
# ============================================================================
- # 8. Combined Features
+ # 11. Page Breaks
# ============================================================================
- doc.h2 "8. Combined Features"
+ doc.h2 "11. Page Breaks"
+ doc.p "The next element will force a new page."
+
+ doc.page_break
+
+ # ============================================================================
+ # 12. Combined Features (on new page)
+ # ============================================================================
+ doc.h2 "12. Combined Features"
+ doc.p "This section starts on a new page thanks to the page break above."
+
doc.p do
doc.text "This final paragraph combines "
doc.b { doc.text "multiple" }
@@ -172,9 +288,21 @@ Notare::Document.create(OUTPUT_FILE) do |doc|
doc.i { doc.text "formatting" }
doc.text " options with "
doc.text "custom styles", style: :highlight
+ doc.text ", "
+ doc.s { doc.text "strikethrough" }
+ doc.text ", "
+ doc.text "highlighting", style: :important
+ doc.text ", and "
+ doc.link "https://example.com", "hyperlinks"
doc.text " to demonstrate the full power of Notare."
end
+ doc.p do
+ doc.text "Contact us:"
+ doc.br
+ doc.link "mailto:hello@example.com", "hello@example.com"
+ end
+
doc.p "End of demo document.", style: :centered_large
end
diff --git a/lib/notare.rb b/lib/notare.rb
index 61335f8..264c67e 100644
--- a/lib/notare.rb
+++ b/lib/notare.rb
@@ -4,6 +4,8 @@ require "nokogiri"
require_relative "notare/version"
require_relative "notare/nodes/base"
+require_relative "notare/nodes/break"
+require_relative "notare/nodes/hyperlink"
require_relative "notare/nodes/run"
require_relative "notare/nodes/image"
require_relative "notare/nodes/paragraph"
diff --git a/lib/notare/builder.rb b/lib/notare/builder.rb
index 68b2b6a..9fee3c2 100644
--- a/lib/notare/builder.rb
+++ b/lib/notare/builder.rb
@@ -60,6 +60,30 @@ module Notare
with_format(:underline, &block)
end
+ def s(&block)
+ with_format(:strike, &block)
+ end
+
+ def br
+ @current_target.add_run(Nodes::Break.new(type: :line))
+ end
+
+ def page_break
+ @nodes << Nodes::Break.new(type: :page)
+ end
+
+ def link(url, text = nil, &block)
+ hyperlink = register_hyperlink(url)
+ if block
+ with_target(hyperlink, &block)
+ elsif text
+ hyperlink.add_run(Nodes::Run.new(text, underline: true, color: "0000FF"))
+ else
+ hyperlink.add_run(Nodes::Run.new(url, underline: true, color: "0000FF"))
+ end
+ @current_target.add_run(hyperlink)
+ end
+
def ul(&block)
list(:bullet, &block)
end
@@ -111,6 +135,7 @@ module Notare
def list(type, &block)
@num_id_counter ||= 0
@num_id_counter += 1
+ mark_has_lists!
list_node = Nodes::List.new(type: type, num_id: @num_id_counter)
previous_list = @current_list
@@ -139,7 +164,8 @@ module Notare
{
bold: @format_stack.include?(:bold),
italic: @format_stack.include?(:italic),
- underline: @format_stack.include?(:underline)
+ underline: @format_stack.include?(:underline),
+ strike: @format_stack.include?(:strike)
}
end
diff --git a/lib/notare/document.rb b/lib/notare/document.rb
index 3912f2e..7cac14a 100644
--- a/lib/notare/document.rb
+++ b/lib/notare/document.rb
@@ -4,7 +4,7 @@ module Notare
class Document
include Builder
- attr_reader :nodes, :styles
+ attr_reader :nodes, :styles, :hyperlinks
def self.create(path, &block)
doc = new
@@ -21,7 +21,9 @@ module Notare
@current_table = nil
@current_row = nil
@num_id_counter = 0
+ @has_lists = false
@images = {}
+ @hyperlinks = []
@styles = {}
register_built_in_styles
end
@@ -42,10 +44,25 @@ module Notare
@nodes.select { |n| n.is_a?(Nodes::List) }
end
+ def uses_lists?
+ @has_lists
+ end
+
+ def mark_has_lists!
+ @has_lists = true
+ end
+
def images
@images.values
end
+ def register_hyperlink(url)
+ rid = next_hyperlink_rid
+ hyperlink = Nodes::Hyperlink.new(url: url, rid: rid)
+ @hyperlinks << hyperlink
+ hyperlink
+ end
+
def register_image(path, width: nil, height: nil)
return @images[path] if @images[path]
@@ -61,11 +78,17 @@ module Notare
def next_image_rid
# rId1 = styles.xml (always present)
# rId2 = numbering.xml (if lists present)
- # rId3+ = images
- base = lists.any? ? 3 : 2
+ # rId3+ = images, then hyperlinks
+ base = @has_lists ? 3 : 2
"rId#{base + @images.size}"
end
+ def next_hyperlink_rid
+ # Hyperlinks come after images
+ base = @has_lists ? 3 : 2
+ "rId#{base + @images.size + @hyperlinks.size}"
+ end
+
def register_built_in_styles
# Headings (spacing_before ensures they're rendered as paragraph styles)
define_style :heading1, size: 24, bold: true, spacing_before: 240, spacing_after: 120
diff --git a/lib/notare/nodes/break.rb b/lib/notare/nodes/break.rb
new file mode 100644
index 0000000..7c9cbc1
--- /dev/null
+++ b/lib/notare/nodes/break.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Notare
+ module Nodes
+ class Break < Base
+ attr_reader :type
+
+ def initialize(type: :line)
+ super()
+ @type = type
+ end
+
+ def page?
+ type == :page
+ end
+ end
+ end
+end
diff --git a/lib/notare/nodes/hyperlink.rb b/lib/notare/nodes/hyperlink.rb
new file mode 100644
index 0000000..4461959
--- /dev/null
+++ b/lib/notare/nodes/hyperlink.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Notare
+ module Nodes
+ class Hyperlink < Base
+ attr_reader :url, :rid, :runs
+
+ def initialize(url:, rid:)
+ super()
+ @url = url
+ @rid = rid
+ @runs = []
+ end
+
+ def add_run(run)
+ @runs << run
+ end
+ end
+ end
+end
diff --git a/lib/notare/nodes/run.rb b/lib/notare/nodes/run.rb
index 67ad253..5cc9e6e 100644
--- a/lib/notare/nodes/run.rb
+++ b/lib/notare/nodes/run.rb
@@ -3,14 +3,18 @@
module Notare
module Nodes
class Run < Base
- attr_reader :text, :bold, :italic, :underline, :style
+ attr_reader :text, :bold, :italic, :underline, :strike, :highlight, :color, :style
- def initialize(text, bold: false, italic: false, underline: false, style: nil)
+ def initialize(text, bold: false, italic: false, underline: false,
+ strike: false, highlight: nil, color: nil, style: nil)
super()
@text = text
@bold = bold
@italic = italic
@underline = underline
+ @strike = strike
+ @highlight = highlight
+ @color = color
@style = style
end
end
diff --git a/lib/notare/package.rb b/lib/notare/package.rb
index 4538e9b..85509fd 100644
--- a/lib/notare/package.rb
+++ b/lib/notare/package.rb
@@ -29,13 +29,17 @@ module Notare
private
def lists?
- @document.lists.any?
+ @document.uses_lists?
end
def images
@document.images
end
+ def hyperlinks
+ @document.hyperlinks
+ end
+
def content_types_xml
Xml::ContentTypes.new(has_numbering: lists?, images: images, has_styles: true).to_xml
end
@@ -45,7 +49,9 @@ module Notare
end
def document_relationships_xml
- Xml::DocumentRelationships.new(has_numbering: lists?, images: images, has_styles: true).to_xml
+ Xml::DocumentRelationships.new(
+ has_numbering: lists?, images: images, hyperlinks: hyperlinks, has_styles: true
+ ).to_xml
end
def document_xml
diff --git a/lib/notare/style.rb b/lib/notare/style.rb
index 47edee7..3683890 100644
--- a/lib/notare/style.rb
+++ b/lib/notare/style.rb
@@ -2,18 +2,24 @@
module Notare
class Style
- attr_reader :name, :bold, :italic, :underline, :color, :size, :font,
+ attr_reader :name, :bold, :italic, :underline, :strike, :highlight, :color, :size, :font,
:align, :indent, :spacing_before, :spacing_after
ALIGNMENTS = %i[left center right justify].freeze
+ HIGHLIGHT_COLORS = %w[
+ black blue cyan darkBlue darkCyan darkGray darkGreen darkMagenta
+ darkRed darkYellow green lightGray magenta red white yellow
+ ].freeze
- def initialize(name, bold: nil, italic: nil, underline: nil, color: nil,
- size: nil, font: nil, align: nil, indent: nil,
- spacing_before: nil, spacing_after: nil)
+ def initialize(name, bold: nil, italic: nil, underline: nil, strike: nil,
+ highlight: nil, color: nil, size: nil, font: nil, align: nil,
+ indent: nil, spacing_before: nil, spacing_after: nil)
@name = name
@bold = bold
@italic = italic
@underline = underline
+ @strike = strike
+ @highlight = validate_highlight(highlight)
@color = normalize_color(color)
@size = size
@font = font
@@ -36,7 +42,7 @@ module Notare
end
def text_properties?
- !!(bold || italic || underline || color || size || font)
+ !!(bold || italic || underline || strike || highlight || color || size || font)
end
# Size in half-points for OOXML (14pt = 28 half-points)
@@ -61,5 +67,14 @@ module Notare
raise ArgumentError, "Invalid alignment: #{align}. Use #{ALIGNMENTS.join(", ")}"
end
+
+ def validate_highlight(highlight)
+ return nil if highlight.nil?
+
+ color = highlight.to_s
+ return color if HIGHLIGHT_COLORS.include?(color)
+
+ raise ArgumentError, "Invalid highlight color: #{highlight}. Use one of: #{HIGHLIGHT_COLORS.join(", ")}"
+ end
end
end
diff --git a/lib/notare/xml/document_xml.rb b/lib/notare/xml/document_xml.rb
index f259b50..ec808ed 100644
--- a/lib/notare/xml/document_xml.rb
+++ b/lib/notare/xml/document_xml.rb
@@ -37,6 +37,16 @@ module Notare
render_list(xml, node)
when Nodes::Table
render_table(xml, node)
+ when Nodes::Break
+ render_page_break(xml, node)
+ end
+ end
+
+ def render_page_break(xml, _node)
+ xml["w"].p do
+ xml["w"].r do
+ xml["w"].br("w:type" => "page")
+ end
end
end
@@ -71,19 +81,42 @@ module Notare
case run
when Nodes::Image
render_image(xml, run)
+ when Nodes::Break
+ render_break(xml, run)
+ when Nodes::Hyperlink
+ render_hyperlink(xml, run)
when Nodes::Run
render_text_run(xml, run)
end
end
+ def render_hyperlink(xml, hyperlink)
+ xml["w"].hyperlink("r:id" => hyperlink.rid) do
+ hyperlink.runs.each { |run| render_run(xml, run) }
+ end
+ end
+
+ def render_break(xml, break_node)
+ xml["w"].r do
+ if break_node.page?
+ xml["w"].br("w:type" => "page")
+ else
+ xml["w"].br
+ end
+ end
+ end
+
def render_text_run(xml, run)
xml["w"].r do
- if run.bold || run.italic || run.underline || run.style
+ if run.bold || run.italic || run.underline || run.strike || run.highlight || run.color || run.style
xml["w"].rPr do
xml["w"].rStyle("w:val" => run.style.style_id) if run.style
xml["w"].b if run.bold
xml["w"].i if run.italic
xml["w"].u("w:val" => "single") if run.underline
+ xml["w"].strike if run.strike
+ xml["w"].highlight("w:val" => run.highlight) if run.highlight
+ xml["w"].color("w:val" => run.color) if run.color
end
end
xml["w"].t(run.text, "xml:space" => "preserve")
diff --git a/lib/notare/xml/relationships.rb b/lib/notare/xml/relationships.rb
index bfa9d59..cb9077e 100644
--- a/lib/notare/xml/relationships.rb
+++ b/lib/notare/xml/relationships.rb
@@ -24,10 +24,12 @@ module Notare
STYLES_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles"
NUMBERING_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering"
IMAGE_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"
+ HYPERLINK_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"
- def initialize(has_numbering: false, images: [], has_styles: false)
+ def initialize(has_numbering: false, images: [], hyperlinks: [], has_styles: false)
@has_numbering = has_numbering
@images = images
+ @hyperlinks = hyperlinks
@has_styles = has_styles
end
@@ -60,6 +62,16 @@ module Notare
Target: "media/#{image.filename}"
)
end
+
+ # Hyperlinks come after images
+ @hyperlinks.each do |hyperlink|
+ xml.Relationship(
+ Id: hyperlink.rid,
+ Type: HYPERLINK_TYPE,
+ Target: hyperlink.url,
+ TargetMode: "External"
+ )
+ end
end
end
builder.to_xml
diff --git a/lib/notare/xml/styles_xml.rb b/lib/notare/xml/styles_xml.rb
index 48c4bf0..d6e6f08 100644
--- a/lib/notare/xml/styles_xml.rb
+++ b/lib/notare/xml/styles_xml.rb
@@ -59,6 +59,8 @@ module Notare
xml["w"].b if style.bold
xml["w"].i if style.italic
xml["w"].u("w:val" => "single") if style.underline
+ xml["w"].strike if style.strike
+ xml["w"].highlight("w:val" => style.highlight) if style.highlight
end
end
end
diff --git a/test/formatting_test.rb b/test/formatting_test.rb
index 6cd3a72..214faf1 100644
--- a/test/formatting_test.rb
+++ b/test/formatting_test.rb
@@ -133,4 +133,49 @@ class FormattingTest < Minitest::Test
assert_includes xml, "bold+italic "
assert_includes xml, "all three"
end
+
+ def test_strikethrough_text
+ xml = create_doc_and_read_xml do |doc|
+ doc.p do
+ doc.s { doc.text "strikethrough text" }
+ end
+ end
+
+ assert_includes xml, ""
+ assert_includes xml, "strikethrough text"
+ end
+
+ def test_strikethrough_with_other_formatting
+ xml = create_doc_and_read_xml do |doc|
+ doc.p do
+ doc.b do
+ doc.s { doc.text "bold and strikethrough" }
+ end
+ end
+ end
+
+ assert_includes xml, ""
+ assert_includes xml, ""
+ assert_includes xml, "bold and strikethrough"
+ end
+
+ def test_all_four_formatting_options
+ xml = create_doc_and_read_xml do |doc|
+ doc.p do
+ doc.b do
+ doc.i do
+ doc.u do
+ doc.s { doc.text "all four" }
+ end
+ end
+ end
+ end
+ end
+
+ assert_includes xml, ""
+ assert_includes xml, ""
+ assert_includes xml, ''
+ assert_includes xml, ""
+ assert_includes xml, "all four"
+ end
end
diff --git a/test/hyperlink_test.rb b/test/hyperlink_test.rb
new file mode 100644
index 0000000..74adeec
--- /dev/null
+++ b/test/hyperlink_test.rb
@@ -0,0 +1,127 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class HyperlinkTest < Minitest::Test
+ include NotareTestHelpers
+
+ def test_simple_hyperlink
+ xml = create_doc_and_read_xml do |doc|
+ doc.p do
+ doc.link "https://example.com", "Example"
+ end
+ end
+
+ assert_includes xml, ''
+ assert_includes xml, ''
+ end
+
+ def test_hyperlink_url_as_text
+ xml = create_doc_and_read_xml do |doc|
+ doc.p do
+ doc.link "https://example.com"
+ end
+ end
+
+ assert_includes xml, "https://example.com"
+ end
+
+ def test_hyperlink_with_block
+ xml = create_doc_and_read_xml do |doc|
+ doc.p do
+ doc.link "https://example.com" do
+ doc.b { doc.text "Bold Link" }
+ end
+ end
+ end
+
+ assert_includes xml, '"
+ assert_includes xml, "Bold Link"
+ end
+
+ def test_hyperlink_relationship
+ xml_files = create_doc_and_read_all_xml do |doc|
+ doc.p do
+ doc.link "https://example.com", "Example"
+ end
+ end
+
+ rels = xml_files["word/_rels/document.xml.rels"]
+ assert_includes rels, "https://example.com"
+ assert_includes rels, 'TargetMode="External"'
+ assert_includes rels, "relationships/hyperlink"
+ end
+
+ def test_multiple_hyperlinks
+ xml = create_doc_and_read_xml do |doc|
+ doc.p do
+ doc.link "https://one.com", "One"
+ doc.text " and "
+ doc.link "https://two.com", "Two"
+ end
+ end
+
+ assert_includes xml, '"
+ end
+
+ def test_multiple_line_breaks
+ xml = create_doc_and_read_xml do |doc|
+ doc.p do
+ doc.text "Line 1"
+ doc.br
+ doc.text "Line 2"
+ doc.br
+ doc.text "Line 3"
+ end
+ end
+
+ assert_equal 2, xml.scan("").count
+ end
+
+ def test_line_break_with_formatting
+ xml = create_doc_and_read_xml do |doc|
+ doc.p do
+ doc.b { doc.text "Bold first line" }
+ doc.br
+ doc.i { doc.text "Italic second line" }
+ end
+ end
+
+ assert_includes xml, ""
+ assert_includes xml, ""
+ assert_includes xml, ""
+ assert_includes xml, "Bold first line"
+ assert_includes xml, "Italic second line"
+ end
+
+ def test_line_break_in_list_item
+ xml = create_doc_and_read_xml do |doc|
+ doc.ul do
+ doc.li do
+ doc.text "First line"
+ doc.br
+ doc.text "Second line"
+ end
+ end
+ end
+
+ assert_includes xml, ""
+ assert_includes xml, "First line"
+ assert_includes xml, "Second line"
+ end
+
+ def test_line_break_in_table_cell
+ xml = create_doc_and_read_xml do |doc|
+ doc.table do
+ doc.tr do
+ doc.td do
+ doc.text "Cell line 1"
+ doc.br
+ doc.text "Cell line 2"
+ end
+ end
+ end
+ end
+
+ assert_includes xml, ""
+ assert_includes xml, "Cell line 1"
+ assert_includes xml, "Cell line 2"
+ end
+end
diff --git a/test/page_break_test.rb b/test/page_break_test.rb
new file mode 100644
index 0000000..cb0988d
--- /dev/null
+++ b/test/page_break_test.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require "test_helper"
+
+class PageBreakTest < Minitest::Test
+ include NotareTestHelpers
+
+ def test_page_break
+ xml = create_doc_and_read_xml do |doc|
+ doc.p "First page content"
+ doc.page_break
+ doc.p "Second page content"
+ end
+
+ assert_includes xml, "First page content"
+ assert_includes xml, "Second page content"
+ assert_includes xml, ''
+ end
+
+ def test_multiple_page_breaks
+ xml = create_doc_and_read_xml do |doc|
+ doc.p "Page 1"
+ doc.page_break
+ doc.p "Page 2"
+ doc.page_break
+ doc.p "Page 3"
+ end
+
+ assert_equal 2, xml.scan('').count
+ end
+
+ def test_page_break_between_different_elements
+ xml = create_doc_and_read_xml do |doc|
+ doc.h1 "Chapter 1"
+ doc.p "Some content"
+ doc.page_break
+ doc.h1 "Chapter 2"
+ doc.ul do
+ doc.li "Item 1"
+ end
+ end
+
+ assert_includes xml, "Chapter 1"
+ assert_includes xml, "Chapter 2"
+ assert_includes xml, ''
+ end
+
+ def test_page_break_renders_in_own_paragraph
+ xml = create_doc_and_read_xml(&:page_break)
+
+ # Page break should be wrapped in its own paragraph
+ assert_match(%r{\s*\s*\s*\s*}m, xml)
+ end
+end
diff --git a/test/style_test.rb b/test/style_test.rb
index 148a024..5b48bbd 100644
--- a/test/style_test.rb
+++ b/test/style_test.rb
@@ -168,4 +168,56 @@ class StyleTest < Minitest::Test
assert_includes styles_xml, 'w:ascii="Arial"'
assert_includes styles_xml, '"
+ end
+
+ def test_invalid_highlight_raises_error
+ assert_raises(ArgumentError) do
+ Notare::Style.new(:bad, highlight: "invalid_color")
+ end
+ end
+
+ def test_valid_highlight_colors
+ # Test a few valid highlight colors
+ %w[yellow red blue green cyan magenta].each do |color|
+ style = Notare::Style.new(:test, highlight: color)
+ assert_equal color, style.highlight
+ end
+ end
+
+ def test_highlight_in_text_run
+ xml_files = create_doc_and_read_all_xml do |doc|
+ doc.define_style :yellow_highlight, highlight: "yellow"
+ doc.p do
+ doc.text "Normal "
+ doc.text "highlighted", style: :yellow_highlight
+ end
+ end
+
+ # Highlight is in the style definition, not inline when using style reference
+ styles_xml = xml_files["word/styles.xml"]
+ assert_includes styles_xml, '