Using lists in prawn
Asked Answered
A

7

13

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?

Assembly answered 9/5, 2012 at 9:43 Comment(1)
I just tryed iplement lists as texts but it looks crapy as soon as the comment has more then one row.Assembly
N
25

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"] ])
Nerveracking answered 23/6, 2012 at 16:10 Comment(1)
Great! You can also use cell_style: { borders: [] } as an option to get rid of the borders and make it look like an actual list.Standush
B
9

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).

Blazonry answered 5/10, 2012 at 21:37 Comment(2)
This solution has an issue when the PDF document has multiple pages: when text goes to the next page, it shows up at the bottom.Congdon
I added start_new_page if cursor < 50 to the iteration to solve the multiple pages problemTera
H
8

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:

Example Rendered List

Headcheese answered 11/9, 2014 at 14:57 Comment(0)
P
4

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.

Pt answered 2/8, 2013 at 16:43 Comment(1)
Why did this answer not receive more recognition? @Pt -- just used this myself, it worked perfectly well. It should be said that your syntax assumes encapsulation in double quotes.Semiporcelain
R
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
Ripplet answered 30/9, 2013 at 9:41 Comment(1)
Thanks man it's been really useful to me!! Possible improvements: String#blank? is not pure Ruby. Used .strip == '' as a quick fix. Used #{item.strip} when rendering a bullet item, otherwise I ended up with a line feed after my bullets (because of linefeeds in my HTML markup probably)Hindustan
S
2

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
Subroutine answered 25/3, 2018 at 15:36 Comment(0)
C
1

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.

Congdon answered 18/6, 2013 at 14:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.