Wednesday | 28 FEB 2024
[ previous ]
[ next ]

How to Use the BASIC Templating Language

Title:
Date: 2023-08-28
Tags:  pick, basic

The BASIC Templating Language is written in Pick BASIC for multivalue database environments. It let's you use BASIC code inside templates which removes the need to learn a custom templating language.

What is a templating language?

A templating language is a language that is used to generate strings. A templating engine will combine a template with data to form a fully rendered page.

Requirements

The BASIC Templating Language has a requirement to use my implementation of hashmaps as that is how the data is set up and passed into the template.

Hashmaps Implementation

Installation

The BASIC Templating Language is made up of three programs. You can download the files individually and compile and catalog them.

   wget https://raw.githubusercontent.com/Krowemoh/basic-template-language/main/RENDER
   wget https://raw.githubusercontent.com/Krowemoh/basic-template-language/main/RAW.RENDER
   wget https://raw.githubusercontent.com/Krowemoh/basic-template-language/main/EVALUATE.BTL

You can also use my package manager, NPM, to install the 3 routines:

   NPM INSTALL BP RENDER
   NPM INSTALL BP RAW.RENDER
   NPM INSTALL BP EVALUATE.BTL

Using RENDER

Only the RENDER program is expected to be used, the RAW.RENDER and EVALUATE.BTL programs are used internally.

   SUBROUTINE RENDER(MAT ENV,ENV.SIZE,TEMPLATE,RESULT)

The RENDER routine takes an environment variable that contains all the data the template needs, it takes the size of the environment, it takes the template to render and finally it returns a fully formed result string.

An example program:

*
   EQU ENV.SIZE TO 5000
   DIM ENV(ENV.SIZE)
   MAT ENV = ''
*
   CALL MAP.SET(MAT ENV,ENV.SIZE,'NAME','Nivethan')
   TEMPLATE = 'Hello, {{ NAME }}!'
*
   CALL RENDER(MAT ENV,ENV.SIZE,TEMPLATE,RESULT)
*
   PRINT RESULT
*
*
   END
*

This program sets the variable NAME to my name. The template string references NAME inside the curly braces and so this will get evaluated in the RENDER routine.

The result will be:

Hello, Nivethan!

The TEMPLATE variable should really be a record in the HTML-TEMPLATE-file. However to keep everything simple for the examples I will put the templates directly in the BASIC program.

BASIC Templating Language

A template string will contain both strings and BASIC code, BASIC code needs to be embedded within curly braces so that the RENDER routine knows what needs to get evaluate and what is really a string.

The templating language is what get's evaluated by the RENDER routine. A template can contain the following:

  • Variables
  • Assign variables
  • Loops
  • Conditionals
  • Print during rendering
  • Include code from other files
  • Open files
  • Read from files
  • Built in functions: DCOUNT, LEN, OCONV, MOD, DATE, NOT
  • Call external subroutines to get data (limited to 1 parameter)

Variables

As in the example above, the simplest part of the templating language is simply showing a variable.

*
   EQU ENV.SIZE TO 5000
   DIM ENV(ENV.SIZE)
   MAT ENV = ''
*
   CALL MAP.SET(MAT ENV,ENV.SIZE,'NAME','Nivethan')
*
   TEMPLATE = 'Hello, {{ NAME }}!'
*
   CALL RENDER(MAT ENV,ENV.SIZE,TEMPLATE,RESULT)
*
   PRINT RESULT
*
   END
*

Results in:

Hello, Nivethan!

You can also run BASIC code.

*
   EQU ENV.SIZE TO 5000
   DIM ENV(ENV.SIZE)
   MAT ENV = ''
*
   CALL MAP.SET(MAT ENV,ENV.SIZE,'VAR1',3)
   CALL MAP.SET(MAT ENV,ENV.SIZE,'VAR2',4)
*
   TEMPLATE = '3 + 4 = {{ VAR1 + VAR2 }}'
*
   CALL RENDER(MAT ENV,ENV.SIZE,TEMPLATE,RESULT)
*
   PRINT RESULT
*
   END
*

Results in:

3 + 4 = 7

Assignment

You can assign variables inside a template:

*
   EQU ENV.SIZE TO 5000
   DIM ENV(ENV.SIZE)
   MAT ENV = ''
*
   TEMPLATE = ''
   TEMPLATE<-1> = '{{ VAR1 = 3 }}'
   TEMPLATE<-1> = '{{ VAR2 = 4 }}'
   TEMPLATE<-1> = '3 + 4 = {{ VAR1 + VAR2 }}'
*
   CALL RENDER(MAT ENV,ENV.SIZE,TEMPLATE,RESULT)
*
   PRINT RESULT
*
   END
*

Results in:

3 + 4 = 7

This is handy inside loops to make things more obvious.

Loops

You can also loop using the templating language:

*
   EQU ENV.SIZE TO 5000
   DIM ENV(ENV.SIZE)
   MAT ENV = ''
*
   CALL MAP.SET(MAT ENV,ENV.SIZE,'VAR1',3)
   CALL MAP.SET(MAT ENV,ENV.SIZE,'VAR2',4)
*
   TEMPLATE = ''
   TEMPLATE<-1> = '{{ FOR I = 1 TO 5 }}'
   TEMPLATE<-1> = '   I = {{ I }}'
   TEMPLATE<-1> = '{{ NEXT I }}'
*
   CALL RENDER(MAT ENV,ENV.SIZE,TEMPLATE,RESULT)
*
   PRINT RESULT
*
   END
*

Results in:

I = 1
I = 2
I = 3
I = 4
I = 5

You can use this to loop through multivalues to display them.

*
   EQU ENV.SIZE TO 5000
   DIM ENV(ENV.SIZE)
   MAT ENV = ''
*
   NUMBERS = 100 : @AM : 200 : @AM : 300
   CALL MAP.SET(MAT ENV,ENV.SIZE,'NUMBERS',NUMBERS)
*
   TEMPLATE = ''
   TEMPLATE<-1> = '{{ FOR CTR = 1 TO DCOUNT(NUMBERS,@AM) }}'
   TEMPLATE<-1> = '   CTR = {{ NUMBERS<CTR> }}'
   TEMPLATE<-1> = '{{ NEXT CTR }}'
*
   CALL RENDER(MAT ENV,ENV.SIZE,TEMPLATE,RESULT)
*
   PRINT RESULT
*
   END
*

Results in:

I = 100
I = 200
I = 300

A faster loop that can be used is the FOR OF loop. This is not valid BASIC but is an optimization I've added for attribute marked lists.

*
   EQU ENV.SIZE TO 5000
   DIM ENV(ENV.SIZE)
   MAT ENV = ''
*
   NUMBERS = 100 : @AM : 200 : @AM : 300
   CALL MAP.SET(MAT ENV,ENV.SIZE,'NUMBERS',NUMBERS)
*
   TEMPLATE = ''
   TEMPLATE<-1> = '{{ FOR NUM OF NUMBERS}}'
   TEMPLATE<-1> = '   I = {{ NUM }}'
   TEMPLATE<-1> = '{{ NEXT I }}'
*
   CALL RENDER(MAT ENV,ENV.SIZE,TEMPLATE,RESULT)
*
   PRINT RESULT
*
   END
*

Results in:

I = 100
I = 200
I = 300

Conditionals

You can have conditional logic in the template:

*
   EQU ENV.SIZE TO 5000
   DIM ENV(ENV.SIZE)
   MAT ENV = ''
*
   TEMPLATE = ''
   TEMPLATE<-1> = '{{ FOR I = 1 TO 5 }}'
   TEMPLATE<-1> = '   {{ IF I % 3 = 0 AND I % 5 = 0 THEN }}'
   TEMPLATE<-1> = '       FizzBuzz'
   TEMPLATE<-1> = '   {{ END ELSE IF I % 3 = 0 THEN }}'
   TEMPLATE<-1> = '       Fizz'
   TEMPLATE<-1> = '   {{ END ELSE IF  I % 5 = 0 THEN }}'
   TEMPLATE<-1> = '       Buzz'
   TEMPLATE<-1> = '   {{ END ELSE }}'
   TEMPLATE<-1> = '       {{ I }}'
   TEMPLATE<-1> = '   {{ END }}'
   TEMPLATE<-1> = '{{ NEXT I }}'
*
   CALL RENDER(MAT ENV,ENV.SIZE,TEMPLATE,RESULT)
*
   PRINT RESULT
*
   END
*

This program will print FizzBuzz when I is divisible by both 3 and 5. It will print Fizz when I is divisible by just 3 and Buzz when it is divisible by 5. Otherwise it will print the number.

This results in:

1
2
Fizz
4
Buzz

Print while Rendering

To debug while the rendering process is happening you can use the PRINT statement.

*
   EQU ENV.SIZE TO 5000
   DIM ENV(ENV.SIZE)
   MAT ENV = ''
*
   NUMBERS = 100 : @AM : 200 : @AM : 300
   CALL MAP.SET(MAT ENV,ENV.SIZE,'NUMBERS',NUMBERS)
*
   TEMPLATE = ''
   TEMPLATE<-1> = '{{ FOR NUM OF NUMBERS}}'
   TEMPLATE<-1> = '   I = {{ NUM }}'
   TEMPLATE<-1> = '   {{ PRINT NUM }}'
   TEMPLATE<-1> = '{{ NEXT I }}'
*
   CALL RENDER(MAT ENV,ENV.SIZE,TEMPLATE,RESULT)
*
   PRINT RESULT
*
   END
*

The PRINT statement will only print to the terminal, this output won't be part of the resulting string.

The result:

I = 100
I = 200
I = 300

The terminal will look like:

100
200
300
I = 100
I = 200
I = 300

Include Templates

You can include templates:

{{ INCLUDE "HTML-TEMPLATE-FILE" "TEMPLATE-A.HTML" }}

The INCLUDE statement takes a MD entry and a item id.

Open Files

You can open files directly from within a template.

*
   EQU ENV.SIZE TO 5000
   DIM ENV(ENV.SIZE)
   MAT ENV = ''
*
   TEMPLATE = ''
   TEMPLATE<-1> = "{{ FILE.OPENED = 1 }}"
   TEMPLATE<-1> = "{{ OPEN '','SITE-FILE' TO SITE.FILE ELSE FILE.OPENED = 0 }}"
   TEMPLATE<-1> = "{{ FILE.OPENED }}"
*
   CALL RENDER(MAT ENV,ENV.SIZE,TEMPLATE,RESULT)
*
   PRINT RESULT
*
   END
*

This will open a file and if the file does not exist it will set the FILE.OPENED variable.

This results in:

1

Read a Record

You can read a record from within the template:

*
   EQU ENV.SIZE TO 5000
   DIM ENV(ENV.SIZE)
   MAT ENV = ''
*
   TEMPLATE = ''
   TEMPLATE<-1> = "{{ FILE.OPENED = 1 }}"
   TEMPLATE<-1> = "{{ OPEN '','SITE-FILE' TO SITE.FILE ELSE FILE.OPENED = 0 }}"
   TEMPLATE<-1> = "{{ READ ITEM FROM SITE.FILE,'MAIN' ELSE ITEM = '' }}"
   TEMPLATE<-1> = "{{ ITEM<1> }}"
*
   CALL RENDER(MAT ENV,ENV.SIZE,TEMPLATE,RESULT)
*
   PRINT RESULT
*
   END
*

This will print out what is in ITEM in the first attribute.

Functions

You can use the below functions inside a template:

  • DCOUNT
  • LEN
  • OCONV
  • MOD
  • DATE
  • NOT

It is straightforward to add functions. They need to be added the EVALUATE.BTL routine.

Calling External Subroutines

You can call subroutine from within a template. I have only allowed subroutine calls of 1 parameter for now.

*
   EQU ENV.SIZE TO 5000
   DIM ENV(ENV.SIZE)
   MAT ENV = ''
*
   TEMPLATE = "{{ CALL TEST.CALL(RESULT) }}"
   TEMPLATE<-1> = "{{ RESULT }}"
*
   CALL RENDER(MAT ENV,ENV.SIZE,TEMPLATE,RESULT)
*
   PRINT RESULT
*
   END
*

This will call the TEST.CALL subroutine and print the RESULT.

Conclusion

The above examples all have the template directly in the BASIC program however the way I use it is to have HTML-TEMPLATE-FILE and then I write my templates there.

I then read the template in and call the render program.

   EQU ENV.SIZE TO 5000
   DIM ENV(ENV.SIZE)
   MAT ENV = ''
*
   OPEN '','HTML-TEMPLATE-FILE' TO HTML.TEMPLATE.FILE ELSE
      PRINT 'Unable to open file'
      STOP
   END
*
   TEMPLATE.ID = 'INDEX.HTML'
   READ TEMPLATE FROM HTML.TEMPLATE.FILE,TEMPLATE.ID ELSE TEMPLATE = ''
*
   CALL RENDER(MAT ENV,ENV.SIZE,TEMPLATE,RESULT)
*
   PRINT RESULT
*
   END
*

The BASIC Templating Language let's you compose the various things all together to make relatively complex programs using the templating language.

The OPEN, READ and the external subroutine statements are definitely dangerous and should not be used in templates. Templates should try to minimize the magic and so any data they need should be passed in directly via the environment rather than have the template go out and fetch data.

However these features were fun to implement so here they are.