How to let a user render a partial?

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.

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

Frederick Cheung 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?

Frederick Cheung 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?

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

David Trasbo 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?

David Trasbo 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

Frederick Cheung 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?

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

Frederick Cheung 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:

Frederick Cheung 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.

accumulate the replacements you need and do them at the end (last one
first)

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:

given how :locals is handled it seems highly likely

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

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

Frederick Cheung 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?

Frederick Cheung 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?

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

Frederick Cheung 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?

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

Frederick Cheung 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:

Frederick Cheung 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.

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

Frederick Cheung 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:

Frederick Cheung 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

Frederick Cheung 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:

David Trasbo wrote:

========== 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

Okay, I actually have a concern about this method. Right now it only accepts this kind of syntax:

{test foo:foo_bar}

Since spaces are not covered by \w+ this is not accepted:

{test foo:foo bar}

So I tried changing \w+ to .+ but of course that is never going to work. E.g. if I try to pass multiple arguments to the partial like this:

{test foo:foo bar bar:bar foo}

then only one argument will only be passed (foo) and it will have this value: "foo bar bar" because the .+ includes that.

But what about putting quotation marks around it?

".+"

Still the same problem. Now foo will just have this value: "“foo bar” bar". So, here is my question: How can I make this method accept multiple arguments without being limited to that the values have to match "\w+"?