mirror of
https://github.com/rejetto/hfs2.git
synced 2025-12-19 18:13:46 +01:00
248 lines
7.3 KiB
ObjectPascal
248 lines
7.3 KiB
ObjectPascal
unit parserLib;
|
|
|
|
interface
|
|
|
|
uses
|
|
strutils, sysutils, classes, types, utilLib, windows;
|
|
|
|
type
|
|
TmacroCB = function(fullMacro:string; pars:Tstrings; cbData:pointer):string;
|
|
EtplError = class(Exception)
|
|
pos, row, col: integer;
|
|
code: string;
|
|
constructor Create(const msg, code:string; row,col:integer);
|
|
end;
|
|
const
|
|
MARKER_OPEN = '{.';
|
|
MARKER_CLOSE = '.}';
|
|
MARKER_SEP = '|';
|
|
MARKER_QUOTE = '{:';
|
|
MARKER_UNQUOTE = ':}';
|
|
MARKERS: array [0..4] of string = ( MARKER_OPEN, MARKER_CLOSE, MARKER_SEP, MARKER_QUOTE, MARKER_UNQUOTE );
|
|
|
|
function anyMacroMarkerIn(s:string):boolean;
|
|
function findMacroMarker(s:string; ofs:integer=1):integer;
|
|
procedure applyMacrosAndSymbols(var txt:string; cb:TmacroCB; cbData:pointer; removeQuotings:boolean=TRUE);
|
|
|
|
implementation
|
|
|
|
const
|
|
MAX_RECUR_LEVEL = 50;
|
|
type
|
|
TparserIdsStack = array [1..MAX_RECUR_LEVEL] of string;
|
|
|
|
constructor EtplError.create(const msg, code:string; row, col:integer);
|
|
begin
|
|
inherited create(msg);
|
|
self.row:=row;
|
|
self.col:=col;
|
|
self.code:=code;
|
|
end;
|
|
|
|
procedure applyMacrosAndSymbols2(var txt:string; cb:TmacroCB; cbData:pointer; var idsStack:TparserIdsStack; recurLevel:integer=0);
|
|
const
|
|
// we don't track SEPs, they are handled just before the callback
|
|
QUOTE_ID = 0; // QUOTE must come before OPEN because it is a substring
|
|
UNQUOTE_ID = 1;
|
|
OPEN_ID = 2;
|
|
CLOSE_ID = 3;
|
|
MAX_MARKER_ID = 3;
|
|
|
|
function alreadyRecurredOn(s:string):boolean;
|
|
var
|
|
i: integer;
|
|
begin
|
|
result:=TRUE;
|
|
for i:=recurLevel downto 1 do
|
|
if sameText(s, idsStack[i]) then exit;
|
|
result:=FALSE;
|
|
end; // alreadyRecurredOn
|
|
|
|
procedure handleSymbols();
|
|
var
|
|
b, e, l : integer;
|
|
s, newS: string;
|
|
begin
|
|
e:=0;
|
|
l:=length(txt);
|
|
while e < l do
|
|
begin
|
|
// search for next symbol
|
|
b:=posEx('%',txt,e+1);
|
|
if b = 0 then break;
|
|
e:=b+1;
|
|
if txt[e] = '%' then
|
|
begin // we don't accept %% as a symbol. so, restart parsing from the second %
|
|
e:=b;
|
|
continue;
|
|
end;
|
|
if not charInSet(txt[e], ['_','a'..'z','A'..'Z']) then continue; // first valid character
|
|
while (e < l) and charInSet(txt[e], ['0'..'9','a'..'z','A'..'Z','-','_']) do
|
|
inc(e);
|
|
if txt[e] <> '%' then continue;
|
|
// found!
|
|
s:=substr(txt,b,e);
|
|
if alreadyRecurredOn(s) then continue; // the user probably didn't meant to create an infinite loop
|
|
|
|
newS:=cb(s, NIL, cbData);
|
|
if s = newS then continue;
|
|
|
|
idsStack[recurLevel]:=s; // keep track of what we recur on
|
|
// apply translation, and eventually recur
|
|
try applyMacrosAndSymbols2(newS, cb, cbData, idsStack, recurLevel);
|
|
except end;
|
|
idsStack[recurLevel]:='';
|
|
inc(e, replace(txt, newS, b, e));
|
|
l:=length(txt);
|
|
end;
|
|
end; // handleSymbols
|
|
|
|
procedure handleMacros();
|
|
var
|
|
pars: Tstrings;
|
|
|
|
function expand(from,to_:integer):integer;
|
|
var
|
|
s, fullMacro: string;
|
|
i, o, q, u: integer;
|
|
begin
|
|
result:=0;
|
|
fullMacro:=substr(txt, from+length(MARKER_OPEN), to_-length(MARKER_CLOSE));
|
|
if alreadyRecurredOn(fullMacro) then exit; // the user probably didn't meant to create an infinite loop
|
|
|
|
// let's find the SEPs to build 'pars'
|
|
pars.clear();
|
|
i:=1; // char pointer from where we shall copy the macro parameter
|
|
o:=0;
|
|
q:=posEx(MARKER_QUOTE, fullMacro); // q points to _QUOTE
|
|
repeat
|
|
o:=posEx(MARKER_SEP, fullmacro, o+1);
|
|
if o = 0 then break;
|
|
if (q > 0) and (q < o) then // this SEP is possibly quoted
|
|
begin
|
|
// update 'q' and 'u'
|
|
repeat
|
|
u:=posEx(MARKER_UNQUOTE, fullMacro, q);
|
|
if u = 0 then exit; // macro quoting not properly closed
|
|
q:=posEx(MARKER_QUOTE, fullMacro, q+1); // update q for next cycle
|
|
// if we find other _QUOTEs before _UNQUOTE, then they are stacked, and we must go through the same number of both markers
|
|
while (q > 0) and (q < u) do
|
|
begin
|
|
u:=posEx(MARKER_UNQUOTE, fullMacro, u+1);
|
|
if u = 0 then exit; // macro quoting not properly closed
|
|
q:=posEx(MARKER_QUOTE, fullMacro, q+1);
|
|
end;
|
|
until (q = 0) or (o < q);
|
|
// eventually skip this chunk of string
|
|
if o < u then
|
|
begin // yes, this SEP is quoted
|
|
o:=u;
|
|
continue;
|
|
end;
|
|
end;
|
|
// ok, that's a valid SEP, so we collect this as a parameter
|
|
pars.add(substr(fullMacro, i, o-1));
|
|
i:=o+length(MARKER_SEP);
|
|
until false;
|
|
pars.add(substr(fullMacro, i, length(fullMacro))); // last piece
|
|
// ok, 'pars' has now been built
|
|
|
|
// do the call, recur, and replace with the result
|
|
s:=cb(fullMacro, pars, cbData);
|
|
idsStack[recurLevel]:=fullmacro; // keep track of what we recur on
|
|
try
|
|
try applyMacrosAndSymbols2(s, cb, cbData, idsStack, recurLevel) except end;
|
|
finally idsStack[recurLevel]:='' end;
|
|
result:=replace(txt, s, from, to_);
|
|
end; // expand
|
|
|
|
const
|
|
ID2TAG: array [0..MAX_MARKER_ID] of string = (MARKER_QUOTE, MARKER_UNQUOTE, MARKER_OPEN, MARKER_CLOSE);
|
|
type
|
|
TstackItem = record
|
|
pos: integer;
|
|
row, col: word;
|
|
quote: boolean;
|
|
end;
|
|
var
|
|
i, lastNL, row, m, t: integer;
|
|
stack: array of TstackItem;
|
|
Nstack: integer;
|
|
begin
|
|
setLength(stack, length(txt) div length(MARKER_OPEN)); // it will never need more than this
|
|
Nstack:=0;
|
|
pars:=TstringList.create;
|
|
try
|
|
i:=1;
|
|
row:=1;
|
|
lastNL:=0;
|
|
while i <= length(txt) do
|
|
begin
|
|
if txt[i] = #10 then
|
|
begin
|
|
inc(row);
|
|
lastNL:=i;
|
|
end;
|
|
for m:=0 to MAX_MARKER_ID do
|
|
begin
|
|
if not strAt(txt, ID2TAG[m], i) then continue;
|
|
case m of
|
|
QUOTE_ID,
|
|
OPEN_ID:
|
|
begin
|
|
if (m = OPEN_ID) and (Nstack > 0) and stack[Nstack-1].quote then continue; // don't consider quoted OPEN markers
|
|
stack[Nstack].pos:=i;
|
|
stack[Nstack].quote:= m=QUOTE_ID;
|
|
stack[Nstack].row:=row;
|
|
stack[Nstack].col:=i-lastNL;
|
|
inc(Nstack);
|
|
end;
|
|
CLOSE_ID:
|
|
begin
|
|
if Nstack = 0 then
|
|
raise EtplError.create('unmatched marker', copy(txt,i,30), row, i-lastNL);
|
|
if (Nstack > 0) and stack[Nstack-1].quote then continue; // don't consider quoted CLOSE markers
|
|
t:=length(MARKER_CLOSE);
|
|
inc(i, t-1+expand(stack[Nstack-1].pos, i+t-1));
|
|
dec(Nstack);
|
|
end;
|
|
UNQUOTE_ID:
|
|
begin
|
|
if (Nstack = 0) or not stack[Nstack-1].quote then continue;
|
|
dec(Nstack);
|
|
end;
|
|
end;
|
|
end;//for
|
|
inc(i);
|
|
end;
|
|
finally pars.free end;
|
|
if Nstack > 0 then
|
|
with stack[Nstack-1] do
|
|
raise EtplError.create('unmatched marker', copy(txt,pos,30), row, col)
|
|
end; // handleMacros
|
|
|
|
begin
|
|
if recurLevel > MAX_RECUR_LEVEL then exit;
|
|
inc(recurLevel);
|
|
handleSymbols();
|
|
handleMacros();
|
|
end; //applyMacrosAndSymbols2
|
|
|
|
procedure applyMacrosAndSymbols(var txt:string; cb:TmacroCB; cbData:pointer; removeQuotings:boolean=TRUE);
|
|
var
|
|
idsStack: TparserIdsStack;
|
|
begin
|
|
enforceNUL(txt);
|
|
applyMacrosAndSymbols2(txt,cb,cbData,idsStack);
|
|
if removeQuotings then
|
|
txt:=xtpl(txt, [MARKER_QUOTE, '', MARKER_UNQUOTE, ''])
|
|
end;
|
|
|
|
function findMacroMarker(s:string; ofs:integer=1):integer;
|
|
begin result:=reMatch(s, '\{[.:]|[.:]\}|\|', 'm!', ofs) end;
|
|
|
|
function anyMacroMarkerIn(s:string):boolean;
|
|
begin result:=findMacroMarker(s) > 0 end;
|
|
|
|
end.
|