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?
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 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).
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:
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.
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
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.
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
Related
I'm new to Stack overflow so kindly excuse if my question deviates from the expected standard.
I processed a log file to a below format and I would like to form a HTML representation from the below data structure.
holder = [
{:states=>"Texas"}
{:cities=>"Antonio", :data=>{"Ron"=>"26", "Rock"=>"23", "Jake"=>"33"}}
{:cities=>"West_CIT", :data=>{}}
{:cities=>"Austin", :data=>{"Ron"=>"26", "Mike"=>"53", "Jake"=>"36"}}
{:states=>"California"}
{:cities=>"Sacramento", :data=>{"Jill"=>"584", "Rudy"=>"3"}}
{:cities=>"Los Angeles", :data=>{"Jill"=>"4", "Rudy"=>"2"}}
{:states=>"Georgia"}
{:cities=>"Atlanta", :data=>{"Ron"=>"6", "Ross"=>"2", "Jake"=>"35"}}
{:cities=>"Athens", :data=>{"Jill"=>"16", "Mike"=>"4", "Reeves"=>"8"}} ]
I'm trying to create a HTML file which displays the States only on the Top page, like below
<h1> States and Associate Demat details <h1>
Texas
California
Georgia
but on clicking any State it should expand to a below format holding the sub details
Texas
Antonio Ron 26
Rock 23
Jake 33
West_CIT
Austin Ron 26
Mike 53
Jake 36
California
Georgia
Likewise the other states.
I've gone thru the Ruby documentation as well as the Nokogiri gem but with my limited knowledge in ruby (or any programming language) hampers my progress.. Is it really possible with Ruby or should I go with PHP (learn again). Looking for the guidance here, thank you.
P.S I've taken this as a self assignment in an attempt to improve my Ruby (or programming skills)
What I wrote :
holder = []
counter = -1
text = File.open("states.log").read
text.each_line do |line|
line.strip!
next if line.empty?
next if line.include?('persons') || line.include?('demat')
next if ['-','*'].include? line[0]
chip = line.split ' '
if chip.size == 1 and line.start_with?('state')
holder[counter += 1] = {states: line, data: {}}
next
elsif chip.size == 1 and chip = /^(?!.*state_).*$/
holder[counter += 1] = {cities: line, data: {}}
next
end
chip.each_cons(2) do |key, value|
holder[counter][:data][key] = value
end
end
puts holder
Adding the sample log file (raw)
state_Texas
Antonio
persons demat
------------------------------ ----------
Ron 26
Ron 23
Jake 33
=========================================
----Persons
West_CIT
persons demat
------------------------------ ----------
=========================================
----Persons
Austin
persons demat
------------------------------ ----------
Ron 26
Mike 53
Jake 36
=========================================
state_California
Sacramento
persons demat
------------------------------ ----------
Jill 584
Rudy 3
=========================================
---- Persons
Los Angeles
persons demat
------------------------------ ----------
Jill 4
Rudy 2
=========================================
Likewise .......
First thing first: if you need to exchange data between applications I suggest to stick with standard formats like JSON or YAML. I don't know if you can control the data logging, but if you can, I suggest to change the code there.
That log file is really a mess but it contains enough information for convert it into a ruby data structure like arrays and hashes.
There is always a better way but I ended up with this solution.
REJECT_THIS = ["", "------------------------------ ----------", "----Persons", "---- Persons", "persons demat"]
holder = []
separator = '|||'
# here we store the file into the holder, skipping rows in REJECT_THIS
text = File.open("_states.log").read
text.each_line do |line|
line = line.split.join(' ')
holder << line unless REJECT_THIS.include? line
end
# just to change the separator mark into a shorter one
holder.map! { |e| e == "=========================================" ? separator : e}
# map where to split the array grouping by state
split_idxs = holder.map.with_index { |e, i| e [0..4] == 'state' ? i : 0}.uniq[1..-1]<<holder.size
# split the array in states using the index map using the my_split_at_index method and building the states hash
holder = holder.my_split_at_index(split_idxs).map { |element| {element.shift => element} }
# remove 'state_' and convert the key to symbol
holder.map! { |e| e.transform_keys { |key| key[6..-1].to_sym } }
# splits subarrays by separator then build the nested hash
holder.map! do |array|
array.transform_values do |sub_array|
split_idxs = sub_array.map.with_index { |e, i| e == separator ? i : 0 }.uniq[1..-1]
sub_array = sub_array.my_split_at_index(split_idxs).map! { |e| e[0] == separator ? e[1..-1] : e }
sub_array.map { |element| {city: element.shift, people: element} }
end
end
# splits the people string
holder.map do |state|
state.transform_values do |array|
array.map do |hash|
hash[:people].map! { |string| string.split(' ') }
end
end
end
p holder
In the code I used this Array monkey patch
class Array
def my_split_at_index(indexes = [])
shift = 0
splitted = []
indexes.map do |index|
splitted << self[shift..index-1]
shift = index
end
splitted
end
end
The variable holder now is an array of nested hashes that you can use with ERB in a code like Dan Hilton posted. The data structure of holder is not the same, so you need to tweak the code.
One last thing, to see how the structure of your data as a YAML looks:
require 'yaml'
puts holder.to_yaml
As iGian said you can use ERB templates to create your HTML. I would also recommend changing your data structure to be easier to iterate. You are currently using the array order to determine where the cities are. This requires additional checks on the template side. Instead, the cities should be nested in the states hash to look like: {:state => "Texas", :cities=>[{:name => "Antonio", :data=>{"Ron"=>"26", "Rock"=>"23", "Jake"=>"33"}]}. This way you have an array of cities for a given state. Using this data structure, your template would look something like:
template = ERB.new <<-EOF
<h1> States and Associate Demat details <h1>
<% Holder.each do |state| %>
<h2><%= state[:state] %></h2>
<% state[:cities].each do |city| %>
<h3><%= city[:name] %></h3>
<% city[:data].each do |name, value| %>
<%= name %> - <%= value %>
<% end %>
<% end %>
<% end %>
EOF
Note that using <%= %> with the = is used to wrap ruby expressions that you want to show up on your template. On the other hand <% %> is used to set variables and iterate through your data structure, but will not show up in your template.
In your case, the use of templating is probably overkill and you can get away with using File.write('index.html') but getting ERB practice is useful because it will be directly applicable if you decide to learn Ruby on Rails.
I have a #profile that has_many positions. The HTML version looks like this:
<h3>Defensive Midfield</h3>
<div class="alternative-positions">
<small>Attacking Midfield</small><br />
<small>Right Midfield</small>
</div>
However, what I am doing is creating a helper to abstract this view logic, so I am putting a bunch of content_tags in my profile_helper.rb.
The rules are as follows:
when a profile only has 1 position, then it goes into an h3 tag.
when there are 2, the first one goes into h3, and the second goes into small which is in div.alternative-positions.
when there are 3, follow #1 & #2 then put the third position within a newly created <small> within the EXISTING div.alternative-positions.
1 & 2 are straightforward and I can do that with this code:
def player_positions(profile)
profile.positions.each_with_index do |index, position|
if index == 0
content_tag(:h3, position.position_type)
else
content_tag(:div, content_tag(:small, position.position_type), class: "alternative-positions")
end
end
end
Which was called in my view like so:
<%= player_positions(#profile) %>
However, the issue I am struggling with is the 3rd one.
I am not quite sure how to select that div, and make sure the newly created small tag and content is nested within the existing div.
How do I achieve that?
I'd recommend treating the first in the list separately (ie not being part of the each clause) eg:
def player_positions(profile)
position_str = ""
position_str << content_tag(:h3, profile.positions.first.position_type) if profile.positions.size >= 1
if profile.positions.size >= 2
# this is the surrounding div for all other positions
position_str << "<div class='alternative-positions'>"
profile.positions[1..-1].each_with_index do |index, position|
position_str << content_tag(:small, position.position_type)
end
position_str << "</div>"
end
position_str
end
or similar (note: not checked for typos or bugs)
I'm trying to take many posts with example text "you can find other #apple #orchard examples at www.google.com and www.bing.com #funfruit" and display the text to the user with URLs and #tags linking to their appropriate routes.
I have successfully done this with text that only contains any number of #tags, or a single URL, with the following code:
application_controller.rb
def splice_posts(posts, ptags, spliced)
# Build all posts as items in spliced, with each item an post_pieces array
posts.reverse.each do |post|
tag_indices = []
tag_links = []
# Get post URLs: [{:url=>"www.google.com", :indices=>[209, 223]}]
post_links = extract_urls_with_indices(post.text)
# Save each as rails style link with indices
# For each of the ptags associated with post
ptags.where(post_id:post.id).each do |ptag|
# Store hashtag's start/stop indices for splicing post
tag_indices.append([ptag.index_start, ptag.index_end])
# Store hashtag links for splicing post
tag_links.append(view_context.link_to '#' + ptag.hashtag, atag_path(Atag.find_by(id:ptag.atag_id).id),
:class => 'post_hashtag', :remote => true, :onclick => "location.href='#top'")
end
# Create and store post as post_pieces in spliced
# If there are no hashtags
if tag_indices.length == 0
# And no links
if post_links.length == 0
spliced.append([post.text, post.id])
# But links
else
spliced.append([post.text[0..post_links[0][:indices][0]-2],
view_context.link_to(post_links[0][:url], post_links[0][:url], target: "_blank"),
post.text[post_links[0][:indices][1]..-1], post.id])
end
# Elsif there is one hashtag
elsif tag_indices.length == 1
if post.text[0] == '#'
spliced.append([post.text[2..tag_indices[0][0]], tag_links[0],
post.text[tag_indices[0][1]..-1], post.id])
else
spliced.append([post.text[0..tag_indices[0][0]-2], tag_links[0],
post.text[tag_indices[0][1]..-1], post.id])
end
# Else there are multiple hashtags, splice them in and store
else
# Reset counter for number of tags in this post
#tag_count = 0
# If post begins with tag, no text before first tag
if tag_indices[0][0] == 0
post_pieces = []
# Else store text before first tag
else
post_pieces = [post.text[0..tag_indices[0][0]-2]]
end
# Build core of post_pieces, splicing together tags and text
tag_indices.each do |indice|
post_pieces.append(tag_links[#tag_count])
post_pieces.append(post.text[indice[1]..tag_indices[#tag_count+1][0]-2])
if #tag_count < (tag_indices.length-2)
#tag_count += 1
else
# Do nothing
end
end
# Knock off the junk at the end
post_pieces.pop
post_pieces.pop
# Finish compiling post_pieces and store it in spliced
post_pieces.append(tag_links[-1])
post_pieces.append(post.text[tag_indices[-1][1]..-1])
# Make last item in array post id for comment association purposes
post_pieces.append(post.id)
spliced.append(post_pieces)
end
end
end
The spliced posts are then easily served in the view piece by piece:
<% #posts_spliced.each do |post_pieces| %>
<%# Build post from pieces (text and hashtags), excluding last element which is post_id %>
<% post_pieces[0..-2].each do |piece| %>
<%= piece %>
<% end %>
<% end %>
The problem is that this implementation is convoluted to begin with, and trying to patch it with dozens of nested if/else statement to handle URLs seems like madness, as I'm suspecting that a more experienced software engineer/rails developer could enlighten me on how to do this with a fraction of the code.
To clarify I have the following variables already available for each post (with examples) :
post = 'some text with #tags and www.urls.com potentially #multiple of each.com'
post_urls = [{:url=>"www.urls.com", :indices=>[25, 37]}, {:url=>"each.com", :indices=>[63, 71]}]
post_tags = [{:hashtag=>"tags", :indices=>[15, 20]}, {:hashtag=>"multiple", :indices=>[50, 59]}]
I'm thinking that a more practical implementation might involve the indices more directly, but perhaps breaking the post into elements in an array is the wrong idea altogether, or perhaps there is an easier way, but before I spend a couple hours conceptualizing the logic and writing the code for another possible unideal solution, I thought I should see if someone could enlighten me here.
Thanks so much!
Unless I'm missing something important, I think you've overcomplicated things.
First, you split the string by spaces.
string = "whatever string typed in by user"
split_string = string.split
Then you map the split-string-array according to your requirements and join the results.
# create show_hashtag(str) and show_link(str) helpers
split_string.map do |str|
if str.starts_with?('#')
show_hashtag(str)
elsif url_regexp.match(str) # you must define url_regexp
show_link(str)
else
str
end
end.join(' ')
You won't have to worry about positions of the text, tags, or links because map will take care of it for you.
Wrap all of that in a helper and in your view you could do the following:
<%= your_helper(string_typed_in_by_user).html_safe %>
Watch out for the user typing in HTML though!
I have a controller that calls a find_photos method, passing it a query string (name of file)
class BrandingPhoto < ActiveRecord::Base
def self.find_photos(query)
require "find"
found_photos = []
Find.find("u_photos/photo_browse/photos/") do |img_path|
# break off just the filename from full path
img = img_path.split('/').last
if query.blank? || query.empty?
# if query is blank, they submitted the form with browse all- return all photos
found_photos << img
else
# otherwise see if the file includes their query and return it
found_photos << img if img.include?(query)
end
end
found_photos.empty? ? "no results found" : found_photos
end
end
This is just searching a directory full of photos- there is no table backing this.
Ideally what I would like is to be able to limit the number of results returned by find_photos to around 10-15, then fetch the next 10-15 results as needed.
I was thinking that the code to do this might involve looping through 10 times and grabbing those files- store the last filename in a variable or as a parameter, and then send that variable back to the method, telling it to continue the search from that filename.
This assumes that the files are looped through in the same order everytime, and that there is no simpler way to accomplish this.
If there are any suggestions, I'd love to hear them/see some examples of how you'd accomplish this.
Thank you.
The first thing that comes to mind for this problem is to cut the array down after you come out of the loop. This wouldn't work well with a ton of files though A different solution might be to add a break for the size of the array viz. break if found_photos.length > 10 inside the loop
It's not too hard to do what you want, but you need to consider how you'll handle entries that are added or removed in-between page loads, filenames with UTF-8 or Unicode characters, and embedded/parent directories.
This is old-school code for the basis for what you're talking about:
require 'erb'
require 'sinatra'
get '/list_photos' do
dir = params[ :dir ]
offset = params[ :offset ].to_i
num = params[ :num ].to_i
files = Dir.entries(dir).reject{ |fn| fn[/^\./] || File.directory?(File.join(dir, fn)) }
total_files = files.size
prev_a = next_a = ''
if (offset > 0)
prev_a = "<a href='/list_photos?dir=#{ dir }&num=#{ num }&offset=#{ [ 0, offset - num ].max }'><< Previous</a>"
end
if (offset < total_files)
next_a = "<a href='/list_photos?dir=#{ dir }&num=#{ num }&offset=#{ [ total_files, offset + num ].min }'>Next >></a>"
end
files_to_display = files[offset, num]
template = ERB.new <<EOF
<html>
<head></head>
<body>
<table>
<% files_to_display.each do |f| %>
<tr><td><%= f %></td></tr>
<% end %>
</table>
<%= prev_a %> | <%= total_files %> files | <%= next_a %>
</body>
</html>
EOF
content_type 'text/html'
template.result(binding)
end
It's a little Sinatra server, so save it as test.rb and run from the command-line using:
ruby test.rb
In a browser connect to the running Sinatra server using a URL like:
http://hostname:4567/list_photos?dir=/path/to/image/files&num=10&offset=0
I'm using Sinatra for convenience, but the guts of the routine is the basis for what you want. How to convert it into Rails terms is left as an exercise for the reader.
I have a page that will list news articles. To cut down on the page's length, I only want to display a teaser (the first 200 words / 600 letters of the article) and then display a "more..." link, that, when clicked, will expand the rest of the article in a jQuery/Javascript way. Now, I've all that figured out and even found the following helper method on some paste page, which will make sure, that the news article (string) is not chopped up right in the middle of a word:
def shorten (string, count = 30)
if string.length >= count
shortened = string[0, count]
splitted = shortened.split(/\s/)
words = splitted.length
splitted[0, words-1].join(" ") + ' ...'
else
string
end
end
The problem that I have is that the news article bodies that I get from the DB are formatted HTML. So if I'm unlucky, the above helper will chop up my article string right in the middle of an html tag and insert the "more..." string there (e.g. between ""), which will corrupt my html on the page.
Is there any way around this or is there a plugin out there that I can use to generate excerpts/teasers from an HTML string?
You can use a combination of Sanitize and Truncate.
truncate("And they found that many people were sleeping better.",
:omission => "... (continued)", :length => 15)
# => And they found... (continued)
I'm doing a similar task where I have blog posts and I just want to show a quick excerpt. So in my view I simply do:
sanitize(truncate(blog_post.body, length: 150))
That strips out the HTML tags, gives me the first 150 characters and is handled in the view so it's MVC friendly.
Good luck!
My answer here should do work. The original question (err, asked by me) was about truncating markdown, but I ended up converting the markdown to HTML then truncating that, so it should work.
Of course if your site gets much traffic, you should cache the excerpt (perhaps when the post is created/updated, you could store the excerpt in the database?), this would also mean you could allow the user to modify or enter their own excerpt
Usage:
>> puts "<p><b>Something</p>".truncate_html(5, at_end = "...")
=> <p><b>Someth...</b></p>
..and the code (copied from the other answer):
require 'rexml/parsers/pullparser'
class String
def truncate_html(len = 30, at_end = nil)
p = REXML::Parsers::PullParser.new(self)
tags = []
new_len = len
results = ''
while p.has_next? && new_len > 0
p_e = p.pull
case p_e.event_type
when :start_element
tags.push p_e[0]
results << "<#{tags.last}#{attrs_to_s(p_e[1])}>"
when :end_element
results << "</#{tags.pop}>"
when :text
results << p_e[0][0..new_len]
new_len -= p_e[0].length
else
results << "<!-- #{p_e.inspect} -->"
end
end
if at_end
results << "..."
end
tags.reverse.each do |tag|
results << "</#{tag}>"
end
results
end
private
def attrs_to_s(attrs)
if attrs.empty?
''
else
' ' + attrs.to_a.map { |attr| %{#{attr[0]}="#{attr[1]}"} }.join(' ')
end
end
end
Thanks a lot for your answers!
However, in the meantime I stumbled upon the jQuery HTML Truncator plugin, which perfectly fits my purposes and shifts the truncation to the client-side. It doesn't get any easier :-)
you would have to write a more complex parsers if you dont want to split in the middle of html elements. it would have to remember if it is in the middle of a <> block and if its between two tags.
even if you did that, you would still have problems. if some put the whole article into an html element, since the parser couldnt split it anywhere, because of the missing closing tag.
if it is possible at all i would try not to put any tags into the articles or keep it to tags that dont contain anything (no <div> and so on). that way you would only have to check if you are in the middle of a tag which is pretty simple:
def shorten (string, count = 30)
if string.length >= count
shortened = string[0, count]
splitted = shortened.split(/\s/)
words = splitted.length
if(splitted[words-1].include? "<")
splitted[0,words-2].join(" ") + ' ...'
else
splitted[0, words-1].join(" ") + ' ...'
else
string
end
end
I would have sanitized the HTML and extracted the first sentence. Assuming you have an article model, with a 'body' attribute that contains the HTML:
# lib/core_ext/string.rb
class String
def first_sentence
self[/(\A[^.|!|?]+)/, 1]
end
end
# app/models/article.rb
def teaser
HTML::FullSanitizer.new.sanitize(body).first_sentence
end
This would convert "<b>This</b> is an <em>important</em> article! And here is the rest of the article." into "This is an important article".
I solved this using following solution
Install gem 'sanitize'
gem install sanitize
and used following code, here body is text containing html tags.
<%= content_tag :div, Sanitize.clean(truncate(body, length: 200, separator: ' ', omission: "... #{ link_to '(continue)', '#' }"), Sanitize::Config::BASIC).html_safe %>
Gives excerpt with valid html.
I hope it helps somebody.
There is now a gem named HTMLTruncator that takes care of this for you. I've used it to display post excerpts and the like, and it's very robust.
If you are using Active Text, I would suggest first converting the text using to_plain_text.
truncate(sanitize(career.content.body.to_plain_text), length: 150).squish