From 597bc91c40c4eb29268da93d7375cd723ba7507f Mon Sep 17 00:00:00 2001 From: mathias234 Date: Tue, 2 Dec 2025 14:43:53 +0100 Subject: [PATCH] Implement many more nodes Adds these new styling and formatting nodes * strike * highlight * linebreaks * pagebreaks * Hyperlinks --- .claude/settings.local.json | 9 ++ README.md | 103 ++++++++++++++++++++++ examples/full_demo.rb | 146 ++++++++++++++++++++++++++++++-- lib/notare.rb | 2 + lib/notare/builder.rb | 28 +++++- lib/notare/document.rb | 29 ++++++- lib/notare/nodes/break.rb | 18 ++++ lib/notare/nodes/hyperlink.rb | 20 +++++ lib/notare/nodes/run.rb | 8 +- lib/notare/package.rb | 10 ++- lib/notare/style.rb | 25 ++++-- lib/notare/xml/document_xml.rb | 35 +++++++- lib/notare/xml/relationships.rb | 14 ++- lib/notare/xml/styles_xml.rb | 2 + test/formatting_test.rb | 45 ++++++++++ test/hyperlink_test.rb | 127 +++++++++++++++++++++++++++ test/line_break_test.rb | 85 +++++++++++++++++++ test/page_break_test.rb | 54 ++++++++++++ test/style_test.rb | 52 ++++++++++++ 19 files changed, 788 insertions(+), 24 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 lib/notare/nodes/break.rb create mode 100644 lib/notare/nodes/hyperlink.rb create mode 100644 test/hyperlink_test.rb create mode 100644 test/line_break_test.rb create mode 100644 test/page_break_test.rb 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, '