Frontier Scripting Tutorial

Frontier is, among other things, a scripting system. It includes its own scripting language, UserTalk, that's similar to JavaScript, Java, and C. It's less like AppleScript and Visual Basic, but it has some similarities. It's also older than many of these languages.

You don't have to learn how to write scripts to make Frontier websites. But it's often helpful, particularly for complex websites.

This portion of the guide introduces you to scripting. It assumes you've written HTML and have worked through the website tutorial, but doesn't assume you've done any kind of scripting or programming.

This introduction to script is geared toward writing macros, outline renderers, and other scripts that are used in Frontier websites. It starts with the basics of scripting, then moves to the practical, scripts you can use right now.

1. What Is a Script?

2. Quick Script Window

3. Parameters

4. Calling Another Script

5. Variables

6. The Local Keyword

7. Assigning Values to Variables

8. Your First Macro: Addresses

9. Your Second Macros: Loops, Scope, and Strings

10. Your Third Macro: Files

11. Your Fourth Macro: the Page Table

12. Writing Filters

13. Writing Outline Renderers

14. Website Scripting Resources


 

1. What Is a Script?

A script is a set of instructions. The computer executes the instructions, then returns data, a value -- which could be a number, a string, a list, or another data type.

What is a string?

A string is a type of data: it's zero or more characters, it's a string of text.

What is a character?

A character is a single letter, digit, space, punctuation, carriage return, tab, or other code. Most characters can be typed via the keyboard.

'A' is a character, '5' is a character, '(' is a character.

"A5(" is a string -- it's three characters in a row.

Characters and strings are just two of the many types of data Frontier supports. These will be explained in more detail later.

The important thing to know is: scripts act on data, and there are many types of data.

A very simple script does nothing but return a value. To return a value, it uses the "return" keyword.

The word "return" is a word that UserTalk understands. It means: return this value to the script that called me.

A very simple script might look like this:


return ("This is a string.")

The return keyword is usually followed by open and close parentheses. What appears between the parentheses is what gets returned.

Returned Where?

A script is called -- usually by another script. In some cases a script may be called by the Frontier application itself. And of course you can run scripts yourself -- and those scripts may in turn call other scripts.

When a script is called, it runs and returns data to the caller.



 

2. Quick Script Window

One of the easiest ways to run a short script is to use the Quick Script window. You can open it by choosing the Quick Script command from the Main menu. Or you can open it from the keyboard by typing ctrl-; (or command-; on Macintosh).

Go ahead and open it now. Delete what's in there, and type the short script in the example above, so it looks like this:

quickscript1.gif:

Quick Script window

Now click the Run button. This will call the script -- the text you typed in the window -- and get the result. The result will be displayed in the small pane below the script panel. Like this:

quickscript2.gif:

Quick Script window with returned value displayed.



 

3. Parameters

Scripts are often, but not always, called with one or more parameters.

Parameters are values, data, that a script will do something with, then return a result.

It's like a conversation: the caller makes a request, the script performs actions based on the request, then the script answers the caller.

Imagine a script that does one simple job: given some text, it answers with the number of characters in the text. (Or, a scripter might say: given a string parameter, the script returns the number of characters in the string.)

If the caller sent the string parameter "Hello, Script!" to the script, the script would answer with a number, 14, the number of characters in "Hello, Script!"



 

4. Calling Another Script

The previous example -- a script that tells how long a string is -- is something you might want to do sometimes. Frontier includes a built-in script to do just that.

Here's how your script can call it. Open your Quick Script window, delete what's in there now, and type:

string.length ("Hello, Script!")

Click the Run button. In the result pane you'll see the number 14.

Your script -- what you typed in the Quick Script window -- called another script, string.length, with a parameter: "Hello, Script!"

The script your script called then counted the characters in "Hello, Script!" and returned the result, which the Quick Script window displayed for you.

This is the sytax to use when calling another script. First is the name of the script, then left parenthesis, then any parameters you want to pass to the script, then right parenthesis. In other words, the parentheses contain the parameters.

If you're not passing a parameter or parameters, the parentheses are empty. If you're passing two or more parameters, the parameters are separated by commas.

Try running this in the Quick Script Window:

string.mid ("Hello, Script!", 1, 5)

(Tip: If you're reading this in your web browser, you can copy-and-paste into the Quick Script window.)

String.mid extracts a section of a string. It takes three parameters: a string, followed by the starting character index (a number), then the count of how many characters to extract (another number).

DocServer

Frontier has hundreds of scripts like this: they're built in to Frontier, and they do something that lots of scripts might need to.

These built-in scripts are usually called verbs.

You can read all about them at the DocServer website: http://docserver.userland.com/



 

5. Variables

A variable is a container for data. A variable can contain any type of data that Frontier supports, including strings, characters, lists, numbers, and so on.

It's called a variable because its contents can change, they may vary, it's a variable.

Variables have names. Here's an example script that uses a variable named s.


local (s = "This is a string!")

s = "This is another string!"

return (s)

In the above script, the variable s contains "This is a string!" -- but then it changes value and contains "This is another string!" instead. Finally, the contents of s is returned to the calling script (if there is one).

You can see how variables can change. Create a new script by choosing New->Script from the File menu.

A new, empty script window will appear, like this:

newEmptyScriptWindow.gif:

New, empty script window.

Type (or copy-and-paste) the following script into the script window:


local (s = "This is a string!")

dialog.notify (s)

s = "This is another string!"

dialog.notify (s)

return (s)

To run the script, click the Run button at the top of the window.

You'll see a dialog box that displays the string "This is a string!" then a dialog box that displays the string "This is another string!"

The dialog.notify verb is used to display these dialog boxes. Dialog.notify takes one parameter, a string, that gets displayed in the window.

Note that the parameter being passed is a variable -- the variable s. You could have called it x or abc or xyz123: the name doesn't matter -- except that variable names should be only letters and numbers (no spaces), and should start with a letter.

Variable naming conventions

You didn't have to name the variable s -- but for strings, the variable s is often used. It's a convention. This means it's a practice that many people follow, and it makes reading code easier. Whenever you see a variable named s, it probably contains a string.



 

6. The Local Keyword

Notice the first line of the above script:


local (s = "This is a string!")

The "local" keyword tells Frontier that what's in parentheses is a new variable. It's a local variable, because only this script can see it.

Global variables are the opposite of local variables -- globals are data stored in an object database. More about that later.

A line like the above is also known as a declaration: the script declares there's a variable named s, and it gives it an initial value.

Always declare local variables!

While Frontier doesn't enforce this rule, it's very important that you declare your local variables. Always use the local keyword to let Frontier know about your local variables.

If you don't, your script may not work as expected. A worst-case scenario is that you might over-write something in an object database that you don't want to over-write.

Another way of writing the above line is to split into two lines:


local (s)

s = "This is a string!"

The first line simply declares that there is a local variable named s. The second line gives s a value.

You can also declare multiple local variables:


local (s, i, htmlText)

Note that they're separated by commas. You can also mix in initial values:


local (s = "This is a string!", i, htmlText = "<html><head>")



 

7. Assigning Values to Variables

You've seen in the above examples how to give a value to a variable. This is called assignment. The = sign is used for assignment, as in:


local (s)

s = "This is a string!"

Frontier allows variables to change types: s can be a string on one line, and then turn into a number the next line. As in:


local (s)

s = "This is a string!"

s = 7

Avoid changing types

In general, it's better programming practice not to change the type of a variable once it's set -- it's better if a variable that starts out as a string remains a string until the script is finished.

But: UserTalk is a scripting language. In this respect it's unlike Java, C++, and other strongly-typed languages. (A strongly-typed language is one that enforces that variables remain the type they are. You couldn't turn a variable from a string into a number the same way you can in Frontier.)

This is not a hard-and-fast rule, but it is something remember.

Variables can be added together. Like this:


local (x = 5)

x = x + 10

return (x)

The above script would return 15, the value of x at the end of the script. The variable x is first set to 5, then 10 is added to that, then the contents of x is returned.

With numbers you can subtract, multiply, divide, and so on. See the list of Frontier operators at the end of this site.



 

8. Your First Macro: Addresses

What is a macro?

The word macro refers to things. In a web page, it's something between curly braces, between { and }.

It's also the script that's called by the text inside the curly braces.

What does a macro do?

A macro inserts text in the web page. It can do anything it wants, but at the end it must return a string of text that replaces the curly braces and what's inside the curly braces.

Simple macro

Go back to the tutorial website. Expand the #tools table. Inside, create a new script, via New Script from the Table menu. Call the script myName.

Inside the script, indented one level under the on myName () line, type:

return (user.prefs.name)

Then click the Compile button. Always compile your scripts when you create a new one or make a change. If there are any syntax errors, an Error Info window will appear. If you click the Go To button in the Error Info window, the cursor will be positioned at the point of the error (or sometimes just after it). Rule of thumb: don't forget to compile.

Now, edit one of the web pages in the tutorial website. Add a line like this:

This is {myName ()}'s website!

Now preview the page. You'll see something like this:

mynamerendered.gif:

Tutorial page calling a macro

Walk-through

The top line of the script -- on myName () -- is the handler. This is a syntax for saying this is a script that takes no parameters.

If it did take parameters, it might look like this: on myName (s). That's how you'd set up a script that expects one parameter. If it expects two parameters, it might look like this: on myName (s, num). (The parameter names are just examples.)

It's an error to call a script with fewer or more parameters than it expects.

If a script takes no parameters, you can omit the handler. You could have deleted the top line, and had only one line -- return (user.prefs.name) -- and it would have worked the same.

The next line of the script should be familiar: it's a return statement, which means to send something back to the caller. In this case, the script is returning the value at user.prefs.name.

Object database values

"User.prefs.name" is a way of referring to an object inside Frontier.root, inside the object database. It's like a file path, except that instead of folders Frontier has tables, and instead of \ or : Frontier uses dots. (It's called dot notation, something you see in the Internet with domain names, or see in other programming languages.)

The value at user.prefs.name is the name item inside the prefs table that's in the user table. You can see this for yourself. Ctrl-double-click (Macs: command-double-click) the text "user.prefs.name" in your script to go right to it. You should see your name.

The Jump command

Another way to get around in Frontier is to use the Jump command. It's near the top of the Main menu. You can also type ctrl-J as a keyboard shortcut.

In the prompt, type the path to something you want to see in Frontier, such as user.html.glossary.

The Jump command is a command you'll probably use often.

user.prefs.name

All your macro is doing is returning the value at user.prefs.name, which is your name.

Addresses

Let's make the macro more complex to make a point about Frontier addresses.

Frontier paths are usually called addresses. Addresses are a particular data type in Frontier.

Here's an anology: an address like 123 Kelly Lane points to a house. The address is not the house, it just points to it.

In Frontier, user.prefs.name is the value at user.prefs.name. But an address begins with an @ symbol: @user.prefs.name is not the contents of user.prefs.name, it points to user.prefs.name. It's an address.

So, here's a complex, Rube-Goldberg-ish version of the myName macro:


on myName ()

   local (adr = @user.prefs.name)

   return (adr^)

The variable adr is an address pointing to user.prefs.name. It does not equal the contents of user.prefs.name.

The ^ sign in the return statement means: get me the contents of what this address points to. That's known as de-referencing.

You could have written this same exact script, but without the variable. It would have looked like:


on myName ()

   return ((@user.prefs.name)^)

The @ symbol means it's an address, and the ^ symbol means de-reference the address -- that is, get its contents.

The two things cancel each other out: the best way to write this is still: return (user.prefs.name).

Back to the house analogy: 123 Kelly Lane is an address, 123 Kelly Lane^ is the contents of the house at Kelly Lane.

Some people take a while to get this concept: it's probably the most difficult in all of Frontier scripting. It's presented early, so in the rest of the tutorial you'll see more examples.

Inserting object database values in web pages

You didn't really have to write a macro at all to add the value at user.prefs.name to your web page. You could have just as easily put this text in your web page: {user.prefs.name}. Frontier's macro processor would recognize that this isn't a script but a reference, and it would have inserted your name. There's lots of power there. Nevertheless, you'll see that are plenty of times you do want to write macros.



 

9. Your Second Macro: Loops, Scope, and Strings

String processing

This macro is going to get just your first name from the value at user.prefs.name.

It will do that by getting everything up to the first space. You'll see two ways of doing it, the hard way and the easy way.

The hard way

Create another script in the #tools table called myFirstName. It should look like this:

on myFirstName ()

    local (s, i)

    local (adr = @user.prefs.name) //the address of the name

    for i = 1 to sizeOf (adr^) //loop through every character in the name

        local (ch = adr^ [i]) //get the i'th character of the name

        if ch == ' ' //is this a space character?

            break //break out of this loop

        s = s + ch //add the character to s

    return (s) //return the first name to the caller

Walk-through

The first line is the handler. The second line declares two local variables, s and i. The third creates a variable named adr that contains an address, @user.prefs.name. Note the use of // -- this means that a comment starts here.

What is a comment?

A comment is some text inside a script that doesn't get executed. It's there for humans to read.

The reason to use comments is you might look at a script you wrote a year ago and not remember exactly what it's doing or how it does it. If you've included good comments, it'll be easy to understand.

The other reason is that sometimes someone else will want to look at one of your scripts. They might not have any idea how the script works -- unless you include comments that describe how the script works.

Including comments is a good idea, even if you don't think you'll forget how the script works, even if you don't think someone else will ever look at your script -- because the odds are you your comments will be useful at a later date.

The fourth line is the top statement in a loop. When you want an action to be performed repeatedly, use a loop. There are various kinds of loops: one of the most common is a "for" loop.

The way this kind of loop works: in the first pass, the variable is equal to 1. The second time it's equal to 2. The third time it's equal to 3. And so on.

The loop stops after i is equal to sizeOf (adr^). The expression sizeOf (adr^) means -- what is the size of the value whose contents are pointed to by the address that adr equals.

In other words, how many characters are in the string at user.prefs.name.

A loop can also break when it sees the break statement -- there is such a statement a few lines down. Sometimes you want to break a loop early, before it reaches the condition for terminating (in this case, that's sizeOf (adr^)).

In Frontier, everything under a loop is indented one level. Since a script is a type of outline, you can expand and collapse and re-organize as in outlines.

The first line inside the loop creates a variable local to that loop. It's scope is the loop.

What does scope mean?

It means where a variable is valid. A local variable declared inside a loop is valid only in that loop. It can't be seen -- it doesn't exist -- outside that loop. A local variable declared at the top of a script can be seen anywhere inside the script. It's scope is the entire script. An object in an object database can be seen by any script -- it's scope is global. User.prefs.name, for instance, is global: any script can see it.

That local variable -- ch (short for character) -- is set to equal one character, the character at position i in the string user.prefs.name. If your name is Bull Mancuso, the first time through ch is equal to B. The second time through it's equal to u. And so on.

The next line is an if statement, otherwise known as a conditional. If ch is a space -- if ch == ' ' -- then execute the line indented one line to the right. Otherwise, go on to the next statement at the same level.

Why the double equals sign?

If you're checking to see if something is equal to something else, use the == sign.

If you're setting something to be equal to something else, use the = sign.

The first case is an equality check, the second case is an assignment. Those are different things: one's a question, the other's an action.

That's why you use == sometimes and = other times, because they mean different things.

If ch is a space, then the next line is a break statement. That means: break out of this loop now, don't keep looping.

If the break statement is executed, then the next line executed is the return statement that appears right after the loop.

If the break statement is not executed -- if ch isn't a space yet -- then add ch to the variable s.

If your name is Bull Mancuso, here's what happens. The first time through the loop, s equals B. The second time, add a u to s, so s equals Bu. Then add the two l's, one at a time, so s equals Bull.

Finally, a space is encountered. In this case, break out of the loop.

The variable s equals Bull, and that's what's returned. Your first name probably isn't Bull -- so whatever your first name is gets returned.

This is an example of string processing: you've started with a string, the value at user.prefs.name, and returned a different string that's derived from that string.

The easy way

Luckily, Frontier provides a verb for this. That whole script could be re-written this way:

on myFirstName ()

    return (string.nthField (user.prefs.name, ' ', 1))

Look it up in DocServer: http://docserver.userland.com/string/nthField

Walk-through

Before the script returns, it first executes (evaluates) what's inside the parentheses.

The string.nthField verb takes three parameters: the string to process, the field delimiter, and the number of the field to get.

In this case, the first parameter is your name. The second parameter is a space character, and the third parameter is 1.

This means: get me everything up to the first space.

This same script could be written with variables, if it makes it easier to follow:

on myFirstName ()

    local (s)

    s = string.nthField (user.prefs.name, ' ', 1)

    return (s)



 

10. Your Third Macro: Files

Reading from files

In this macro you'll include the contents of a text file on disk in a web page.

Create a file

Open BBEdit or Notepad or SimpleText and create a text file and save it on your hard drive. It can say anything, but ideally for this example it would be a mixture of text and HTML.

Note the path

Remember where you've saved it. You'll need to know the path to the file, as in Macintosh HD:Documents:myTextFile or C:\myFiles\testing\myFile.txt.

Create a new script

Create a new script in your #tools table called insertFile. It should look like this:

on insertFile (f, flProcessMacros)

    local (s)

    s = string (file.readWholeFile (f)) //read the file as a string into s

    if flProcessMacros //should macros get processed?

        s = html.processMacros (s) //yes, process macros

    return (s) //return the string

Call it from your web page

Edit one of the pages in the tutorial site to call the insertFile macro. Your call might look something like: {insertFile ("D:\\Documents\\Foo.txt", false)}

Preview the page -- you'll see the contents of the file have been inserted.

Walk-through

The handler line expects two parameters. f is the path to the file on disk, and flProcessMacros is true or false -- it's a boolean. (Booleans are either true or false, that's all a boolean is.)

So when you called this from your page you provided two parameters: the path to the file to read, and either true or false as the second parameter.

The next line -- local (s) -- declares a local variable (you can guess it will be a string) whose contents will be returned at the end of this script.

The next line -- s = string (file.readWholeFile (f)) -- reads the entire file that you specified into the variable s.

File.readWholeFile is another Frontier verb. You can read about it: http://docserver.userland.com/file/readWholeFile

This verb expects one parameter, the path to the file to read.

Note the coercion to string: it's string (file.readWholeFile (f)), not just file.readWholeFile (f). This is because file.readWholeFile doesn't return a string, as files might contain anything. So we use this syntax to coerce the result to a string, because we want s to be a string. That's what coercion is: taking something of one type and changing the type. You can't always coerce any type to any type, but you can coerce many types to a string.

So now s contains the contents of the file as a string. We next check to see if flProcessMacros is true.

The line if flProcessMacros could be re-written as if flProcessMacros == true -- the two things mean the same thing.

Similarly, if you want to check a boolean to see if it's false, you might write if not flProcessMacros or if flProcessMacros == false. Again, it's the same thing. How you do it is up to you, but at UserLand we prefer the shorter form.

If flProcessMacros is true -- that is, if you passed true as the second parameter -- then we run s through Frontier's macro processor. This way you can call macros from the text in the file, and they'll get run. That's what the line s = html.processMacros (s) does. You can read about this verb, too: http://docserver.userland.com/html/processMacros

Finally, the contents of s is returned in the last line, via the return statement.

More about files

You can create and write files too. You can check if files exist, you can delete files, you can create and delete folders, and more. DocServer has lots of pages on Frontier's file verbs: http://docserver.userland.com/file/



 

11. Your Fourth Macro: the Page Table

What is the page table?

When a page is rendered, Frontier collects directives and information about the page and puts that information in a table called the page table.

Your macros have access to the page table. Imagine you have macro that wants to know the title of the current page being rendered. It can find out by looking in the page table.

If you want to see an example of what the page table looks like, expand the websites table in Frontier.root. Then expand the #data table. That's the page table for the last page you rendered. It's a good idea to get acquainted with what's in there.

The page table doesn't always contain the same elements. For instance, your site might have a #renderOutlineWith directive, and that would be placed in the page table when a page is being built. But your site might not have such a directive at all, and thus there would be no such entry in the page table.

But some things in the page table are always there, such as title, url, f, and so on.

Create a new macro

In your site's #tools table, create a new macro called insertByDirective. This macro will insert a file, as before, but instead of taking two parameters you will create your own directives that tell the macro what file to get and whether or not to process macros.

The script should look like this:

on insertByDirective ()

    local (pta = html.getPageTableAddress ()) //get the page table address

    local (f, s = "", flProcessMacros = false) //set up locals

    if defined (pta^.fileToInsert) //does the #fileToInsert directive exist?

        f = pta^.fileToInsert //set f equal to the file path

        if defined (pta^.processMacrosInInsertedFiles) //does the directive exist?

            flProcessMacros = pta^.processMacrosInInsertedFiles //set flProcessMacros

        s = string (file.readWholeFile (f)) //read the file as a string

        if flProcessMacros //process macros?

            s = html.processMacros (s) //yes, process macros

    return (s) //return the string

Edit a web page

In your tutorial site, edit a web page so that it contains two new directives at the top of the page and calls the new macro. Your page will look something like this:

#title "My Page"

#fileToInsert "Macintosh HD:myFile"

#processMacrosInInsertFiles true

This is my page. Here's the contents of a file.

{insertByDirective ()}

Walk-through

Note that this macro takes no parameters. It's getting it's information from the page table, from the custom directives you've created. (Custom directives means that these are your directives: other parts of Frontier won't look at these directives. Only you and your macro know what these are for. You can create custom directives any time you want.)

The first local gets the address of the current page table. It's not always at websites.["#data"], you don't know where it is. Always use html.getPageTableAddress () to get the address of the current page table.

There's a DocServer page: http://docserver.userland.com/html/getPageTableAddress

The second local statement creates f, which will hold the path to the file. It creates s and sets it to "", which means it's an empty string. It sets flProcessMacros to false.

The next line looks in the page table to see if the #fileToInsert directive is there. If not, then it skips to the end of this script, to the return statement. Since s is "" at this point, "" is what gets returned.

Since you have defined a #fileToInsert directive, the variable f gets set to its value on the next line.

After that, the script checks to see if #processMacrosInInsertedFiles is defined. If it is, and it's true, then flProcessMacros is true. If it's defined but false, then flProcessMacros is false.

If it's not defined, then flProcessMacros is false by default.

Next the contents of the file is read into s as a string, just like in the last macro.

Then, if flProcessMacros is true, then html.processMacros is called.

Finally, the contents of s is returned.

More about the page table

You can find out more about what's in the page table and what are the standard Frontier directives at the Frontier site: http://frontier.userland.com/



 

12. Writing Filters

What are filters?

Filter scripts act on the page at various times during the rendering process. You can sometimes do things with filters that are difficult to do with macros. For instance, you might want to replace every occurence of a word with another word. Or you might want to send email somewhere every time a certain page is rendered. And so on.

The #filters table

Look inside the #filters table in the tutorial website. There are three scripts: finalFilter, firstFilter, and pageFilter. Note that each script takes one parameter, adrPageTable, the address of the page table.

firstFilter

This script runs after the page table has been initially built but before directives in the page have been added to the page table. The page has not yet been rendered.

pageFilter

This script runs after directives have been gathered from the page and added to the page table.

The page has not had the template added to it yet, and no macro processing has yet happened.

By default, this filter calls html.addPageToGlossary to add the current page to the glossary for this site.

To access the text of the page before it's been rendered, refer to adrPageTable^.bodyText.

finalFilter

This filter is called last, after the page has been entirely rendered. The rendered text of the page is in the page table: refer to adrPageTable^.renderedText.

Writing a simple filter

Open the finalFilter script. A simple filter will replace every occurence of the word foo with the word bar.

Edit the script to look like this:

on finalFilter (adrPageTable)

    local (s = adrPageTable^.renderedText) //get the rendered text into s

    s = string.replaceAll (s, "foo", "bar") //replace all occurences of foo with bar

    adrPageTable^.renderedText = s //replace rendered text with s

    return (true) //filters always return true

Walk-through

The first and last lines in a filter script should never change. Filters always take the address of the page table as a parameter, and they always return true.

The local declaration sets the variable s to the contents of the rendered text, which are stored in the page table.

The next line calls string.replaceAll -- http://docserver.userland.com/string/replaceAll -- to replace all occurences of foo with bar in s.

The next line sets the rendered text equal to the contents of s.

That's it -- any time a page in this site has the word foo, the filter will change that to bar. You can try it out: edit one of the pages in the tutorial site to include the word foo, preview the page, and verify that it now says bar where you typed foo.

Since web pages are strings, the string verbs are very useful in filter scripts. DocServer has lots of pages on the string verbs: http://docserver.userland.com/string/



 

13. Writing Outline Renderers

What are outline renderers?

In the website tutorial you learned how to use outline renderers, you learned that an outline could get rendered many different ways, depending on the renderer used.

Now it's time to write one.

First you might want to take a look at the user.html.renderers table. Most outline renderers that ship with Frontier are stored in this table. You can also add your own: the user table is yours to add things to. You can also store renderers with your websites in the #tools table. That way if you move your website to another machine your outline renderers will go with the site.

Simple renderer

Let's write a renderer that adds an <hr> before every top-level headline and surrounds the text in top-level headlines with <h3> and </h3> tags. This may not be something you ever want to do in particular, but this renderer makes a great template for writing other renderers.

Create a script in your #tools table called myRenderer. In the outline that's in your tutorial website, set the #renderOutlineWith directive to "myRenderer, " as in: #renderOutlineWith "myRenderer"

Type the script

The script should look like this:

on myRenderer (adrOutline)

    local (level = 0) //the top level is level 0

    local (htmlText) //this is the text that will be returned

    on add (s) //subroutine for adding to htmlText

        htmlText = htmlText + s //add s to htmlText

    op.firstSummit () //go to the top of the outline

    op.fullExpand () //completely expand the outline

    on visit ()

        loop

            local (s = op.getLineText ()) //get the current line of text

            if level == 0 //is this a top-level item?

                s = "<hr><h3>" + s + "</h3>" //add the hr and h3 tags

            else

                s = s + "<br>" //add a br tag

            add (s) //add the line from the outline to htmlText

            if op.go (right, 1) //there are lines indented under this one

                level++ //incremement the level by one

                visit () //loop through the items at this level

                level-- //decrement the level by one

                op.go (left, 1) //go back to the line before visit was called above

            if not op.go (down, 1) //go straight down one line

                break //couldn't go straight down, break

    visit ()

    return (htmlText)

Walk-through

All outline renderers take the address of the outline to render as their sole parameter. This address doesn't point to the original outline, but to a copy that's had its directives removed.

The first local declaration sets the level count at 0. This variable will contain a number, the level of the current line. What an outline renderer does is start at the top of an outline and work its way down, line-by-line. Top-level headlines are at level 0, lines indented one line are at level 1, and so on.

Then the script declares a variable htmlText, and sets up a subroutine that allows you to add text to htmlText. (A subroutine is a script within a script.)

The call to op.firstSummit () gets the cursor to the top of the outline. The call to op.fullExpand () expands the outline completely.

These and other op (outline processor) verbs are documented in DocServer: http://docserver.userland.com/ (By the way, op is pronounced "oh pee," not "ahp.")

Then there's the visit subroutine, where the bulk of the work is done. Let's skip that and go to the bottom of this script. First it calls visit (), where the outline is rendered, then it returns htmlText.

The visit subroutine loops through the outline.

The first thing it does is read the contents of the current line into s, by calling op.getLineText.

Then it checks to see if the current level is 0. If so, then hr and h3 tags are added to s. Otherwise, a br tag is appended to s.

Then s is added to htmlText, by the call to the add subroutine.

Next the renderer tries to go right one line. If it can, then that means there are lines indented under this line. So then the level count gets incremented and visit () is called again. (This is known as recursion, when a script or subroutine calls itself.) Once visit returns, there are no more unprocessed lines to the right of the current line, so the level count is subtracted from (decremented). Then the script sends the cursor back to the line it was on before visit was called a few lines above.

Finally, the script tries to go down one line to the next line by calling op.go (down, 1). If it fails, there are no more lines at this level, and so the break statement terminates the loop.



 

14. Website Scripting Resources

Where to learn more

DocServer is the place to read about Frontier's built-in verbs. Each verb get its own page, and the pages are consistently formatted.

http://docserver.userland.com/

The Frontier 5 User's Guide contains chapters about writing Frontier scripts. This users guide was written before Frontier's website framework was developed, but it's still very useful. You can find it at the Frontier site: http://frontier.userland.com/

The chapter called Writing Frontier Scripts is most useful because it introduces you to Frontier's built-in debugger, which takes the guesswork out of finding errors in your scripts.

Finally, there is lots of sample code everywhere. Frontier.root itself is full of scripts. There are samples on various websites. Once again, a great starting place is the Frontier site.



Copyright 1992-2002 UserLand Software, Inc..