tye has asked for the wisdom of the Perl Monks concerning the following question:
Have I just not found it? It seems a pretty basic, fundamental, and important feature. And I have not yet found any built-in support in HTML::Mason for constructing a link (as in <a href=...) from one Mason component to another where that link includes arguments.
That is, other than a quite awkward construction like:
<a href="/path/to/component?who=<% $who |u %>;why=<% $why |u %>"> Title goes here</a>
Having to remember to include "|u" for each parameter makes that nearly unacceptable in my book (after seeing way too many bugs from lack of URL escaping that pass unnoticed for a long time and then turn into a crisis, even a security problem). It also gets quite tedious (and error-prone and hard to read) when producing a table full of similar links.
But to see how awkward that really can be, imagine what I find to be a common case: Having a hash of arguments that you want to include in the link. Am I really supposed to roll my own URL constructor for such an obvious case?
<& .link, 'Edit Settings', '/widget/settings', %Context &> ... <%def .link> % my( $title, $page, %args ) = @_; <a href="<% $page %>? % for my $key ( sort keys %args ) { <% $key |u %>=<% $args{$key} |u %>; % } "><% $title |h %></a> </%def>
Although that first line is a reasonable interface, the implementation, of course, doesn't actually work, producing:
<a href="/widget/settings? acct=some_acct%23id; widget=widget%2Bid; ">Edit Settings</a>
Is there a better (and actually correct) way to write such in Mason?
Too bad defining a "removing newlines and adjacent whitespace" Mason filter (call it "|w") doesn't allow me to address this problem as simply as:
<& .link, 'Edit Settings', '/widget/settings', %Context |w &>
(You can use "|w" inside of <% ... %> but not inside of <& ... &>.)
So, (at least for now) I resign myself to looking outside of Mason for a solution.
My first stop was CGI because I already know that CGI.pm knows how to construct a URL with parameters. I know it even allows me to choose to use ';' to separate parameters instead of the old, ugly '&'. Unfortunately, I end up disappointed to find that CGI.pm only knows how to construct URLs to the current page.
So, CGI also stopped doing URL escaping itself, now delegating that feature to URI::Escape. Maybe it knows how to construct URLs? No.
Clearly, URI knows how to construct URLs. Of course it does. Sadly, it doesn't appear to know anything about CGI parameters in a URL.
(Sidebar) Not that this is terribly shocking. Even JavaScript got this embarrassingly wrong "forever". JavaScript originally came with a function for URL-encoding strings called escape(). It didn't actually do it right. You should probably never use it.
JavaScript 1.5 add encodeURI(). It appears to be designed to be used in a manner that encourages encoding bugs. You should probably never use it. 1.5 also added the awkwardly-named encodeURIComponent()... which actually does URL encoding correctly. Of course, you still have to roll your own iterating and concatenating and inserting the delimiters.
A language designed for working in web pages doesn't actually come with a feature that will build a URL with parameters. (But it does include the ability to convert numbers to base 11 and atan2(), of course.)
And JavaScript isn't the only example. There have been a lot of web projects that I've dived into to find URLs being constructed with the equivalent of "$page?acct=$acct;phone=$phone". Hope that phone number isn't, for example, "+44 20 773 1234".
Even PerlMonks was such a "web project". Remember when replying to a node whose title contained a double quote didn't work right (and numerous other similar bugs)?
So, I roll these pieces together and get:
package LinkToMason; use strict; use CGI(); use URI::Escape(); sub escape_url { my( $class, $string ) = @_; # Escape using default except don't escape '{' nor '}': return URI::Escape::uri_escape( $string, "^A-Za-z0-9\\-_.!~*'(){}" + ); } sub as_url { my( $class, $page, $rel, %args ) = @_; if( $page !~ m(^/) ) { require Carp; Carp::croak( "..." ) if ! $rel; $rel =~ s{/[^/]*$}{}; $page = "$rel/$page"; } return $page if ! %args; return "$page?" . join ';', map { join '=', map $class->escape_url($_), $_, $args{$_} } sort keys %args; } sub html_link { my( $class, $title, $page, $args, $attrs, $rel ) = @_; $args ||= { }; $attrs ||= { }; if( ref $title ) { $title = $$title; # \ '<bold>Real</bold> HTM +L' } else { $title = CGI->escapeHTML( $title ); # Non-HTML string } my $url = $class->as_url( $page, $rel, %$args ); return CGI->a( { href => $url, %$attrs }, $title ); } 1;
And try to use that in my Mason:
<%once> use LinkToMason; </%once> <%args> $acct_id </%args> <%shared> my %Context = ( acct => $acct_id, widget => $widget_id, ); </%shared> ... <& .link, "Edit $widget_name Settings", 'settings', %Context &> ... <%def .link><%perl> my( $title, $page, %args ) = @_; my $link = LinkToMason->html_link( $title, $page, \%args, { }, '/widget/', ); </%perl><% $link |n %></%def>
Note the gyrations to prevent .link from including newlines.
Okay, that is quite a bit uglier than I had hoped for. But it actually works.
But it quickly demonstrated how it wasn't very flexible when I tried to use it in a page that uses JavaScript to generate a list of links client-side (also changing it to not take a hash of parameters but instead just a comma-separated list of key names used to look up the parameter names and values that are ever used from this page):
<& .link, "Edit $widget_name Settings", 'settings', 'acct,widg' &> ... <script type="text/javascript"> ... + '<& .link, "Edit {{feature_name}}", 'edit', 'acct,widg,feat' + &>'
Where the new .link replaces the ',feat' with feature_id => '{{feature_id}}' and the JavaScript replaces '{{feature_name}}' and '{{feature_id}}' with values that vary between rows (one row generated per feature).
There is a risk that the second call to .link above would include something (a ', a \, or a newline) that wouldn't be legal inside of a JavaScript string. That sounds like a job for a Mason filter. I could define "|l" to strip newlines (a common desire when using Mason, it seems) and "|sq" to escape those problem characters.
Oh, except, as we already mentioned, you can't use something like "|sq" with <& ... &>.
I could define .sq that escapes the string passed to it. Ooh, I just found this syntax:
+ '<&| .sq &><& .link, ... &></&>'
That actually addresses (if in a manner still uglier than I had hoped) some of the questions I had when I started writing this.
What other features am I missing? How can I do this better?
- tye
|
|---|
| Replies are listed 'Best First'. | |
|---|---|
|
Re: Links between Mason components?
by Anonymous Monk on May 17, 2012 at 20:36 UTC | |
by tye (Sage) on May 18, 2012 at 03:00 UTC | |
by Anonymous Monk on May 18, 2012 at 03:49 UTC | |
|
Re: Links between Mason components?
by FloydATC (Deacon) on May 17, 2012 at 20:48 UTC | |
by tye (Sage) on May 18, 2012 at 03:54 UTC | |
by FloydATC (Deacon) on May 18, 2012 at 05:42 UTC |