How to let a user render a partial?


#1

Hi all,

I’m currently doing a little CMS system in Rails and I would like the
user/administrator of a site to be able to render partials on pages. I
have a Page model with a content attribute. Inside that attribute I
would like the admin to be able to put something like this:

{contact_form submit:“Submit now!” reset:“Reset form”}

This is just an example, but this should then render a partial like
this:

render :partial => “widgets/contact_form”, :locals => {:submit =>
“Submit now!”, :reset => “Reset form”}

I have been trying to make up a regular expression to approach this, but
I’m not very good at it. It looks like this:

/{(\w+) (\w+):(.+)*}/

But it is hard to make it respect multiple arguments, if you know what I
mean. Now, if any of you have an idea of how to finish this regular
expression, or maybe know a different solution of how to let the admin
render partials “dynamically”, I would appreciate if you’d let me know.


#2

I’d have a look a string scanner if I were you
Fred

Sent from my iPhone

On 8 Oct 2008, at 18:12, David T. <rails-mailing-list@andreas-


#3

Frederick C. wrote:

I’d have a look a string scanner if I were you
Fred

Sent from my iPhone

That’s exactly what I’ve been thinking of, but the real challenge is to
get the regular expression right. It needs to respect multiple
arguments. Any suggestions?


#4

Frederick C. wrote:

get the regular expression right. It needs to respect multiple
arguments. Any suggestions?

that’s the point - with stringscanner you don’t need to come up with
one giant regular expression that gets everything right

Aha! I thought you meant the RegExp#scan method.

as a quick example, this

require ‘strscan’
s = StringScanner.new(‘blah {some_partial arg1:foo arg2:bar} other
stuff here {otherpartial arg1:baz}’)

while s.scan_until /{/
puts “start of call”
puts s.scan /\w+/
s.skip /\s+/
while output=s.scan( /\w+:\w+/)
puts output
s.skip /\s+/
end
s.skip_until /}/
puts “end of call”
end

outputs

start of call
some_partial
arg1:foo
arg2:bar
end of call
start of call
otherpartial
arg1:baz
end of call

This looks just great! I’ll return when I’ve written a complete method.


#5

On 8 Oct 2008, at 19:00, David T. wrote:

get the regular expression right. It needs to respect multiple
arguments. Any suggestions?

that’s the point - with stringscanner you don’t need to come up with
one giant regular expression that gets everything right

as a quick example, this

require ‘strscan’
s = StringScanner.new(‘blah {some_partial arg1:foo arg2:bar} other
stuff here {otherpartial arg1:baz}’)

while s.scan_until /{/
puts “start of call”
puts s.scan /\w+/
s.skip /\s+/
while output=s.scan( /\w+:\w+/)
puts output
s.skip /\s+/
end
s.skip_until /}/
puts “end of call”
end

outputs

start of call
some_partial
arg1:foo
arg2:bar
end of call
start of call
otherpartial
arg1:baz
end of call

Fred


#6

David T. wrote:

as a quick example, this

require ‘strscan’
s = StringScanner.new(‘blah {some_partial arg1:foo arg2:bar} other
stuff here {otherpartial arg1:baz}’)

while s.scan_until /{/
puts “start of call”
puts s.scan /\w+/
s.skip /\s+/
while output=s.scan( /\w+:\w+/)
puts output
s.skip /\s+/
end
s.skip_until /}/
puts “end of call”
end

This looks just great! I’ll return when I’ve written a complete method.

Okay, I’ve been taking a look at this, and I’m facing a problem. When
I’ve scanned a string I want to actually replace all the {some_partial
foo:bar}'s with partial renderings.

I’ve been looking through the StringScanner documentation
(http://www.ruby-doc.org/core/classes/StringScanner.html), but it
doesn’t seem to cover the topic of replacing matches instead of just
extracting it, if you know what I mean.

How can I replace a {some_partial foo:bar} match with a partial
rendering?


#7

Frederick C. wrote:

How can I replace a {some_partial foo:bar} match with a partial
rendering?

the string scanner will tell you its position. Keep track of the
relevant positions and then use String’s usual methods to replace
content between 2 positions.

Got it. Okay, I have made this method:

def dropify(content)

create a StringScanner

s = StringScanner.new(content)
while s.scan_until /{/
# where are we now?
from = s.pointer - 1
# locals to pass to the partial
arguments = {}
partial = s.scan /\w+/
s.skip /\s+/
while argument = s.scan( /\w+:\w+/)
# splitting the argument up into a name and a value
argument.split!(":")
name = argument.first.to_sym
value = argument.last
# putting it into the arguments hash
arguments[name] = value
s.skip /\s+/
end
# where are we now?
to = s.pointer+1
s.skip_until /}/
# put the partial on top of the {some_partial foo:bar} match
content[from, to] = render :partial => “drops/#{partial}”, :locals
=> arguments
end

return

content
end

Let me start out by saying that both the argument, name, value,
arguments, to and from has the values they should have when I debug
them, but for some reason I’m getting an error that looks like this:

compile error
C:/…/app/views/drops/_contact_form.html.erb:-11: syntax error,
unexpected tIDENTIFIER, expecting ‘]’
bar=foos = local_assigns[:bar=foo]
^
C:/…/app/views/drops/_contact_form.html.erb:-7: syntax error,
unexpected tASSOC, expecting kEND
bar=>foos = local_assigns[:bar=>foo]
^
C:/…/app/views/drops/_contact_form.html.erb:-5: syntax error,
unexpected tIDENTIFIER, expecting ‘]’
foo=bar = local_assigns[:foo=bar]

Do anyone know what this means?


#8

On Oct 9, 7:53 am, David T. removed_email_address@domain.invalid
wrote:

David T. wrote:
I’ve been looking through the StringScanner documentation
(http://www.ruby-doc.org/core/classes/StringScanner.html), but it
doesn’t seem to cover the topic of replacing matches instead of just
extracting it, if you know what I mean.

How can I replace a {some_partial foo:bar} match with a partial
rendering?

the string scanner will tell you its position. Keep track of the
relevant positions and then use String’s usual methods to replace
content between 2 positions.

Fred


#9

Frederick C. wrote:

Let me start out by saying that both the argument, name, value,
arguments, to and from has the values they should have when I debug
them, but for some reason I’m getting an error that looks like this:

A general point - it may not be advisable to modify the string while
stringscanner is iterating over it

I don’t see other possibilities.

At least on my version of ruby String#split! doesn’t exist, and the
errors suggest it didn’t actually do what you think it did (and I
struggle to see how it could exist)
name, value = arguments.split(/:/) would probably work better.

I see, that is corrected now.

The error you get later is a reflection of the fact that the keys in
your locals are things like bar=foo (and i imagine the values are
blank)

Unfortunately that is not true. When I comment out the “content[from,
to] = render…” line and put “puts debug arguments” on the line before
so it looks like this:


s.skip_until /}/
puts debug arguments
#content[from, to] = render :partial => “drops/#{partial}”, :locals =>
arguments

I get output similar to this:


foo: bars
bar: foos

I assume this means that the arguments hash is correctly formatted. But
I’m getting the error when the “content[from, to] = render…” line is
NOT commented out, anyway.


#10

On 9 Oct 2008, at 10:15, David T. wrote:

I don’t see other possibilities.
accumulate the replacements you need and do them at the end (last one
first)

your locals are things like bar=foo (and i imagine the values are
blank)

Unfortunately that is not true. When I comment out the “content[from,
to] = render…” line and put “puts debug arguments” on the line
before
so it looks like this:

given how :locals is handled it seems highly likely

foo: bars
bar: foos

It would be a lot simpler to just stick a breakpoint in there (or
output arguments.inspect)


#11

Frederick C. wrote:

On 9 Oct 2008, at 10:15, David T. wrote:

I don’t see other possibilities.
accumulate the replacements you need and do them at the end (last one
first)

your locals are things like bar=foo (and i imagine the values are
blank)

Unfortunately that is not true. When I comment out the “content[from,
to] = render…” line and put “puts debug arguments” on the line
before
so it looks like this:

given how :locals is handled it seems highly likely

foo: bars
bar: foos

It would be a lot simpler to just stick a breakpoint in there (or
output arguments.inspect)

I’m quite close to the solution now, my method now looks like this:

def dropify(content)
s = StringScanner.new(content)
drops = {}
i = 0
while s.scan_until /{/
drop = {}
drop[:from] = s.pointer - 1
drop[:arguments] = {}
drop[:partial] = s.scan /\w+/
s.skip /\s+/
while argument = s.scan( /\w+:\w+/)
name, value = argument.split(/:/)
drop[:arguments][name.to_sym] = value
s.skip /\s+/
end
drop[:to] = s.pointer+1
s.skip_until /}/
i = i+1
drops[i] = drop
end
puts drops.inspect
drops.each do |drop|
#content[drop[:from], drop[:to]] = “…Partial content…”
end
content
end

The difference is, that I now have a hash called “drops” with properties
for all my partials to render in there. For each partial I create a
temporary hash called “drop” that I apply to “drops” in the end.

Then I loop over each drop and replace the {some_partial foo:bar} stuff
with a partial (like you wanted: after the StringScanner has finished).

As you can see, I’m doing a drops.inspect, and it outputs this:

{1=>{:to=>42, :partial=>“contact_form”, :arguments=>{:hello=>“you”,
:bar=>“foos”, :foo=>“bars”}, :from=>0}, 2=>{:to=>88,
:partial=>“contact_form”, :arguments=>{:hi=>“you”, :its=>“a_good_day”},
:from=>43}}

It looks right but when I uncomment one of the last lines
(content[drop[:from], drop[:to]] = "…) I get this error:

Symbol as array index

But I’m not using an array!.. Am I missing something?


#12

On Oct 9, 9:39 am, David T. removed_email_address@domain.invalid
wrote:

def dropify(content)
# splitting the argument up into a name and a value
# put the partial on top of the {some_partial foo:bar} match
content[from, to] = render :partial => “drops/#{partial}”, :locals
=> arguments
end

return

content
end

Let me start out by saying that both the argument, name, value,
arguments, to and from has the values they should have when I debug
them, but for some reason I’m getting an error that looks like this:

A general point - it may not be advisable to modify the string while
stringscanner is iterating over it

At least on my version of ruby String#split! doesn’t exist, and the
errors suggest it didn’t actually do what you think it did (and I
struggle to see how it could exist)
name, value = arguments.split(/:/) would probably work better.

The error you get later is a reflection of the fact that the keys in
your locals are things like bar=foo (and i imagine the values are
blank)

Fred


#13

On 9 Oct 2008, at 11:16, David T. wrote:

Unfortunately that is not true. When I comment out the
output arguments.inspect)
drop[:arguments] = {}
drops[i] = drop
for all my partials to render in there. For each partial I create a
:bar=>“foos”, :foo=>“bars”}, :from=>0}, 2=>{:to=>88,
:partial
=>“contact_form”, :arguments=>{:hi=>“you”, :its=>“a_good_day”},
:from=>43}}

It looks right but when I uncomment one of the last lines
(content[drop[:from], drop[:to]] = "…) I get this error:

Symbol as array index

But I’m not using an array!.. Am I missing something?

Yes :slight_smile:
here drop is an array (the first element is the key, the second is the
value)
Typically one using each with a hash, one does
some_hash.each do |key, value|

end

lastly, why is drops a hash at all and not an array ?

Fred


#14

On 9 Oct 2008, at 15:14, David T. wrote:

In this case the partial is longer than the {some_partial} and some of
the second instance of the partial disappears. How can I force Ruby to
replace the characters between to positions with a longer string?

You could fiddle with insert or something like that. There is another
way of doing it:
Say you’ve determine the two sections to replace are between positions
20 and 40 and then between 100 and 120
you would do

result = “”
result << original_string[0, 20]
result << render of first section
result << original_string[40, 60]
result << render of last section
result << original_string[120, original_string.length - 120

Fred


#15

Frederick C. wrote:

But I’m not using an array!.. Am I missing something?

Yes :slight_smile:
here drop is an array (the first element is the key, the second is the
value)
Typically one using each with a hash, one does
some_hash.each do |key, value|

end

All right. Fixed. :slight_smile:

lastly, why is drops a hash at all and not an array ?

I don’t know, that is changed now, too. Thanks.

One last question: I’m currently replacing the {some_partial foo:bar} to
the partial like this:

content[drop[:from], drop[:to]] = render(:partial =>
“drops/#{drop[:partial]}”, :locals => drop[:arguments])

But this is very buggy. The [fixnum, fixnum] method assumes that the new
thing (in my case, a partial) is of the same length as the {some_partial
foo:bar}. That means the partial is “swallowing” up the next partial if
it’s too long. If my partial looks like this:

This is a test drop. Please don’t edit or remove it.

and I do {some_partial foo:bar} twice I get this output:

This is a test drop. Please don’t edit or remove it.
don’t edit or remove it.

In this case the partial is longer than the {some_partial} and some of
the second instance of the partial disappears. How can I force Ruby to
replace the characters between to positions with a longer string?


#16

Frederick C. wrote:

In this case the partial is longer than the {some_partial} and some of
the second instance of the partial disappears. How can I force Ruby to
replace the characters between to positions with a longer string?

You could fiddle with insert or something like that. There is another
way of doing it:
Say you’ve determine the two sections to replace are between positions
20 and 40 and then between 100 and 120
you would do

result = “”
result << original_string[0, 20]
result << render of first section
result << original_string[40, 60]
result << render of last section
result << original_string[120, original_string.length - 120

That’s seems a little complicated (and I don’t see the flexibility of
it, either) but now I’m using the String#insert and at the end I do a
String#gsub to remove the {some_partial foo:bar}'s.

So my final method looks like this:

==========
def dropify(content)
s = StringScanner.new(content)
drops = Array.new
while s.scan_until(/{/)
drop = {}
drop[:from] = s.pointer - 1
drop[:arguments] = {}
drop[:partial] = s.scan(/\w+/)
s.skip /\s+/
while argument = s.scan(/\w+:\w+/)
name, value = argument.split(/:/)
drop[:arguments][name.to_sym] = value
s.skip /\s+/
end
s.skip_until /}/
drops << drop
end
drops.reverse!
drops.each do |drop|
content.insert(drop[:from], render(:partial =>
“drops/#{drop[:partial]}”, :locals => drop[:arguments]))
end
content.gsub!(/{\w+.*}/, “”)
content
end

Thanks for your help! I hope some of you can use this method as well.


#17

On 9 Oct 2008, at 15:52, David T. wrote:

way of doing it:
result << original_string[120, original_string.length - 120

That’s seems a little complicated (and I don’t see the flexibility of
it, either) but now I’m using the String#insert and at the end I do a
String#gsub to remove the {some_partial foo:bar}'s.

It avoids the need to maintain the drop array (and also means that you
don’t need a gsub that as is i believe will remove the wrong content
if there are more than one partial (because the * is greedy)

you could have something like

def dropify(content)
s = StringScanner.new(content)
output = “”
previous_end = 0
while s.scan_until(/{/)
output << content[previous_end, s.pointer - previous_end]
partial = s.scan(/\w+/)
s.skip /\s+/
arguments = {}
while argument = s.scan(/\w+:\w+/)
name, value = argument.split(/:/)
arguments[name.to_sym] = value
s.skip /\s+/
end
s.skip_until /}/
previous_end = s.pointer
end
output << content[s.pointer, content.length - s.pointer]
end

which is marginally simpler (the above could contain off by 1 errors
in the various offsets).

Fred


#18

Frederick C. wrote:

That’s seems a little complicated (and I don’t see the flexibility of
it, either) but now I’m using the String#insert and at the end I do a
String#gsub to remove the {some_partial foo:bar}'s.

It avoids the need to maintain the drop array (and also means that you
don’t need a gsub that as is i believe will remove the wrong content
if there are more than one partial (because the * is greedy)

I admit the gsub was an emergency solution, and yes, there are a risk
that it will strip the wrong content.

you could have something like

def dropify(content)
s = StringScanner.new(content)
output = “”
previous_end = 0
while s.scan_until(/{/)
output << content[previous_end, s.pointer - previous_end]
partial = s.scan(/\w+/)
s.skip /\s+/
arguments = {}
while argument = s.scan(/\w+:\w+/)
name, value = argument.split(/:/)
arguments[name.to_sym] = value
s.skip /\s+/
end
s.skip_until /}/
previous_end = s.pointer
end
output << content[s.pointer, content.length - s.pointer]
end

I’ve been taking a look at your code and it looks interesting. But the
only thing I can’t figure out is where (and how) you’ld render the
partial in the code above.

I’ve been testing your method and something is wrong. E.g. if you have
this content:


{test}

Lorem ipsum dolor sit amet.

{test}

Lorem ipsum dolor sit amet.

you’ll get absolutely nothing back. I tried changing the 6th line to
this:

output << content[previous_end, s.pointer]

Still wrong:


{

Lorem ipsum dolor sit amet.

Lorem ipsum dolor sit amet.

{test}

Lorem ipsum dolor sit amet.

Did your method work for you, or what?

which is marginally simpler (the above could contain off by 1 errors
in the various offsets).

What is “off by 1 errors”?


#19

Frederick C. wrote:

Did your method work for you, or what?

yeah i left out that bit by accident. you should do that (ie output <<
render(…)) after I set previous_end at the end of the loop

Okay, thanks!

What is “off by 1 errors”?

some of the offsets might be off by 1 (eg someplaces where it says
pointer it might need to be s.pointer-1 etc…). More generally I just
bashed that out in Mail so I expect that there’s the odd mistake
like that in there.

All right, with my new knowledge I’ve changed your method a little bit
and it works perfectly well.

So this is the new final version of the method:

==========
def dropify(content)
s = StringScanner.new(content)
output = “”
previous_end = 0
while s.scan_until(/{/)
output << content[previous_end, s.pointer - previous_end - 1]
partial = s.scan(/\w+/)
s.skip /\s+/
arguments = {}
while argument = s.scan(/\w+:\w+/)
name, value = argument.split(/:/)
arguments[name.to_sym] = value
s.skip /\s+/
end
s.skip_until /}/
previous_end = s.pointer
output << render(:partial => “drops/#{partial}”, :locals =>
arguments)
end
output << content[s.pointer, content.length - s.pointer]
end

Notice that in the 6th line i subtract s.pointer-previous_end by 1 and
that’s what made it work.

Thanks for your help, Fred!


#20

On Oct 15, 8:00 am, David T. removed_email_address@domain.invalid
wrote:

Frederick C. wrote:

Did your method work for you, or what?

yeah i left out that bit by accident. you should do that (ie output <<
render(…)) after I set previous_end at the end of the loop

which is marginally simpler (the above could contain off by 1 errors
in the various offsets).

What is “off by 1 errors”?

some of the offsets might be off by 1 (eg someplaces where it says
pointer it might need to be s.pointer-1 etc…). More generally I just
bashed that out in Mail so I expect that there’s the odd mistake
like that in there.

Fred