基于Prawn与Nokogiri处理嵌套ul/ol标签生成PDF的技术问题
Alright, let’s fix those nested list headaches with Prawn and Nokogiri— I’ve wrestled with this exact problem when building PDF reports that needed to mirror HTML list structures. Here’s how to tackle both your issues step by step:
1. Fixing Spacing Control for UL/OL Tags
The biggest spacing pain points usually come from inconsistent indentation across levels and unregulated gaps between list markers (like • or 1.) and item text. Instead of relying on ad-hoc string hacks, we’ll use a consistent indent formula and fixed spacing between markers and content:
- Indent Calculation: Use
2 spaces per nesting level(adjustable) to ensure uniform horizontal offset for each level. - Marker-Content Spacing: Hardcode a single space after markers (e.g.,
•instead of•) to avoid random gaps or cramped text.
2. Handling Deeply Nested LI Elements
Deep nesting can’t be solved with flat iteration— you need recursive traversal of the Nokogiri node tree, since <li> elements can contain their own <ul>/<ol> children. We’ll also track ordered list counters per nesting level to avoid numbering conflicts between nested OLs.
Full Working Code
Here’s a complete implementation that addresses both problems:
require 'prawn' require 'nokogiri' class NestedListPdfRenderer def initialize(prawn_doc) @pdf = prawn_doc @ol_counters = {} # Tracks independent counters for each nested OL level end def render(html_list) # Parse the HTML fragment with Nokogiri doc = Nokogiri::HTML.fragment(html_list) # Start processing from root level (0) process_node(doc, 0) end private def process_node(node, current_level) case node.name when 'ul' # Iterate over all child LI elements node.children.each do |child| process_node(child, current_level) if child.element? end when 'ol' # Initialize counter for this specific OL level @ol_counters[current_level] = 1 node.children.each do |child| process_node(child, current_level) if child.element? end # Clean up counter when exiting this OL scope @ol_counters.delete(current_level) when 'li' # Calculate indent: 2 spaces per level (tweak for tighter/wider spacing) indent = ' ' * (current_level * 2) # Get the correct list marker based on parent list type parent_list = node.parent marker = if parent_list.name == 'ul' "#{indent}• " else # Use and increment the level-specific OL counter counter = @ol_counters[current_level] @ol_counters[current_level] += 1 "#{indent}#{counter}. " end # Render LI text with clean spacing (strip extra whitespace from node content) @pdf.text "#{marker}#{node.text.strip}" # Recursively process any nested lists inside this LI node.children.each do |child| process_node(child, current_level + 1) if %w[ul ol].include?(child.name) end end end end # Example Usage Prawn::Document.generate('nested_lists.pdf') do |pdf| renderer = NestedListPdfRenderer.new(pdf) sample_html = <<~HTML <ul> <li>Level 1 Unordered Item <ol> <li>Level 2 Ordered Item 1 <ul> <li>Level 3 Unordered Item A</li> <li>Level 3 Unordered Item B <ol> <li>Level 4 Ordered Item 1</li> <li>Level 4 Ordered Item 2</li> </ol> </li> </ul> </li> <li>Level 2 Ordered Item 2</li> </ol> </li> <li>Level 1 Unordered Item 2</li> </ul> HTML renderer.render(sample_html) end
Key Features Explained
- Consistent Spacing: The
indentvariable ensures every nesting level has the same horizontal offset, and markers always have one space before the item text. - Recursive Nesting Handling: The
process_nodemethod calls itself for nested lists inside<li>elements, incrementing thecurrent_levelto adjust indentation and OL counters. - OL Counter Isolation: The
@ol_countershash tracks separate counters for each OL nesting level, so nested ordered lists don’t inherit or interfere with parent list numbering.
You can tweak the indent space count (change 2 to 3 or 4) to match your PDF’s styling needs. If you need to support other inline elements (like bold or italic text), you can extend the process_node method to handle those tags too!
内容的提问来源于stack exchange,提问作者Aniket Tiwari

