Using lists in prawn

2019-03-25 15:20发布

问题:

Im using prawn to create pdfs that contain much data in table format and some lists. The problem with the lists is that Im just using text as lists because there is no semantic equivalent to ul > li lists like I use them in the webfrointend. So the lists arent justified. A list point that uses more than one line looks creapy because I doesnt fit the list icon. How can I implement lists in prawn that dont look like crap?

回答1:

Prawn was a good PDF library but the problem is its own view system. There is Prawn-format but is not maintained anymore.

I suggest to use WickedPDF, it allows you to include simple ERB code in your PDF.

Using Prawn: another dirty and ugly solution is a two column table without border, first column contains list-bullet, second column text:

table([ ["•", "First Element"],
        ["•", "Second Element"],
        ["•", "Third Element"] ])


回答2:

I just had a similar problem and solved it within Prawn a slightly different way than using a table:

["Item 1","Item 2","Item 3"].each() do |list-item|

  #create a bounding box for the list-item label
  #float it so that the cursor doesn't move down
  float do
    bounding_box [15,cursor], :width => 10 do
      text "•"
    end
  end

  #create a bounding box for the list-item content
  bounding_box [25,cursor], :width => 600 do
    text list-item
  end

  #provide a space between list-items
  move_down(5)

end

This could obviously be extended (for example, you could do numbered lists with an each_with_index() rather than each()). It also allows for arbitrary content in the bounding box (which isn't allowed in tables).



回答3:

To create a bullet with Adobe's built in font, use \u2022.

\u2022 This will be the first bullet item
\u2022 blah blah blah

Prawn supports symbols (aka glyphs) with WinAnsi codes and these must be encoded as UTF-8. See this post for more details: https://groups.google.com/forum/#!topic/prawn-ruby/axynpwaqK1g

The Prawn manual has a complete list of the glyphs that are supported.



回答4:

Just did this for a customer. For everybody who wants to render preformatted html containing ul / ol lists:

def render_html_text(text, pdf)
  #render text (indented if inside ul)
  indent = 0 #current indentation (absolute, e.g. n*indent_delta for level n)
  indent_delta = 10 #indentation step per list level
  states = [] #whether we have an ol or ul at level n
  indices = [] #remembers at which index the ol list at level n, currently is

  #while there is another list tag do
  #  => starting position of list tag is at i
  #  render everything that comes before the tag
  #  cut everything we have rendered from the whole text
  #end
  while (i = text.index /<\/?[ou]l>/) != nil do
    part = text[0..i-1]
    if indent == 0 #we're not in a list, but at the top level
      pdf.text part, :inline_format => true
    else
      pdf.indent indent do
        #render all the lis
        part.gsub(/<\/li>/, '').split('<li>').each do |item|
          next if item.blank? #split may return some ugly start and end blanks

          item_text = if states.last == :ul
                        "• #{item}"
                      else # :ol
                        indices[indices.length-1] = indices.last + 1
                        "#{indices.last}. #{item}"
                      end

          pdf.text item_text, :inline_format => true
        end
      end
    end

    is_closing = text[i+1] == '/' #closing tag?
    if is_closing
      indent -= indent_delta
      i += '</ul>'.length

      states.pop
      indices.pop
    else
      pdf.move_down 10 if indent == 0

      type_identifier = text[i+1] #<_u_l> or <_o_l>
      states << if type_identifier == 'u'
                  :ul
                elsif type_identifier == 'o'
                  :ol
                else
                  raise "what means type identifier '#{type_identifier}'?"
                end
      indices << 0

      indent += indent_delta
      i += '<ul>'.length
    end

    text = text[i..text.length-1] #cut the text we just rendered
  end

  #render the last part
  pdf.text text, :inline_format => true unless text.blank?
end


回答5:

An excellent solution that respects the cursor position as well as render like a true list with a small number of lines of code is:

items = ["first","second","third"]
def bullet_list(items)
  start_new_page if cursor < 50
  items.each do |item|
    text_box "•", at: [13, cursor]
    indent(30) do
      text item
    end
  end
end

The start_new_page clause covers scenarios where the bullet line item may need to go onto the next page. This maintains keeping the bullet with the bullet content.

Example PDF Rendering Screenshot:



回答6:

I think a better approach is pre-processing the HTML string using Nokogiri, leaving only basics tags that Prawn could manage with "inline_format" option, as in this code:

def self.render_html_text(instr)
   # Replacing <p> tag
   outstr = instr.gsub('<p>',"\n")
   outstr.gsub!('</p>',"\n")
   # Replacing <ul> & <li> tags
   doc = Nokogiri::HTML(outstr)
   doc.search('//ul').each do |ul|
     content = Nokogiri::HTML(ul.inner_html).xpath('//li').map{|n| "• #{n.inner_html}\n"}.join
     ul.replace(content)
   end
   #removing some <html><body> tags inserted by Nokogiri
   doc.at_xpath('//body').inner_html
end


回答7:

One go-around is to create a method similar to crm's answer. The difference is that it won't break when the text goes to another page and you can have multiple levels as well.

def bullet_item(level = 1, string)
    indent (15 * level), 0 do
        text "• " + string
    end
end

Simply call this method like so:

bullet_item(1, "Text for bullet point 1")
bullet_item(2, "Sub point")

Feel free to refactor.