Chris Sainty

A technical blog covering full-stack web development.

twitter | github | stackoverflow

Scripted Text Generation

The first piece of code I would like to post is one of my favourites, in that geeky kind of way. A quick little routine to take a file that mixes static text with foxpro expressions and scripting then turns it into a foxpro procedure.

We use this code to process HTML and XML template files used by our websites into efficient compiled foxpro code. I made mention of this code as part of my presentation at OzFox Lite in March 2006. So here it is!

### Code

#define k_NL      chr(13) + chr(10)
#define k_VARIABLE    "__cS"
#define k_BLOCK_OPEN  "<%"    && open code blocks
#define k_BLOCK_CLOSE  "%>"    && close code blocks
#define k_EXP_OPEN    "<%="    && open expressions
#define k_EXP_CLOSE    "%>"    && close expressions
#define k_QUOTE_OPEN  '['      && open quote for strings
#define k_QUOTE_CLOSE  ']'      && close quote for strings
#define k_QUOTE2_OPEN   [']      && open quote mark safe to wrap other quote marks in
#define k_QUOTE2_CLOSE  [']      && close quote mark safe to wrap other quote marks in
#define k_SPECIAL    k_QUOTE_OPEN + k_QUOTE_CLOSE + [&]  && Break these synbols out of the string and wrap in quote2
#define k_MAX_LINES    20
#define k_MAX_STRING  254
#define k_SAVE_INDENT  .T.

function compilePage
*************************
lparameters cStr, cFName 
local cS, cLine, inScript, inLine, nLines, cLinePre
local array a_file[1]
nLines= 0
cS= 'function ' + m.cFName + k_NL ;
  + 'lparameters ' + k_VARIABLE + k_NL ;
  + k_VARIABLE + "= " + k_QUOTE_OPEN + k_QUOTE_CLOSE + k_NL
cLinePre= ''
for i = 1 to alines(a_file, m.cStr)
  cLine= alltrim(chrtran(a_file[m.i], chr(9) + chr(10) + chr(13), ''))
  #if k_SAVE_INDENT
    cLinePre= left(a_file[m.i], at(left(m.cLine, 1), a_file[m.i]) - 1)
  #endif
  if !empty(m.cLine)
    do case
      case m.cLine = k_BLOCK_OPEN and m.cLine # k_EXP_OPEN
        inScript= at(k_BLOCK_CLOSE, m.cLine) = 0
        if m.inLine
          inLine= .F.
          cS= left(m.cS, rat(';', m.cS) - 1) + k_NL
          nLines= 0
        endif
        cLine= alltrim(strextract(m.cLine, k_BLOCK_OPEN, k_BLOCK_CLOSE, 1, 2))
        if !empty(m.cLine)
          cS= m.cS + compileLine(m.cLine) + k_NL
        endif
      case ltrim(m.cLine) = k_BLOCK_CLOSE
        inScript= .F.
        nLines= 0
      case m.inScript
        if at(k_BLOCK_CLOSE, m.cLine) # 0
          cLine= substr(m.cLine, 1, at(k_BLOCK_CLOSE, m.cLine) - 1)
          nLines= 0
          inScript= .f.
        endif
        if m.cLine # "*" and m.cLine # "&" + "&"
          cS= m.cS + compileLine(m.cLine) + k_NL
        endif
      otherwise
        if !m.inLine or m.nLines = k_MAX_LINES
          if m.nLines= k_MAX_LINES
            cS= left(m.cS, rat(';', m.cS) - 1) + k_NL
          endif
          nLines= 0
          cS= m.cS + k_VARIABLE + "= m." + k_VARIABLE + " "
          inLine= .T.
        else
          nLines= m.nLines + 1
          cS= m.cS + chr(9)
        endif
        cS= m.cS + "+ " + alltrim(compileExpr(m.cLine, m.cLinePre)) + " + chr(13)+chr(10)" + iif(m.inLine, ' ;', '') + k_NL
    endcase
  endif
endfor
if m.inLine
  inLine= .F.
  cS= left(m.cS, rat(';', m.cS) - 1) + k_NL
endif
return m.cS + k_NL + k_NL

function compileLine
*********************
lparameters cS
cS= alltrim(m.cS)
do case
case m.cS = "?"
  cS= k_VARIABLE + "= m." + k_VARIABLE + " + " + alltrim(substr(m.cS, 2)) + " + chr(13)+chr(10)"
  
otherwise
  * do nothing
endcase
return m.cS

function compileExpr
*********************
lparameters cS, cLinePre
local i, inExp, cStr, inBrack, c, lOpen, nLen
cStr= ''
nLen= 0
for i = 1 to len(m.cS)
  c= substr(m.cS, i)
  if empty(chrtran(m.c, chr(9), ''))
    exit
  endif
  nLen= m.nLen + 1
  do case
  case m.c = k_EXP_OPEN
    i= i + 2
    inExp = .T.
    if m.lOpen
      cStr= m.cStr + k_QUOTE_CLOSE + " + "
      lOpen= .F.
    endif
  case m.c = k_EXP_CLOSE
    i= m.i + 1
    inExp= .F.
    if !empty(substr(m.cS, m.i + 1))
      cStr= m.cStr + " + " + k_QUOTE_OPEN
      lOpen= .T.
      nLen= 0
    endif
  case m.inExp
    cStr= m.cStr + substr(m.cS, i, 1)
  case left(m.c, 1) $ k_SPECIAL
    if m.lOpen
      cStr= m.cStr + k_QUOTE_CLOSE + " + "
    endif
    cStr= m.cStr + k_QUOTE2_OPEN + m.cLinePre + left(m.c, 1) + k_QUOTE2_CLOSE + " + "
    cLinePre= ''
    if m.lOpen
      cStr= m.cStr + k_QUOTE_OPEN
    endif
  otherwise
    if !m.lOpen
      cStr= m.cStr + k_QUOTE_OPEN + m.cLinePre
      lOpen= .T.
      nLen= m.nLen + len(m.cLinePre)
      cLinePre= ''
    endif
    if m.nLen > k_MAX_STRING
      cStr= m.cStr + k_QUOTE_CLOSE + " + " + k_QUOTE_OPEN
      nLen= 0
    endif
    cStr= m.cStr + substr(m.cS, i, 1)
  endcase
endfor
cStr= strtran(m.cStr, k_EXP_OPEN, k_QUOTE_CLOSE + " + ")
cStr= strtran(m.cStr, k_EXP_CLOSE, " + " + k_QUOTE_OPEN)
if m.lOpen
  cStr= m.cStr + k_QUOTE_CLOSE
endif
return m.cStr

The precise way to call this code will depend on how you intend to use it. I have provided the basic calling structure, but a better way to do this is to compile a directory of script files into one single prg which you can add to your SET PROCEDURE line. You can also on-the-fly compile these pages off disk as they are called.

### Example Calling Code

local cIn, cOut, cS
cIn= filetostr("c:\test.htm")
cOut= compilePage(m.cIn, "__test")
strtofile(m.cOut, "c:\test.prg")
open database (home(2) + "northwind\northwind")
use northwind!Customers in 0
do __test in C:\test.prg with cS
?cS
close databases all

You can point the "compiler" at any text based file you like, a very basic example is provided below. Along with the procedure this would generate.

### Example Input

The time is <%= ttoc(datetime()) %>
<%
  select Customers
  scan
%>
    Customer: <%= Customers.CustomerID %><br />
<% endscan %>

### Example Output

function __test
lparameters __cS
__cS= []
__cS= m.__cS + [The time is ] +  ttoc(datetime()) + chr(13)+chr(10) 
select Customers
scan
__cS= m.__cS + [    Customer: ] +  Customers.CustomerID  + [<br />] + chr(13)+chr(10) 
endscan

I find this a nice way to separate the 'design' of text content from your code. I am sure we have all at one stage experienced the nightmare of trying to maintain complex string building code inside fox itself. Not fun.

To try out this code: