# frozen_string_literal: true module Notare module Builder def p(text = nil, style: nil, &block) para = Nodes::Paragraph.new(style: resolve_style(style)) if block with_target(para, &block) elsif text para.add_run(Nodes::Run.new(text, **current_formatting)) end @nodes << para end def text(value, style: nil) formatting = current_formatting.merge(style: resolve_style(style)) @current_target.add_run(Nodes::Run.new(value, **formatting)) end # Heading shortcuts def h1(text = nil, &block) p(text, style: :heading1, &block) end def h2(text = nil, &block) p(text, style: :heading2, &block) end def h3(text = nil, &block) p(text, style: :heading3, &block) end def h4(text = nil, &block) p(text, style: :heading4, &block) end def h5(text = nil, &block) p(text, style: :heading5, &block) end def h6(text = nil, &block) p(text, style: :heading6, &block) end def image(path, width: nil, height: nil) validate_image_path!(path) img = register_image(path, width: width, height: height) @current_target.add_run(img) end def b(&block) with_format(:bold, &block) end def i(&block) with_format(:italic, &block) end def u(&block) 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 def ol(&block) list(:number, &block) end def li(text = nil, &block) 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(style: nil, &block) tbl = Nodes::Table.new(style: resolve_table_style(style)) previous_table = @current_table @current_table = tbl block.call @current_table = previous_table @nodes << tbl end def tr(&block) row = Nodes::TableRow.new previous_row = @current_row @current_row = row block.call @current_row = previous_row @current_table.add_row(row) end def td(text = nil, &block) cell = Nodes::TableCell.new if block with_target(cell, &block) elsif text cell.add_run(Nodes::Run.new(text, **current_formatting)) end @current_row.add_cell(cell) end private def list(type, &block) @num_id_counter ||= 0 @list_level ||= 0 @list_type_stack ||= [] previous_list = @current_list 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) @format_stack ||= [] @format_stack.push(format) block.call @format_stack.pop end def with_target(target, &block) previous_target = @current_target @current_target = target block.call @current_target = previous_target end def current_formatting @format_stack ||= [] { bold: @format_stack.include?(:bold), italic: @format_stack.include?(:italic), underline: @format_stack.include?(:underline), strike: @format_stack.include?(:strike) } end def validate_image_path!(path) raise ArgumentError, "Image file not found: #{path}" unless File.exist?(path) ext = File.extname(path).downcase return if %w[.png .jpg .jpeg].include?(ext) raise ArgumentError, "Unsupported image format: #{ext}. Use PNG or JPEG." end def resolve_style(style_or_name) return nil if style_or_name.nil? return style_or_name if style_or_name.is_a?(Style) 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