diff --git a/UFilter.pas b/UFilter.pas new file mode 100644 index 0000000..a77ccde --- /dev/null +++ b/UFilter.pas @@ -0,0 +1,109 @@ +{ + This file is part of restemplate. + + Copyright (C) 2015 Andreas Schneider + + restemplate is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + restemplate is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with restemplate. If not, see . +} +unit UFilter; + +{$mode objfpc}{$H+} +{$modeswitch advancedrecords} + +interface + +uses + Classes, SysUtils, RegExpr, fgl; + +type + + { THighlightFilter } + + THighlightFilter = class + Expression: TRegExpr; + FGColor: Byte; + BGColor: Byte; + constructor Create(AString: String); + destructor Destroy; override; + class var + FilterExpression: TRegExpr; + ParamExpression: TRegExpr; + end; + TFilterList = specialize TFPGObjectList; + + { THighlight } + + THighlight = record + FGColor: Byte; + BGColor: Byte; + Start: Integer; + Length: Integer; + class operator =(A, B: THighlight): Boolean; + end; + THighlights = specialize TFPGList; + +function CompareHighlights(const AHL1: THighlight; const AHL2: THighlight): Integer; + +implementation + +{ THighlightFilter } + +constructor THighlightFilter.Create(AString: String); +begin + FGColor := $FF; + BGColor := $FF; + if FilterExpression.Exec(AString) then + begin + Expression := TRegExpr.Create(Copy(AString, 1, FilterExpression.MatchPos[0] - 1)); + if ParamExpression.Exec(FilterExpression.Match[0]) then + repeat + if ParamExpression.Match[1] = 'FG' then + FGColor := StrToInt(ParamExpression.Match[2]) + else if ParamExpression.Match[1] = 'BG' then + BGColor := StrToInt(ParamExpression.Match[2]); + until not ParamExpression.ExecNext; + end else + Expression := TRegExpr.Create(AString); +end; + +destructor THighlightFilter.Destroy; +begin + Expression.Free; + inherited Destroy; +end; + +{ THighlight } + +class operator THighlight. = (A, B: THighlight): Boolean; +begin + Result := (A.Start = B.Start) and (A.Length = B.Length); +end; + +function CompareHighlights(const AHL1: THighlight; const AHL2: THighlight): Integer; +begin + if AHL1.Start = AHL2.Start then + begin + Result := AHL1.Length - AHL2.Length; + end else + Result := AHL1.Start - AHL2.Start; +end; + +initialization + THighlightFilter.FilterExpression := TRegExpr.Create('( (FG|BG)(\d+))*$'); + THighlightFilter.ParamExpression := TRegExpr.Create('(FG|BG)(\d+)'); +finalization + THighlightFilter.FilterExpression.Free; + THighlightFilter.ParamExpression.Free; +end. + diff --git a/URestemplateApp.pas b/URestemplateApp.pas new file mode 100644 index 0000000..2dbe778 --- /dev/null +++ b/URestemplateApp.pas @@ -0,0 +1,428 @@ +{ + This file is part of restemplate. + + Copyright (C) 2015 Andreas Schneider + + restemplate is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + restemplate is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with restemplate. If not, see . +} +unit URestemplateApp; + +{$mode objfpc}{$H+} + +interface + +uses + Classes, SysUtils, CustApp, + IniFiles, fphttpclient, jsonparser, JTemplate, + UFilter; + +type + + { TRestemplateApplication } + + TRestemplateApplication = class(TCustomApplication) + protected + FConfigDir: String; + FTemplateDir: String; + FSessionIni: TIniFile; + FTemplateName: String; + FParser: TJTemplateParser; + FHttp: TFPHTTPClient; + FContent: TStringList; + FFilters: TFilterList; + FBeautify: Boolean; + FURL: String; + FMethod: String; + //Main + procedure DoRun; override; + //Command processing + function ProcessCommand(ALine: String): Boolean; + procedure CmdAskUser(AName: String); + procedure CmdHeader(AHeader: String); + procedure CmdBasicAuth(AData: String); + procedure CmdHighlight(AData: String); + procedure ProcessCall(AURL: String); + //Helper + procedure ListProfiles; + procedure WriteContent; + public + constructor Create; overload; + destructor Destroy; override; + end; + +implementation + +uses + strutils, crt, UCRTHelper, fpjson, DOM, XMLRead, XMLWrite; + +type + TContentType = (ctOther, ctJSON, ctXML); + +function IdentifyContentType(AString: String; out ContentType: TContentType): Boolean; +begin + if Pos('application/json', AString) > 0 then + ContentType := ctJSON + else if Pos('application/xml', AString) > 0 then + ContentType := ctXML + else + ContentType := ctOther; + + Result := (ContentType <> ctOther); +end; + +{ TRestemplateApplication } + +procedure TRestemplateApplication.DoRun; +var + templateFile: String; + line: String; + data: TextFile; + commandMode: Boolean; +begin + templateFile := FTemplateDir + ParamStr(1) + '.rest'; + + if ParamCount <> 1 then + begin + Writeln('Usage: ', ExtractFileName(ParamStr(0)), ' '); + Writeln; + ListProfiles; + Halt(0); + end; + + if FileExists(ParamStr(1)) then + begin + AssignFile(data, ParamStr(1)); + FTemplateName := ExtractFileName(ParamStr(1)); + if AnsiEndsStr('.rest', FTemplateName) then + FTemplateName := Copy(FTemplateName, 1, Length(FTemplateName) - 5); + end else + if FileExists(templateFile) then + begin + AssignFile(data, templateFile); + FTemplateName := ParamStr(1); + end else + begin + writeln('Template not found!'); + Halt(1); + end; + Reset(data); + + FBeautify := False; //By default, we don't want to manipulate output. + + commandMode := True; + + try + while not EOF(data) do + begin + ReadLn(data, line); + if commandMode and (line <> '') and (line[1] <> '#') and + not ProcessCommand(line) then + begin + commandMode := False; + end; + + if not commandMode then + FContent.Add(line); + end; + + if FURL <> '' then + ProcessCall(FURL); + + finally + CloseFile(data); + end; + + Terminate; +end; + +function TRestemplateApplication.ProcessCommand(ALine: String): Boolean; +begin + Result := False; + if AnsiStartsStr('Ask ', ALine) then + begin + Result := True; + CmdAskUser(Copy(ALine, 5, Length(ALine))); + end else + if AnsiStartsStr('Header ', ALine) then + begin + Result := True; + CmdHeader(Copy(ALine, 8, Length(ALine))); + end else + if AnsiStartsStr('Method ', ALine) then + begin + Result := True; + FMethod := Copy(ALine, 8, Length(ALine)); + end else + // We don't include the space here, since we also interpret this following + // char as separator for username and password! (In case the username itself + // contains a space.) + if AnsiStartsStr('BasicAuth', ALine) then + begin + Result := True; + CmdBasicAuth(Copy(ALine, 10, Length(ALine))); + end else + if ALine = 'Beautify' then + begin + Result := True; + FBeautify := True; + end else + if AnsiStartsStr('Highlight ', ALine) then + begin + Result := True; + CmdHighlight(Copy(ALine, 11, Length(ALine))); + end else + if AnsiStartsStr('Call ', ALine) then + begin + Result := True; + FURL := Copy(ALine, 6, Length(ALine)); + end; +end; + +procedure TRestemplateApplication.CmdAskUser(AName: String); +var + value, default: String; +begin + default := FSessionIni.ReadString(FTemplateName, AName, ''); + value := Prompt(AName, default); + + if value = '' then + Halt(3); //Cancelled + + FParser.Fields.Add(AName, value); + + FSessionIni.WriteString(FTemplateName, AName, value); +end; + +procedure TRestemplateApplication.CmdHeader(AHeader: String); +var + i: Integer; + varName, varValue: String; +begin + FParser.Content := AHeader; + FParser.Replace; + AHeader := FParser.Content; + + i := 1; + while (i < Length(AHeader)) and (AHeader[i] <> ':') do + Inc(i); + + varName := Trim(Copy(AHeader, 1, i - 1)); + varValue := Trim(Copy(AHeader, i + 1, Length(AHeader))); + + FHttp.AddHeader(varName, varValue); +end; + +procedure TRestemplateApplication.CmdBasicAuth(AData: String); +var + separator: Char; + i: Integer; +begin + separator := AData[1]; + i := 2; + while (i < Length(AData)) and (AData[i] <> separator) do + Inc(i); + + FHttp.UserName := Copy(AData, 2, i - 2); + FHttp.Password := Copy(AData, i + 1, Length(AData)); +end; + +procedure TRestemplateApplication.CmdHighlight(AData: String); +begin + FFilters.Add(THighlightFilter.Create(AData)) +end; + +procedure TRestemplateApplication.ProcessCall(AURL: String); +var + s: String; + request, response: TStream; + jsonParser: TJSONParser; + jsonData: TJSONData; + contentType: TContentType; + xmlDoc: TXMLDocument; +begin + FParser.Content := AURL; + FParser.Replace; + AURL := FParser.Content; + writeln('Calling ', AURL); + + response := TMemoryStream.Create; + request := nil; + + if FContent.Count > 0 then + begin + request := TMemoryStream.Create; + + // Variable replacement + FParser.Content := FContent.Text; + FParser.Replace; + FContent.Text := FParser.Content; + + FContent.SaveToStream(request); + request.Position := 0; + FHttp.RequestBody := request; + end; + + try + FHttp.HTTPMethod(FMethod, AURL, response, []); + except + on E: Exception do + begin + writeln; + writeln('Failed! ', E.Message); + Halt(1); + end; + end; + + writeln; + writeln('Status: ', FHttp.ResponseStatusCode, ' (', FHttp.ResponseStatusText, ')'); + writeln; + writeln('Headers:'); + for s in FHttp.ResponseHeaders do + writeln(' ', s); + writeln; + + response.Position := 0; + + if FBeautify and IdentifyContentType(FHttp.GetHeader(FHttp.ResponseHeaders, 'Content-Type'), contentType) then + begin + case contentType of + ctJSON: + begin + jsonParser := TJSONParser.Create(response); + jsonData := jsonParser.Parse; + FContent.Text := jsonData.FormatJSON; + jsonData.Free; + jsonParser.Free; + WriteContent; + end; + ctXML: + begin + ReadXMLFile(xmlDoc, response); + response.Size := 0; + WriteXMLFile(xmlDoc, response); + response.Position := 0; + FContent.LoadFromStream(response); + WriteContent; + end; + end; + end else + begin + FContent.LoadFromStream(response); + WriteContent; + end; + + response.Free; + request.Free; +end; + +procedure TRestemplateApplication.ListProfiles; +var + sr: TSearchRec; +begin + Writeln('Known profiles:'); + if FindFirst(FTemplateDir + '*.rest', faAnyFile, sr) = 0 then + begin + repeat + writeln(' ', Copy(sr.Name, 1, Length(sr.Name) - 5)); + until FindNext(sr) <> 0; + end; +end; + +procedure TRestemplateApplication.WriteContent; +var + i: Integer; + matchPos, offset, lastPos: Integer; + data: String; + highlights: THighlights; + highlight: THighlight; +begin + highlights := THighlights.Create; + data := FContent.Text; + + offset := 0; + lastPos := 1; + + for i := 0 to FFilters.Count - 1 do + begin + if FFilters[i].Expression.Exec(data) then + begin + repeat + highlight.Start := FFilters[i].Expression.MatchPos[0]; + highlight.Length := FFilters[i].Expression.MatchLen[0]; + highlight.FGColor := FFilters[i].FGColor; + highlight.BGColor := FFilters[i].BGColor; + highlights.Add(highlight); + until not FFilters[i].Expression.ExecNext; + end; + end; + + highlights.Sort(@CompareHighlights); + + // Sanitize highlights + for i := 0 to highlights.Count - 2 do + begin + if (highlights[i].Start + highlights[i].Length) > highlights[i+1].Start then + begin + highlight := highlights[i]; + highlight.Length := highlights[i+1].Start - highlights[i].Start; + highlights[i] := highlight; + end; + end; + + for highlight in highlights do + begin + matchPos := highlight.Start; + offset := highlight.Length; + write(Copy(data, lastPos, matchPos - lastPos)); + if highlight.FGColor < $FF then + TextColor(highlight.FGColor); + if highlight.BGColor < $FF then + TextBackground(highlight.BGColor); + write(Copy(data, matchPos, offset)); + NormVideo; + lastPos := matchPos + offset; + end; + + writeln(Copy(data, lastPos, Length(data))); + + highlights.Free; +end; + +constructor TRestemplateApplication.Create; +begin + inherited Create(nil); + StopOnException := True; + + FConfigDir := GetAppConfigDir(False); + FTemplateDir := FConfigDir + 'templates' + PathDelim; + + FSessionIni := TIniFile.Create(FConfigDir + 'session.ini'); + FContent := TStringList.Create; + FHttp := TFPHTTPClient.Create(Self); + FFilters := TFilterList.Create; + FParser := TJTemplateParser.Create; +end; + +destructor TRestemplateApplication.Destroy; +begin + FSessionIni.Free; + FContent.Free; + FHttp.Free; + FFilters.Free; + FParser.Free; + inherited Destroy; +end; + +end. + diff --git a/restemplate.lpi b/restemplate.lpi index ee187d7..4b080ed 100644 --- a/restemplate.lpi +++ b/restemplate.lpi @@ -17,7 +17,9 @@ - + + + @@ -55,7 +57,7 @@ - + @@ -64,6 +66,14 @@ + + + + + + + + diff --git a/restemplate.pas b/restemplate.pas index f81f67b..fa0a5f6 100644 --- a/restemplate.pas +++ b/restemplate.pas @@ -20,440 +20,18 @@ program restemplate; {$mode objfpc}{$H+} -{$modeswitch advancedrecords} uses - SysUtils, Classes, strutils, IniFiles, fgl, - fphttpclient, - JTemplate, - fpjson, jsonparser, - DOM, XMLRead, XMLWrite, - RegExpr, crt, UCRTHelper; + URestemplateApp, UFilter, UCRTHelper; -type - TContentType = (ctOther, ctJSON, ctXML); - - { THighlightFilter } - - THighlightFilter = class - Expression: TRegExpr; - FGColor: Byte; - BGColor: Byte; - constructor Create(AString: String); - destructor Destroy; override; - class var - FilterExpression: TRegExpr; - ParamExpression: TRegExpr; - end; - TFilterList = specialize TFPGObjectList; - - { THighlight } - - THighlight = record - FGColor: Byte; - BGColor: Byte; - Start: Integer; - Length: Integer; - class operator =(A, B: THighlight): Boolean; - end; - THighlights = specialize TFPGList; +{$R *.res} var - data: TextFile; - line: String; - parser: TJTemplateParser; - http: TFPHTTPClient; - method, url: String; - content: TStringList; - commandMode: Boolean; - configDir, templateDir: String; - templateFile, templateName: String; - sessionIni: TIniFile; - beautify: Boolean; - filters: TFilterList; - -{ THighlight } - -class operator THighlight. = (A, B: THighlight): Boolean; + Application: TRestemplateApplication; begin - Result := (A.Start = B.Start) and (A.Length = B.Length); -end; - - -{ THighlightFilter } - -constructor THighlightFilter.Create(AString: String); -begin - FGColor := $FF; - BGColor := $FF; - if FilterExpression.Exec(AString) then - begin - Expression := TRegExpr.Create(Copy(AString, 1, FilterExpression.MatchPos[0] - 1)); - if ParamExpression.Exec(FilterExpression.Match[0]) then - repeat - if ParamExpression.Match[1] = 'FG' then - FGColor := StrToInt(ParamExpression.Match[2]) - else if ParamExpression.Match[1] = 'BG' then - BGColor := StrToInt(ParamExpression.Match[2]); - until not ParamExpression.ExecNext; - end else - Expression := TRegExpr.Create(AString); -end; - -destructor THighlightFilter.Destroy; -begin - Expression.Free; - inherited Destroy; -end; - -{ Main } - -procedure CmdAskUser(AName: String); -var - value, default: String; -begin - default := sessionIni.ReadString(templateName, AName, ''); - value := Prompt(AName, default); - - if value = '' then - Halt(3); //Cancelled - - parser.Fields.Add(AName, value); - - sessionIni.WriteString(templateName, AName, value); -end; - -procedure CmdHeader(AHeader: String); -var - i: Integer; - name, value: String; -begin - parser.Content := AHeader; - parser.Replace; - AHeader := parser.Content; - - i := 1; - while (i < Length(AHeader)) and (AHeader[i] <> ':') do - Inc(i); - - name := Trim(Copy(AHeader, 1, i - 1)); - value := Trim(Copy(AHeader, i + 1, Length(AHeader))); - - http.AddHeader(name, value); -end; - -function IdentifyContentType(AString: String; out ContentType: TContentType): Boolean; -begin - if Pos('application/json', AString) > 0 then - ContentType := ctJSON - else if Pos('application/xml', AString) > 0 then - ContentType := ctXML - else - ContentType := ctOther; - - Result := (ContentType <> ctOther); -end; - -function CompareHighlights(const AHL1: THighlight; const AHL2: THighlight): Integer; -begin - if AHL1.Start = AHL2.Start then - begin - Result := AHL1.Length - AHL2.Length; - end else - Result := AHL1.Start - AHL2.Start; -end; - -procedure WriteContent; -var - i: Integer; - matchPos, offset, lastPos: Integer; - data: String; - highlights: THighlights; - highlight: THighlight; -begin - highlights := THighlights.Create; - data := content.Text; - - offset := 0; - lastPos := 1; - - for i := 0 to filters.Count - 1 do - begin - if filters[i].Expression.Exec(data) then - begin - repeat - highlight.Start := filters[i].Expression.MatchPos[0]; - highlight.Length := filters[i].Expression.MatchLen[0]; - highlight.FGColor := filters[i].FGColor; - highlight.BGColor := filters[i].BGColor; - highlights.Add(highlight); - until not filters[i].Expression.ExecNext; - end; - end; - - highlights.Sort(@CompareHighlights); - - // Sanitize highlights - for i := 0 to highlights.Count - 2 do - begin - if (highlights[i].Start + highlights[i].Length) > highlights[i+1].Start then - begin - highlight := highlights[i]; - highlight.Length := highlights[i+1].Start - highlights[i].Start; - highlights[i] := highlight; - end; - end; - - for highlight in highlights do - begin - matchPos := highlight.Start; - offset := highlight.Length; - write(Copy(data, lastPos, matchPos - lastPos)); - if highlight.FGColor < $FF then - TextColor(highlight.FGColor); - if highlight.BGColor < $FF then - TextBackground(highlight.BGColor); - write(Copy(data, matchPos, offset)); - NormVideo; - lastPos := matchPos + offset; - end; - - writeln(Copy(data, lastPos, Length(data))); - - highlights.Free; -end; - -procedure ProcessCall(AURL: String); -var - s: String; - request, response: TStream; - jsonParser: TJSONParser; - jsonData: TJSONData; - contentType: TContentType; - xmlDoc: TXMLDocument; -begin - parser.Content := AURL; - parser.Replace; - AURL := parser.Content; - writeln('Calling ', AURL); - - response := TMemoryStream.Create; - request := nil; - - if content.Count > 0 then - begin - request := TMemoryStream.Create; - - // Variable replacement - parser.Content := content.Text; - parser.Replace; - content.Text := parser.Content; - - content.SaveToStream(request); - request.Position := 0; - http.RequestBody := request; - end; - - try - http.HTTPMethod(method, AURL, response, []); - except - on E: Exception do - begin - writeln; - writeln('Failed! ', E.Message); - Halt(1); - end; - end; - - writeln; - writeln('Status: ', http.ResponseStatusCode, ' (', http.ResponseStatusText, ')'); - writeln; - writeln('Headers:'); - for s in http.ResponseHeaders do - writeln(' ', s); - writeln; - - response.Position := 0; - - if beautify and IdentifyContentType(http.GetHeader(http.ResponseHeaders, 'Content-Type'), contentType) then - begin - case contentType of - ctJSON: - begin - jsonParser := TJSONParser.Create(response); - jsonData := jsonParser.Parse; - content.Text := jsonData.FormatJSON; - jsonData.Free; - jsonParser.Free; - WriteContent; - end; - ctXML: - begin - ReadXMLFile(xmlDoc, response); - response.Size := 0; - WriteXMLFile(xmlDoc, response); - response.Position := 0; - content.LoadFromStream(response); - WriteContent; - end; - end; - end else - begin - content.LoadFromStream(response); - WriteContent; - end; - - response.Free; - request.Free; -end; - -procedure CmdBasicAuth(AData: String); -var - separator: Char; - i: Integer; -begin - separator := AData[1]; - i := 2; - while (i < Length(AData)) and (AData[i] <> separator) do - Inc(i); - - http.UserName := Copy(AData, 2, i - 2); - http.Password := Copy(AData, i + 1, Length(AData)); -end; - -procedure CmdHighlight(AData: String); -begin - filters.Add(THighlightFilter.Create(AData)); -end; - -function ProcessCommand(ALine: String): Boolean; -begin - Result := False; - if AnsiStartsStr('Ask ', ALine) then - begin - Result := True; - CmdAskUser(Copy(ALine, 5, Length(ALine))); - end else - if AnsiStartsStr('Header ', ALine) then - begin - Result := True; - CmdHeader(Copy(ALine, 8, Length(ALine))); - end else - if AnsiStartsStr('Method ', ALine) then - begin - Result := True; - method := Copy(ALine, 8, Length(ALine)); - end else - // We don't include the space here, since we also interpret this following - // char as separator for username and password! (In case the username itself - // contains a space.) - if AnsiStartsStr('BasicAuth', ALine) then - begin - Result := True; - CmdBasicAuth(Copy(ALine, 10, Length(ALine))); - end else - if ALine = 'Beautify' then - begin - Result := True; - beautify := True; - end else - if AnsiStartsStr('Highlight ', ALine) then - begin - Result := True; - CmdHighlight(Copy(ALine, 11, Length(ALine))); - end else - if AnsiStartsStr('Call ', ALine) then - begin - Result := True; - url := Copy(ALine, 6, Length(ALine)); - end; -end; - -procedure ListProfiles; -var - sr: TSearchRec; -begin - Writeln('Known profiles:'); - if FindFirst(templateDir + '*.rest', faAnyFile, sr) = 0 then - begin - repeat - writeln(' ', Copy(sr.Name, 1, Length(sr.Name) - 5)); - until FindNext(sr) <> 0; - end; -end; - -begin - configDir := GetAppConfigDir(False); - templateDir := configDir + 'templates' + PathDelim; - - templateFile := templateDir + ParamStr(1) + '.rest'; - - if ParamCount <> 1 then - begin - Writeln('Usage: ', ExtractFileName(ParamStr(0)), ' '); - Writeln; - ListProfiles; - Halt(0); - end; - - if FileExists(ParamStr(1)) then - begin - AssignFile(data, ParamStr(1)); - templateName := ExtractFileName(ParamStr(1)); - if AnsiEndsStr('.rest', templateName) then - templateName := Copy(templateName, 1, Length(templateName) - 5); - end else - if FileExists(templateFile) then - begin - AssignFile(data, templateFile); - templateName := ParamStr(1); - end else - begin - writeln('Template not found!'); - Halt(1); - end; - Reset(data); - - sessionIni := TIniFile.Create(configDir + 'session.ini'); - - parser := TJTemplateParser.Create; - content := TStringList.Create; - beautify := False; //By default, we don't want to manipulate output. - filters := TFilterList.Create; - THighlightFilter.FilterExpression := TRegExpr.Create('( (FG|BG)(\d+))*$'); - THighlightFilter.ParamExpression := TRegExpr.Create('(FG|BG)(\d+)'); - - http := TFPHttpClient.Create(nil); - - commandMode := True; - - try - while not EOF(data) do - begin - ReadLn(data, line); - if commandMode and (line <> '') and (line[1] <> '#') and - not ProcessCommand(line) then - begin - commandMode := False; - end; - - if not commandMode then - content.Add(line); - end; - - if url <> '' then - ProcessCall(url); - - finally - sessionIni.Free; - parser.Free; - http.Free; - content.Free; - filters.Free; - THighlightFilter.FilterExpression.Free; - THighlightFilter.ParamExpression.Free; - - CloseFile(data); - end; + Application := TRestemplateApplication.Create; + Application.Title := 'restemplate'; + Application.Run; + Application.Free; end.