You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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 indent variable ensures every nesting level has the same horizontal offset, and markers always have one space before the item text.
  • Recursive Nesting Handling: The process_node method calls itself for nested lists inside <li> elements, incrementing the current_level to adjust indentation and OL counters.
  • OL Counter Isolation: The @ol_counters hash 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 09:35:32