The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

Apache::AxKit::Language::XSP - eXtensible Server Pages

DESCRIPTION

XSP implements a tag-based dynamic language that allows you to develop your own tags, examples include sendmail and sql taglibs.

DESIGN PATTERNS

Writing your own taglibs can be tricky, because you're using an event based API to write out Perl code. These patterns represent the things you may want to achieve when authoring a tag library.

1. Your tag is a wrapper around other things.

Example:

  <mail:sendmail>...</mail:sendmail>

Solution:

Start a new block, so that you can store lexical variables, and declare any variables relevant to your tag:

in parse_start:

  if ($tag eq 'sendmail') {
    return '{ my ($to, $from, $sender);';
  }

Often it will also be relevant to execute that code when you see the end tag:

in parse_end:

  if ($tag eq 'sendmail') {
    return 'Mail::Sendmail::sendmail( 
            to => $to, 
            from => $from, 
            sender => $sender 
            ); }';
  }

Note there the closing of that original opening block.

2. Your tag indicates a parameter for a surrounding taglib.

Example:

  <mail:to>...</mail:to>

Solution:

Having declared the variable as above, you simply set it to the empty string, with no semi-colon:

in parse_start:

  if ($tag eq 'to') {
    return '$to = ""';
  }

Then in parse_char:

sub parse_char { my ($e, $text) = @_; $text =~ s/^\s*//; $text =~ s/\s*$//;

  return '' unless $text;

  $text =~ s/\|/\\\|/g;
  return ". q|$text|";
}

Note there's no semi-colon at the end of all this, so we add that:

in parse_end:

  if ($tag eq 'to') {
    return ';';
  }

All of this black magic allows other taglibs to set the thing in that variable using expressions.

3. You want your tag to return a scalar (string) that does the right thing depending on context. For example, generates a Text node in one place or generates a scalar in another context.

Solution:

use start_expr(), append_to_script(), end_expr().

Example:

  <example:get-datetime format="%Y-%m-%d %H:%M:%S"/>

in parse_start:

  if ($tag eq 'get-datetime') {
    start_expr($e, $tag); # creates a new { ... } block
    my $local_format = lc($attribs{format}) || '%a, %d %b %Y %H:%M:%S %z';
    return 'my ($format); $format = q|' . $local_format . '|;';
  }

in parse_end:

  if ($tag eq 'get-datetime') {
    append_to_script($e, 'use Time::Object; localtime->strftime($format);');
    end_expr($e);
    return '';
  }

Explanation:

This is more complex than the first 2 examples, so it warrants some explanation. I'll go through it step by step.

  start_expr(...)

This tells XSP that this really generates a <xsp:expr> tag. Now we don't really generate that tag, we just execute the handler for it. So what happens is the <xsp:expr> handler gets called, and it looks to see what the current calling context is. If its supposed to generate a text node, it generates some code to do that. If its supposed to generate a scalar, it does that too. Ultimately both generate a do {} block, so we'll summarise that by saying the code now becomes:

  do {

(the end of the block is generated by end_expr()).

Now the next step (ignoring the simple gathering of the format variable), is a return, which appends more code onto the generated perl script, so we get:

  do {
    my ($format); $format = q|%a, %d %b %Y %H:%M:%S %z|;

Now we immediately receive an end_expr, because this is an empty element (we'll see why we formatted it this way in #5 below). The first thing we get is:

  append_to_script($e, 'use Time::Object; localtime->strftime($format);');

This does exactly what it says, and the script becomes:

  do {
    my ($format); $format = q|%a, %d %b %Y %H:%M:%S %z|;
    use Time::Object; localtime->strftime($format);

Finally, we call:

  end_expr($e);

which closes the do {} block, leaving us with:

  do {
    my ($format); $format = q|%a, %d %b %Y %H:%M:%S %z|;
    use Time::Object; localtime->strftime($format);
  }

Now if you execute that in Perl, you'll see the do {} returns the last statement executed, which is the localtime-strftime()> bit there, thus doing exactly what we wanted.

Note that start_expr, end_expr and append_to_script aren't exported by default, so you need to do:

  use Apache::AxKit::Language::XSP 
        qw(start_expr end_expr append_to_script);

4. Your tag can take as an option either an attribute, or a child tag.

Example:

  <util:include-uri uri="http://server/foo"/>

or

  <util:include-uri>
    <util:uri><xsp:expr>$some_uri</xsp:expr></util:uri>
  </util:include-uri>

Solution:

There are several parts to this. The simplest is to ensure that whitespace is ignored. We have that dealt with in the example parse_char above. Next we need to handle that variable. Do this by starting a new block with the tag, and setting up the variable:

in parse_start:

  if ($tag eq 'include-uri') {
    my $code = '{ my ($uri);';
    if ($attribs{uri}) {
      $code .= '$uri = q|' . $attribs{uri} . '|;';
    }
    return $code;
  }

Now if we don't have the attribute, we can expect it to come in the <util:uri> tag:

in parse_start:

  if ($tag eq 'uri') {
    return '$uri = ""'; # note the empty string!
  }

Now you can see that we're not explicitly setting $uri, that's because the parse_char we wrote above handles it by returning '. q|$text|'. And if we have a <xsp:expr> in there, that's handled automagically too.

Now we just need to wrap things up in the end handlers:

in parse_end:

  if ($tag eq 'uri') {
    return ';';
  }
  if ($tag eq 'include-uri') {
    return 'Taglib::include_uri($uri); # execute the code
            } # close the block
    ';
  }

5. You want to return a scalar that does the right thing in context, but also can take a parameter as an attribute or a child tag.

Example:

  <esql:get-column column="user_id"/>

vs

  <esql:get-column>
    <esql:column><xsp:expr>$some_column</xsp:expr></esql:column>
  </esql:get-column>

Solution:

This is a combination of patterns 3 and 4. What we need to do is change #3 to simply allow our variable to be added as in #4 above:

in parse_start:

  if ($tag eq 'get-column') {
    start_expr($e, $tag);
    my $code = 'my ($col);'
    if ($attribs{col}) {
      $code .= '$col = q|' . $attribs{col} . '|;';
    }
    return $code;
  }
  if ($tag eq 'column') {
    return '$col = ""';
  }

in parse_end:

  if ($tag eq 'column') {
    return ';';
  }
  if ($tag eq 'get-column') {
    append_to_script($e, 'Full::Package::get_column($col)');
    end_expr($e);
    return '';
  }

6. You have a conditional tag

Example:

  <esql:no-results>
    No results!
  </esql:no-results>

Solution:

The problem here is that taglibs normally recieve character/text events so that they can manage variables. With a conditional tag, you want character events to be handled by the core XSP and generate text events. So we have a switch for that:

  if ($tag eq 'no-results') {
    $e->manage_text(0);
    return 'if (AxKit::XSP::ESQL::get_count() == 0) {';
  }

Turning off manage_text with a zero simply ensures that immediate children text nodes of this tag don't fire text events to the tag library, but instead get handled by XSP core, thus creating text nodes (and doing the right thing, generally).

<xsp:expr> (and start_expr, end_expr) Notes

Do not consider adding in the 'do {' ... '}' bits yourself. Always leave this to the start_expr, and end_expr functions. This is because the implementation could change, and you really don't know better than the underlying XSP implementation. You have been warned.