Skip to content
This repository has been archived by the owner on Jan 16, 2021. It is now read-only.
cmccall edited this page Oct 2, 2012 · 4 revisions

The OKWS "Pub" Language

The OKWS publishing system is a way for programmers to separate their C++ code and their HTML, and to keep the HTML on the file system where it's most convenient to manage. There are two sides to this system, then:

  • C++ bindings - in your OKWS services, you'll use these to read HTML templates off the file system, parse them, and publish them.
  • The templates themselves - OKWS templates look a lot like HTML, JS, CSS, or whatever else you're trying to publish, but with some important differences and enhancements.

The first half of this document covers the template language (its enhancements), and the second half covers the C++ bindings.

Template Language

Understanding HTML Mode and Pub Mode

All the text in your pub documents can be classified into two regions. We say those regions are in HTML Mode and Pub Mode. HTML mode is where you find normal HTML and variable substitution. Pub mode is where you find all advanced pub logic.

Pub Mode is activated with the {% .. %} closing markers. For example:

This is some normal text
{% 
     include("somefile1.html")
     include("somefile2.html")
%}

Inside pub mode, some commands -- covered later in detail -- switch back into HTML mode, using the double curly brace markers.

{%
   if(x==1) {{ This is some normal text }}
   include("somefile.html")
%}

The HTML mode inside the double curly braces is just as powerful as the top-level HTML mode. So it, too, can switch back into Pub Mode, with yet another {% .. %} region. You can nest arbitrarily deep:

{%
    if(x==1) {{
         This text will be output if x == 1.
         {%  if (y==1) {{ This text will be output if x == 1 and y == 1. }} %}         
    }}
    include("somefile.html")
%}

Variable Substitution

Inside HTML Mode,

%{X}

is interpreted as a variable that should be resolved at runtime. A practical example:

Welcome home, %{screenname}. You're %{age} years old.

Inside Pub mode, variables should not be wrapped in %{}.

{%
    if (age > 18) {{ You're an adult because you're %{age} years old. }}
%}

Variables can be set either in the C++ side of things -- passed along in calls to pub -- or elsewhere in the HTML templates, which we'll get to in a little while.

Variables as objects

In earlier versions of pub, a template var could only be resolved to a string, an integer, a float, or NULL (when unset). However, in pub3 a var may be any of those things, or an array of other vars, or a dictionary of key-value pairs, where the key is a string, and the value is another var. Those familiar with JavaScript Object Notation or Python should be at home with this kind of object representation:

user : {
     name : "maxwell",
     age : 22,
     emails : ["supermax@maxk.org", "submin@maxk.org"],
     pi : 3.14
}

In pub3 %{user} might very well be an object such as the one above. If you try to print %{user} in your HTML, Pub will output object notation as exampled above. However, most likely you'll want to use the components, like so:

Hi, %{user.name}, welcome back.  
Your primary email address is %{user.emails[0]}.  
On your world, pi is ${user.pi}.

With associative arrays, you can use either dot notation or array notation:

Hi, %{user["name"]}, a.k.a., %{user.name}.

Later in this doc we'll cover looping (iterating) over arrays.

Including Other Templates

Templates can include each other much like the old Server-Side Includes (SSIs) of yore. The syntax is:

{% include (<filename> [, dictionary assignments ]) %}

Here's a simple example:

{% include("header.html") %}

And here's an example that sets a var in the included file:

{% include("header.html", { bodystyle : "old-fashioned" }) %}

Inside header.html, we might see something like this:

<body class="%{bodystyle}">

What's going on here? The include command asks the runtime system to suck in the file subfile.html (in the same directory as the current template) and while so doing, to substitute all instances of %{bodystyle} for ``old-fashioned'' in the included template. Of course, templates can be nested, and there are checks at runtime to make sure there are no circular inclusions. Note it's possible to have assignments of the form:

{% include("subfile.html", { "X" : "some%{foo}bar" }) %} 

That is, the value half of a name-value pair can have interesting resolutions in it too. Also useful, the filename of the include can also be dynamic:

{% include("subfile.%{LANG}.html", { "X" : "some%{foo}bar" }) %} 

This is useful, for instance, when displaying pages in different languages based on user preferences, etc.

Conditionals

The ''If'' Statement

Pub supports the if statement, which expects a series of conditionals and then output. It's best thought of as a series of if, else, else, etc. statements:

   {% if (cond1) {{ output1 }}
      elif   (cond2) {{ output2 }}
      elif   (cond3) {{ output3 }}
      ...etc. 
      else {{ output4 }}
     %}

As soon as a condition is met, the appropriate pub code is executed, and the if statement ends. Here's an example:

   {% if   (user.age < 18) {{ Major burns, minor! }}
      elif (user.age < 22) {{ Welcome to college. }}
      elif (user.age < 30) {{ Get a job! }}
      elif (user.age > 65) {{ Relax... }}
      else {{ You're working for the man. }} %}

A default case (collected with ''true'' above) is not necessary.

Boolean logic is supported in conditionals:

{% if (user.age < 18 && user.gender == "female") {{ Jailbait! }} %}

Boolean operators can be strung together, and order of operation can be controlled through parentheses:

{% if ( (a < 12 && b >= 10) || c == "foo") {{ Awesome. }} %}

Vars that are NULL (i.e., they haven't been set or failed a lookup) will fail comparisons and make messy warnings. You can test whether a variable is null without generating a warning:

{% if (isnull(A)) {{ "A" is not set. }} %}

Double Curly Braces or Single Curly Braces in Conditionals

Double curly braces switch you back to HTML mode:

{% if (is_logged_in) 
   {{ You are logged in!!! }} %}

Single curly braces keep you in Pub Mode:

{% if (is_logged_in) {
      include("logged_in_header.html") 
   } %}

Of course you can still switch modes inside either:

{% if (is_logged_in) 
   {{ You are logged in!!! 
         {% include ("logged_in_header.html") %}
   }} %}

Declaring Variables with globals {}

You can update or create a var within a template with the ''globals'' construct.

{% globals { <assignment1> [,<assignment2>, <assignment3>,... } %}

For example, to set a site_url var:

{% globals { site_url : "http://www.okcupid.com" } %}

This sets two different vars, age and screenname, in one call:

{% globals { age : 10, screenname : "Dr. Who" } %}

And, since variables can be much more complex than just floats, integers, and strings, you might be wondering how to set a big object. It looks like like the object notation at the top of this page, with a set around it:

{% globals { 
     user : {
        name : "maxwell",
        age : 22,
        emails : ["supermax@maxk.org", "submin@maxk.org"],
        pi : 3.14
     }
%}

You can make right-hand assignments that are objects, too.

{% globals {
      old-fashioned : {
         color : "cornsilk",
         font-size : "2.0em"
     }
   globals {
       user : {
          name : "maxwell",
          age : 22,
          template : old-fashioned
      }
%}

Strings can be expanded at run-time:

{% globals { 
      user : {
         name : "Maxwell",
         usa_address : "%{street}\n%{city}, %{state} %{zip}"
     }
%}

Looping

The most powerful addition to pub is the shift from simple scalar vars to full-blown objects. An OKWS service can provide a complicated object or array of objects as a var, and pub can handle it nicely. For example, let's say a var %{buddies} has been populated with the following data, in object notation:

buddies : [
    {name : "Adam", age : "20", gender : "m" },
    {name : "Bill", age : "24", gender : "m" },
    {name : "Caty", age : "25", gender : "f" },
    {name : "Debb", age : "26", gender : "f" }
]

We can print a nicely formatted list of them like so:

{% for (buddy, buddies) {{
      <li>%{buddy.name} is online. 
          {% if (buddy.gender == "m") {{ <a href="#">Send him a message</a> }}
             else                     {{ <a href="#">Send her a message</a> }}
          %}
       </li>
   }}
%}

Don't forget you can include another HTML file inside this loop. You can also pass vars to it, even ones that are objects and local to the loop:

{% for (buddy, buddies) {
       include("person_display.html", { person_to_display : buddy } ) 
   } 
%}

Scope

When you set a var, by default that variable is scoped globally. The key here is that a parent document will get the variable too, as well as any brother or cousin documents, after the set happens.

For example, imagine a parent file:

{%
   globals { x : 10 }
   include ("child.html")
   print "2:%{x}"
   include ("brother.html")
%}

And the child file ("child.html") is:

{%
   globals { x : 20 }
   include ("grandchild.html")
%}

And the brother file ("brother.html") is:

{%
   print "3:%{x}"
%}

And the grandchild file ("grandchild.html") is:

{%
   print "1:%{x}"
%}

This above example will print 1:20 2:20 3:20, since the setting of x to 20 in the child document will persisnt into the grandchild, and also persist past the end of the child document, into the parent's scope, and subsequently into any files included by the parent (like "brother.html").

Now image we changed the child file to use locals rather than globals. Then, the effects of setting x will be available in the file that does the set and all of its descendants, but won't propagate to parents and brothers. The effect of publishing the parent file would be "1:20 2:10 3:10".

Passing vars inside an include is equivalent to putting a locals statement at the top of the include:

{%
   // Locally set age to 20 *inside* foo.html
   include("foo.html", {age : 20})
%}

For obvious reasons, assignments inside for loops are equivalent to locals and therefore only work inside the loop:

{% for(buddy,buddies) {{
      buddy.name
}} %}
<!-- the following will be null -->
%{buddy.name}

Dictionary files and the load command

Because assignments are global, it works to include files that have a series of global statements inside them.

{% include("country_facts.html") %}
The center of mass of the USA is %{usa.center_of_mass}.

By convention, you might like to name such a file with the .dict extension, although that's optional.

To suppress all output from the include -- such as unwanted whitespace (especially important before a doctype tag in HTML) -- use the load command, which is identical to include sans output:

{% load("site_vars.dict" %}<?doctype ... bleah bleha bleah.

It's good style to use load and .dict if you know you're simply setting global vars inside an include.

Comments and language tagging

Anything inside triple square brackets in pub is stripped. Gone are the days of HTML comments your users can read.

   <div>
   [[[ Here's something no one will ever see. ]]]
   </div>

Comments inside commands are not currently allowed, but use C-style comments for those:

  {% globals { foo1 : "bar",
                foo2 : "bar2", /* this is a comment (ERROR!) */
                foo3 : "bar3" } %}

Double square brackets are removed, leaving their contents in place. This can be helpful for tagging plain-language content inside HTML for translators to find:

   <h1>[[Welcome back, %{screenname}.]]</h1>

Double square brackets inside of a double square brackets are stripped. This feature is used by OkCupid.com in its translation software; the inner comments are shown to translators.

   <a href="#">[[ [[ Here "log" means a document, not a felled tree. ]] View the log]]</a>

The above simply outputs:

   <a href="#">View the log</a>

Arithmetic operators

   {% if (1 < 20)         {{ 1 < 20 }}
      if (-5 == 1 - 6)    {{ -5 == 1 - 6 }} else {{ bad 2}}
      if (!(1 >= 20))     {{ ! 1 >= 20 }}
      if (20 + 30 > 40)   {{ 20 + 30 > 40 }}
      if (30 != -30)      {{ 30 != -30}} %}

Caveat: due to parsing difficulty, currently len(str)-1 will give you a syntax error, but len(str)- 1 will work, or you can try len(str) - 1.

Regular expressions

Pub3 has support for perl-compatible regular expressions, supporting most fancy features you use. Regular expressions can be specified with either simple string syntax:

{% locals { regex : "a+b?c+" } %}

or special regular-expression syntax if that floats your boat:

{% locals { regex1 : r{a+b?c+}, regex2 : r/a+b?c+/i, regex3 : r#^a+b?c+$#g, regex4 : r[a+b?c+] } %}

and so forth. As in perl, pub3 allows delimiting regular expressions with many symbols. At the end of the day, the characters you use as delimiters don't really matter, they're just referred to in the parsing step. Also as in perl, you can give commands to the regex after the closing delimiter. Once you have a regular expression, you can feed it to the ''match'' or ''search'' functions:

{% if (match (regex, "aabccc")) {{ "should print!" }} %} {% if (match (regex1, "aabcc")) {{ "me too!" }} %}

''match'' and ''search'' are largely equivalent, except ''match'' looks to make sure that the whole string is matched, while ''search'' will be happy to find your regex anywhere in the given string. Of course, no need for variable assignments, you can call match directly with a regular expression:

{% if (match (r/a+b?c+/i, "aabccc")) {{ "should print!" }} %} {% if (search ("a+b?c+", "XaabccY")) {{ "me too!" }} %}

Finally, 'match' and 'search' come in two different prototypes. The first, we've already seen, take two arguments:

  • match(/regex/, /text/)
  • search(/regex/, /text/)

The other takes three arguments, the middle argument being the /options/ to feed to regular expression matcher:

  • match(/regex//, /options/, /text/)
  • search(/regex//, /options/, /text/)

Filters and Function Calls

All filters are available via the standard syntax:

toupper (html_escape (""))

or via a Django-inspired "filter" syntax:

""|html_escape()|toupper()

or more succinctly:

"<tag>"|html_escape|toupper

If calling with the Django-style filter syntax, then the value coming through the filter is the /this/ parameter, the first argument to the function.

You can call the following functions and filters from Pub templates MK NOTE Need pointer to DOCS

JSON

If ever you want to use OKWS to output JSON, don't handroll your own:

if (do_json) {{
  {
     "my_foo" : %{foo|json},
     "my_bar" : %{bar|json},
  }
}}

This is what I call "hand-rolling JSON" and there's a much better way in pub:

if (do_json) {
    locals { tmp : { my_foo : foo, my_bar : bar } }
    print tmp
}

There are also nice techniques for putting the results of including a file into your JSON object. Instead of this:

if (do_json) {{
    {
         "file_out" : {% include ("x.html") %}
    }
}}

You can do this:

if (do_json) {
     locals { file_out : {} }
     load ("x.html", { ret : file_out })
     print ( { file_out : file_out })
}

Then in x.html, you store whatever values into the local variable 'ret':

ret["dog"] = 10;
ret["cat"] = [1,2,3];

And the output json will give you:

{ "file_out" : { "dog" : 10, "cat" : [1,2,3} }