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
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:
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
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.
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).
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"] ])
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