// *************************************************************************** // // Copyright (c) 2016-2019 Daniele Teti // // *************************************************************************** // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // *************************************************************************** unit TemplateProU; interface uses System.Generics.Collections, Classes, SysUtils, Data.DB, System.RTTI; type EParserException = class(Exception) end; ITPDataSourceAdapter = interface ['{9A0E5797-A8D2-413F-A8B0-5D6E67DD1701}'] function CurrentIndex: Int64; procedure Reset; function GetMemberValue(const aMemberName: string): string; procedure Next; function Eof: Boolean; end; TTPIdentifierControl = record Identifier: string; class function Create(aIdentifier: string): TTPIdentifierControl; static; end; TTPDatasetDictionary = class(TDictionary); TTPObjectListDictionary = class(TObjectDictionary < string, TObjectList < TObject >> ); TTPDatasetAdapter = class(TInterfacedObject, ITPDataSourceAdapter) private fDataSet: TDataSet; public constructor Create(const aDataSet: TDataSet); protected function CurrentIndex: Int64; procedure Reset; function GetMemberValue(const aMemberName: string): string; procedure Next; function Eof: Boolean; end; TTPObjectListAdapter = class(TInterfacedObject, ITPDataSourceAdapter) private class var CTX: TRttiContext; fObjectList: TObjectList; fIndex: Integer; public constructor Create(const aObjectList: TObjectList); class constructor Create; class destructor Destroy; protected function Current: TObject; function CurrentIndex: Int64; procedure Reset; function GetMemberValue(const aMemberName: string): string; procedure Next; function Eof: Boolean; end; TTemplateFunction = reference to function(aParameters: TArray; const aValue: string): string; TTemplateProEngine = class strict private fOutput: string; fVariables: TDictionary; function MatchStartTag: Boolean; function MatchEndTag: Boolean; function MatchIdentifier(var aIdentifier: string): Boolean; function MatchValue(var aValue: string): Boolean; function MatchReset(var aDataSet: string): Boolean; function MatchField(var aDataSet: string; var aFieldName: string): Boolean; function MatchSymbol(const aSymbol: string): Boolean; private fDataSources: TDictionary; fInputString: string; fCharIndex: Int64; fCurrentLine: Integer; fCurrentColumn: Integer; fLoopStack: TStack; fLoopIdentStack: TStack; fIfIdentStack: TStack; fCurrentDataSource: ITPDataSourceAdapter; fOutputStreamWriter: TStreamWriter; fEncoding: TEncoding; fTemplateFunctions: TDictionary; fInThen: Boolean; fInElse: Boolean; procedure Error(const aMessage: string); procedure ErrorFmt(const aMessage: string; aParameters: array of const); function ExecuteFunction(aFunctionName: string; aParameters: TArray; aValue: string): string; function ExecuteFieldFunction(aFunctionName: string; aParameters: TArray; aValue: TValue): string; function SetDataSourceByName(const aName: string): Boolean; function GetDataSourceByName(const aName: string; out aDataSource: ITPDataSourceAdapter): Boolean; function GetFieldText(const aFieldName: string): string; procedure CheckParNumber(const aHowManyPars: Integer; const aParameters: TArray); overload; procedure CheckParNumber(const aMinParNumber, aMaxParNumber: Integer; const aParameters: TArray); overload; procedure AppendOutput(const aValue: string); procedure LoadDataSources(const aObjectDictionary: TTPObjectListDictionary; const aDatasetDictionary: TTPDatasetDictionary); public procedure Execute(const aTemplateString: string; const aObjectDictionary: TTPObjectListDictionary; const aDatasetDictionary: TTPDatasetDictionary; aStream: TStream); overload; procedure Execute(const aTemplateString: string; const aObjectNames: array of string; aObjects: array of TObjectList; const aDataSetNames: array of string; aDataSets: array of TDataSet; aStream: TStream); overload; procedure Execute(const aTemplateString: string; aStream: TStream); overload; constructor Create(aEncoding: TEncoding = nil); destructor Destroy; override; procedure SetVar(const aName: string; aValue: string); function GetVar(const aName: string): string; procedure ClearVariables; function IsIndentifierTrue(const aIdentifier: string): Boolean; procedure AddTemplateFunction(const FunctionName: string; const FunctionImpl: TTemplateFunction); end; function HTMLEntitiesEncode(s: string): string; implementation uses System.StrUtils; const IdenfierAllowedFirstChars = ['a' .. 'z', 'A' .. 'Z', '_']; IdenfierAllowedChars = IdenfierAllowedFirstChars + ['0' .. '9']; ValueAllowedChars = IdenfierAllowedChars + [' ', '-', '+', '*', '.', '@', '/', '\']; // maybe a lot others START_TAG_1 = '{{'; END_TAG_1 = '}}'; { TParser } procedure TTemplateProEngine.AddTemplateFunction(const FunctionName: string; const FunctionImpl: TTemplateFunction); begin fTemplateFunctions.Add(FunctionName.ToLower, FunctionImpl); end; procedure TTemplateProEngine.AppendOutput(const aValue: string); begin fOutputStreamWriter.Write(aValue); end; procedure TTemplateProEngine.CheckParNumber(const aMinParNumber, aMaxParNumber: Integer; const aParameters: TArray); var lParNumber: Integer; begin lParNumber := Length(aParameters); if (lParNumber < aMinParNumber) or (lParNumber > aMaxParNumber) then begin if aMinParNumber = aMaxParNumber then Error(Format('Expected %d parameters, got %d', [aMinParNumber, lParNumber])) else Error(Format('Expected from %d to %d parameters, got %d', [aMinParNumber, aMaxParNumber, lParNumber])); end; end; procedure TTemplateProEngine.ClearVariables; begin fVariables.Clear; end; constructor TTemplateProEngine.Create(aEncoding: TEncoding = nil); begin inherited Create; if aEncoding = nil then fEncoding := TEncoding.UTF8 { default encoding } else fEncoding := aEncoding; fOutput := ''; fVariables := TDictionary.Create; fLoopStack := TStack.Create; fLoopIdentStack := TStack.Create; fIfIdentStack := TStack.Create; fDataSources := TDictionary.Create; fTemplateFunctions := TDictionary.Create; end; destructor TTemplateProEngine.Destroy; begin fTemplateFunctions.Free; fDataSources.Free; fLoopIdentStack.Free; fIfIdentStack.Free; fLoopStack.Free; fVariables.Free; fOutputStreamWriter.Free; inherited; end; function TTemplateProEngine.SetDataSourceByName(const aName: string): Boolean; var ds: TPair; begin { TODO -oDanieleT -cGeneral : Refactor this method to use GetDataSourceByName } Result := False; for ds in fDataSources do begin if SameText(ds.Key, aName) then begin fCurrentDataSource := ds.Value; Result := True; Break; end; end; end; function TTemplateProEngine.GetDataSourceByName(const aName: string; out aDataSource: ITPDataSourceAdapter): Boolean; var ds: TPair; begin Result := False; for ds in fDataSources do begin if SameText(ds.Key, aName) then begin aDataSource := ds.Value; Result := True; Break; end; end; end; function TTemplateProEngine.GetFieldText(const aFieldName: string): string; begin if not Assigned(fCurrentDataSource) then Error('Current datasource not set'); Result := fCurrentDataSource.GetMemberValue(aFieldName); end; function TTemplateProEngine.GetVar(const aName: string): string; begin if not fVariables.TryGetValue(aName, Result) then Result := ''; end; function TTemplateProEngine.IsIndentifierTrue(const aIdentifier: string): Boolean; var lDataSource: ITPDataSourceAdapter; begin if SameText(aIdentifier, 'true') then Exit(True); if SameText(aIdentifier, 'false') then Exit(False); if SameText(GetVar(aIdentifier), 'true') then begin Exit(True); end; if SameText(GetVar(aIdentifier), 'false') then begin Exit(False); end; Result := not GetVar(aIdentifier).IsEmpty; if Result then Exit; if GetDataSourceByName(aIdentifier, lDataSource) then begin Result := not lDataSource.Eof; end else begin Result := False; end; end; procedure TTemplateProEngine.LoadDataSources(const aObjectDictionary: TTPObjectListDictionary; const aDatasetDictionary: TTPDatasetDictionary); var lDatasetPair: TPair; lObjectPair: TPair>; begin fDataSources.Clear; for lDatasetPair in aDatasetDictionary do begin fDataSources.Add(lDatasetPair.Key, TTPDatasetAdapter.Create(lDatasetPair.Value)); end; for lObjectPair in aObjectDictionary do begin fDataSources.Add(lObjectPair.Key, TTPObjectListAdapter.Create(lObjectPair.Value)); end; end; function TTemplateProEngine.MatchEndTag: Boolean; begin Result := END_TAG_1 = fInputString.Substring(fCharIndex, Length(END_TAG_1)); if Result then begin Inc(fCharIndex, END_TAG_1.Length - 1); if (fInputString.Substring(fCharIndex + 1, 1) = #13) and (fInputString.Substring(fCharIndex + 2, 1) = #10) then begin Inc(fCharIndex, 2); end; end; end; function TTemplateProEngine.MatchField(var aDataSet: string; var aFieldName: string): Boolean; begin Result := False; aFieldName := ''; if not MatchSymbol(':') then Exit; if not MatchIdentifier(aDataSet) then Error('Expected dataset name'); if not MatchSymbol('.') then Error('Expected "."'); if not MatchIdentifier(aFieldName) then Error('Expected field name'); Result := True; end; function TTemplateProEngine.MatchIdentifier(var aIdentifier: string): Boolean; begin aIdentifier := ''; Result := False; if CharInSet(fInputString.Chars[fCharIndex], IdenfierAllowedFirstChars) then begin while CharInSet(fInputString.Chars[fCharIndex], IdenfierAllowedChars) do begin aIdentifier := aIdentifier + fInputString.Chars[fCharIndex]; Inc(fCharIndex); end; Result := True; end end; function TTemplateProEngine.MatchReset(var aDataSet: string): Boolean; begin if not MatchSymbol('reset') then Exit(False); Result := MatchSymbol('(') and MatchIdentifier(aDataSet) and MatchSymbol(')'); end; function TTemplateProEngine.MatchStartTag: Boolean; begin Result := START_TAG_1 = fInputString.Substring(fCharIndex, Length(START_TAG_1)); if Result then Inc(fCharIndex, START_TAG_1.Length); end; function TTemplateProEngine.MatchSymbol(const aSymbol: string): Boolean; var lSymbolIndex: Integer; lSavedCharIndex: Int64; begin if aSymbol.IsEmpty then Exit(True); lSavedCharIndex := fCharIndex; lSymbolIndex := 0; // lChar := FInputString.Chars[FCharIndex]; while fInputString.Chars[fCharIndex] = aSymbol.Chars[lSymbolIndex] do begin Inc(fCharIndex); Inc(lSymbolIndex); // lChar := FInputString.Chars[FCharIndex] end; Result := (lSymbolIndex > 0) and (lSymbolIndex = Length(aSymbol)); if not Result then fCharIndex := lSavedCharIndex; end; function TTemplateProEngine.MatchValue(var aValue: string): Boolean; begin aValue := ''; while CharInSet(fInputString.Chars[fCharIndex], ValueAllowedChars) do begin aValue := aValue + fInputString.Chars[fCharIndex]; Inc(fCharIndex); end; Result := not aValue.IsEmpty; end; procedure TTemplateProEngine.Execute(const aTemplateString: string; const aObjectDictionary: TTPObjectListDictionary; const aDatasetDictionary: TTPDatasetDictionary; aStream: TStream); var lChar: Char; lVarName: string; lFuncName: string; lIdentifier: string; lDataSet: string; lFieldName: string; lFuncParams: TArray; lDataSourceName: string; function GetFunctionParameters: TArray; var lFuncPar: string; begin Result := []; while MatchSymbol(':') do begin lFuncPar := ''; if not MatchValue(lFuncPar) then Error('Expected function parameter'); Result := Result + [lFuncPar]; end; end; procedure Step; begin Inc(fCharIndex); lChar := aTemplateString.Chars[fCharIndex]; if lChar = #13 then begin Inc(fCurrentLine); fCurrentColumn := 1; end else begin Inc(fCurrentColumn); end; end; begin FreeAndNil(fOutputStreamWriter); fOutputStreamWriter := TStreamWriter.Create(aStream, fEncoding); LoadDataSources(aObjectDictionary, aDatasetDictionary); fLoopStack.Clear; fLoopIdentStack.Clear; fCharIndex := -1; fCurrentLine := 1; fCurrentColumn := 0; fInputString := aTemplateString; while fCharIndex < aTemplateString.Length do begin // Step; // starttag if MatchStartTag then begin // loop if MatchSymbol('loop') then begin if not MatchSymbol('(') then Error('Expected "("'); if not MatchIdentifier(lIdentifier) then Error('Expected identifier after "loop("'); if not MatchSymbol(')') then Error('Expected ")" after "' + lIdentifier + '"'); if not MatchEndTag then Error('Expected closing tag for "loop(' + lIdentifier + ')"'); if not SetDataSourceByName(lIdentifier) then Error('Unknown dataset: ' + lIdentifier); fLoopStack.Push(fCharIndex); fLoopIdentStack.Push(TTPIdentifierControl.Create(lIdentifier)); Continue; end; // endloop if MatchSymbol('endloop') then begin if not MatchEndTag then Error('Expected closing tag'); lIdentifier := fLoopIdentStack.Peek.Identifier; if not SetDataSourceByName(lIdentifier) then Error('Invalid datasource name: ' + lIdentifier); // fCurrentDataSource.Next; if fCurrentDataSource.Eof then begin fLoopIdentStack.Pop; fLoopStack.Pop; end else begin fCurrentDataSource.Next; fCharIndex := fLoopStack.Peek; end; Continue; end; if MatchSymbol('endif') then begin if fIfIdentStack.Count = 0 then begin Error('"endif" without "if"'); end; fIfIdentStack.Pop; if not MatchEndTag then Error('Expected closing tag for "endif"'); end; if MatchSymbol('if') then begin if not MatchSymbol('(') then Error('Expected "("'); if not MatchIdentifier(lIdentifier) then Error('Expected identifier after "if("'); if not MatchSymbol(')') then Error('Expected ")" after "' + lIdentifier + '"'); if not MatchEndTag then Error('Expected closing tag for "if(' + lIdentifier + ')"'); if IsIndentifierTrue(lIdentifier) then begin fIfIdentStack.Push(TTPIdentifierControl.Create('')); fInThen := True; Continue; end else begin // while not(MatchStartTag and MatchSymbol('else') and MatchEndTag) do // begin // Step; // end; while True do begin if not MatchStartTag then begin Step; end else begin if MatchSymbol('else') and MatchEndTag then begin fIfIdentStack.Push(TTPIdentifierControl.Create('')); fInElse := True; Break; end else if MatchSymbol('endif') and MatchEndTag then begin fIfIdentStack.Pop; Break; end; end; end; Continue; end; end; if MatchSymbol('else') then begin if not fInThen then Error('"else" without if'); fInThen := False; if not MatchEndTag then Error('Expected end-tag'); while not(MatchStartTag and MatchSymbol('endif') and MatchEndTag) do begin Step; end; Continue; end; // dataset field if MatchField(lDataSourceName, lFieldName) then begin if lFieldName.IsEmpty then Error('Invalid field name'); lFuncName := ''; if MatchSymbol('|') then begin if not MatchIdentifier(lFuncName) then Error('Invalid function name'); lFuncParams := GetFunctionParameters; if not MatchEndTag then Error('Expected end tag'); end else begin if not MatchEndTag then Error('Expected closing tag'); if not SetDataSourceByName(lDataSourceName) then Error('Unknown datasource: ' + lDataSourceName); end; if lFuncName.IsEmpty then AppendOutput(GetFieldText(lFieldName)) else AppendOutput(ExecuteFieldFunction(lFuncName, lFuncParams, GetFieldText(lFieldName))); end; // reset if MatchReset(lDataSet) then begin if not MatchEndTag then Error('Expected closing tag'); SetDataSourceByName(lDataSet); fCurrentDataSource.Reset; Continue; end; // identifier if MatchIdentifier(lVarName) then begin if lVarName.IsEmpty then Error('Invalid variable name'); lFuncName := ''; if MatchSymbol('|') then begin if not MatchIdentifier(lFuncName) then Error('Invalid function name'); lFuncParams := GetFunctionParameters; if not MatchEndTag then Error('Expected end tag'); AppendOutput(ExecuteFunction(lFuncName, lFuncParams, GetVar(lVarName))); end else begin if not MatchEndTag then Error('Expected end tag'); AppendOutput(GetVar(lVarName)); end; end; end else begin // output verbatim AppendOutput(lChar); end; end; end; procedure TTemplateProEngine.SetVar(const aName: string; aValue: string); begin fVariables.AddOrSetValue(aName, aValue); end; function CapitalizeString(const s: string; const CapitalizeFirst: Boolean): string; const ALLOWEDCHARS = ['a' .. 'z', '_']; var index: Integer; bCapitalizeNext: Boolean; begin bCapitalizeNext := CapitalizeFirst; Result := lowercase(s); if Result <> EmptyStr then begin for index := 1 to Length(Result) do begin if bCapitalizeNext then begin Result[index] := UpCase(Result[index]); bCapitalizeNext := False; end else if not CharInSet(Result[index], ALLOWEDCHARS) then begin bCapitalizeNext := True; end; end; // for end; // if end; procedure TTemplateProEngine.Error(const aMessage: string); begin raise EParserException.CreateFmt('%s - at line %d col %d', [aMessage, fCurrentLine, fCurrentColumn]); end; procedure TTemplateProEngine.ErrorFmt(const aMessage: string; aParameters: array of const); begin Error(Format(aMessage, aParameters)); end; procedure TTemplateProEngine.CheckParNumber(const aHowManyPars: Integer; const aParameters: TArray); begin CheckParNumber(aHowManyPars, aHowManyPars, aParameters); end; function TTemplateProEngine.ExecuteFunction(aFunctionName: string; aParameters: TArray; aValue: string): string; var lFunc: TTemplateFunction; begin aFunctionName := lowercase(aFunctionName); if aFunctionName = 'tohtml' then begin Exit(HTMLEntitiesEncode(aValue)); end; if aFunctionName = 'uppercase' then begin Exit(UpperCase(aValue)); end; if aFunctionName = 'lowercase' then begin Exit(lowercase(aValue)); end; if aFunctionName = 'capitalize' then begin Exit(CapitalizeString(aValue, True)); end; if aFunctionName = 'rpad' then begin CheckParNumber(1, 2, aParameters); if Length(aParameters) = 1 then Exit(aValue.PadRight(aParameters[0].ToInteger)) else Exit(aValue.PadRight(aParameters[0].ToInteger, aParameters[1].Chars[0])); end; if aFunctionName = 'lpad' then begin if Length(aParameters) = 1 then Exit(aValue.PadLeft(aParameters[0].ToInteger)) else Exit(aValue.PadLeft(aParameters[0].ToInteger, aParameters[1].Chars[0])); end; if not fTemplateFunctions.TryGetValue(aFunctionName, lFunc) then begin raise EParserException.CreateFmt('Unknown function [%s]', [aFunctionName]); end; Result := lFunc(aParameters, aValue); end; procedure TTemplateProEngine.Execute(const aTemplateString: string; const aObjectNames: array of string; aObjects: array of TObjectList; const aDataSetNames: array of string; aDataSets: array of TDataSet; aStream: TStream); var lDatasets: TTPDatasetDictionary; lObjects: TTPObjectListDictionary; I: Integer; begin if Length(aObjectNames) <> Length(aObjects) then ErrorFmt('Wrong Names/Objects count. Names: %d, Objects: %d', [Length(aObjectNames), Length(aObjects)]); if Length(aDataSetNames) <> Length(aDataSets) then ErrorFmt('Wrong Names/DataSets count. Names: %d, DataSets: %d', [Length(aDataSetNames), Length(aDataSets)]); lDatasets := TTPDatasetDictionary.Create; try for I := 0 to Length(aDataSetNames) - 1 do begin lDatasets.Add(aDataSetNames[I], aDataSets[I]); end; lObjects := TTPObjectListDictionary.Create([]); try for I := 0 to Length(aObjectNames) - 1 do begin lObjects.Add(aObjectNames[I], aObjects[I]); end; Execute(aTemplateString, lObjects, lDatasets, aStream); finally lObjects.Free; end; finally lDatasets.Free; end; end; procedure TTemplateProEngine.Execute(const aTemplateString: string; aStream: TStream); begin Execute(aTemplateString, [], [], [], [], aStream); end; function TTemplateProEngine.ExecuteFieldFunction(aFunctionName: string; aParameters: TArray; aValue: TValue): string; var lDateValue: TDate; lDateTimeValue: TDateTime; lStrValue: string; begin aFunctionName := lowercase(aFunctionName); if aFunctionName = 'tohtml' then begin Exit(HTMLEntitiesEncode(aValue.AsString)); end; if aFunctionName = 'uppercase' then begin Exit(UpperCase(aValue.AsString)); end; if aFunctionName = 'lowercase' then begin Exit(lowercase(aValue.AsString)); end; if aFunctionName = 'capitalize' then begin Exit(CapitalizeString(aValue.AsString, True)); end; if aFunctionName = 'rpad' then begin if aValue.IsType then lStrValue := aValue.AsInteger.ToString else if aValue.IsType then lStrValue := aValue.AsString else ErrorFmt('Invalid parameter/s for function: %s', [aFunctionName]); CheckParNumber(1, 2, aParameters); if Length(aParameters) = 1 then begin Exit(lStrValue.PadRight(aParameters[0].ToInteger)); end else begin Exit(lStrValue.PadRight(aParameters[0].ToInteger, aParameters[1].Chars[0])); end; end; if aFunctionName = 'lpad' then begin if aValue.IsType then lStrValue := aValue.AsInteger.ToString else if aValue.IsType then lStrValue := aValue.AsString else ErrorFmt('Invalid parameter/s for function: ', [aFunctionName]); CheckParNumber(1, 2, aParameters); if Length(aParameters) = 1 then begin Exit(lStrValue.PadLeft(aParameters[0].ToInteger)); end else begin Exit(lStrValue.PadLeft(aParameters[0].ToInteger, aParameters[1].Chars[0])); end; end; if aFunctionName = 'datetostr' then begin if not aValue.TryAsType(lDateValue) then Error('Invalid Date'); Exit(DateToStr(lDateValue)); end; if aFunctionName = 'datetimetostr' then begin if not aValue.TryAsType(lDateTimeValue) then Error('Invalid DateTime'); Exit(DateTimeToStr(lDateTimeValue)); end; if aFunctionName = 'formatdatetime' then begin CheckParNumber(1, aParameters); if not aValue.TryAsType(lDateTimeValue) then Error('Invalid DateTime'); Exit(FormatDateTime(aParameters[0], lDateTimeValue)); end; ErrorFmt('Unknown function [%s]', [aFunctionName]); end; class function TTPIdentifierControl.Create(aIdentifier: string): TTPIdentifierControl; begin Result.Identifier := aIdentifier; end; { TTPDatasetAdapter } constructor TTPDatasetAdapter.Create(const aDataSet: TDataSet); begin inherited Create; fDataSet := aDataSet; end; function TTPDatasetAdapter.CurrentIndex: Int64; begin Result := fDataSet.RecNo; end; function TTPDatasetAdapter.Eof: Boolean; begin Result := fDataSet.Eof; end; function TTPDatasetAdapter.GetMemberValue(const aMemberName: string): string; begin Result := fDataSet.FieldByName(aMemberName).AsWideString; end; procedure TTPDatasetAdapter.Next; begin fDataSet.Next; end; procedure TTPDatasetAdapter.Reset; begin fDataSet.First; end; { TTPObjectListAdapter } constructor TTPObjectListAdapter.Create(const aObjectList: TObjectList); begin inherited Create; fObjectList := aObjectList; if fObjectList.Count > 0 then fIndex := 0 else fIndex := -1; end; class constructor TTPObjectListAdapter.Create; begin TTPObjectListAdapter.CTX := TRttiContext.Create; end; function TTPObjectListAdapter.Current: TObject; begin if fIndex <> -1 then Result := fObjectList[fIndex] else raise Exception.Create('Empty DataSource'); end; function TTPObjectListAdapter.CurrentIndex: Int64; begin Result := fIndex; end; class destructor TTPObjectListAdapter.Destroy; begin TTPObjectListAdapter.CTX.Free; end; function TTPObjectListAdapter.Eof: Boolean; begin Result := fIndex = fObjectList.Count - 1; end; function TTPObjectListAdapter.GetMemberValue(const aMemberName: string): string; var lRttiType: TRttiType; lRttiProp: TRttiProperty; lCurrentObj: TObject; begin lCurrentObj := Current; lRttiType := CTX.GetType(lCurrentObj.ClassInfo); lRttiProp := lRttiType.GetProperty(aMemberName); Result := lRttiProp.GetValue(lCurrentObj).AsString; end; procedure TTPObjectListAdapter.Next; begin if Eof then raise Exception.Create('DataSource is already at EOF'); Inc(fIndex); end; procedure TTPObjectListAdapter.Reset; begin if fObjectList.Count > 0 then fIndex := 0 else fIndex := -1; end; function HTMLEntitiesEncode(s: string): string; procedure repl(var s: string; r: string; posi: Integer); begin delete(s, posi, 1); insert(r, s, posi); end; var I: Integer; r: string; begin I := 0; while I < Length(s) do begin r := ''; case ord(s[I]) of 160: r := 'nbsp'; 161: r := 'excl'; 162: r := 'cent'; 163: r := 'ound'; 164: r := 'curren'; 165: r := 'yen'; 166: r := 'brvbar'; 167: r := 'sect'; 168: r := 'uml'; 169: r := 'copy'; 170: r := 'ordf'; 171: r := 'laquo'; 172: r := 'not'; 173: r := 'shy'; 174: r := 'reg'; 175: r := 'macr'; 176: r := 'deg'; 177: r := 'plusmn'; 178: r := 'sup2'; 179: r := 'sup3'; 180: r := 'acute'; 181: r := 'micro'; 182: r := 'para'; 183: r := 'middot'; 184: r := 'cedil'; 185: r := 'sup1'; 186: r := 'ordm'; 187: r := 'raquo'; 188: r := 'frac14'; 189: r := 'frac12'; 190: r := 'frac34'; 191: r := 'iquest'; 192: r := 'Agrave'; 193: r := 'Aacute'; 194: r := 'Acirc'; 195: r := 'Atilde'; 196: r := 'Auml'; 197: r := 'Aring'; 198: r := 'AElig'; 199: r := 'Ccedil'; 200: r := 'Egrave'; 201: r := 'Eacute'; 202: r := 'Ecirc'; 203: r := 'Euml'; 204: r := 'Igrave'; 205: r := 'Iacute'; 206: r := 'Icirc'; 207: r := 'Iuml'; 208: r := 'ETH'; 209: r := 'Ntilde'; 210: r := 'Ograve'; 211: r := 'Oacute'; 212: r := 'Ocirc'; 213: r := 'Otilde'; 214: r := 'Ouml'; 215: r := 'times'; 216: r := 'Oslash'; 217: r := 'Ugrave'; 218: r := 'Uacute'; 219: r := 'Ucirc'; 220: r := 'Uuml'; 221: r := 'Yacute'; 222: r := 'THORN'; 223: r := 'szlig'; 224: r := 'agrave'; 225: r := 'aacute'; 226: r := 'acirc'; 227: r := 'atilde'; 228: r := 'auml'; 229: r := 'aring'; 230: r := 'aelig'; 231: r := 'ccedil'; 232: r := 'egrave'; 233: r := 'eacute'; 234: r := 'ecirc'; 235: r := 'euml'; 236: r := 'igrave'; 237: r := 'iacute'; 238: r := 'icirc'; 239: r := 'iuml'; 240: r := 'eth'; 241: r := 'ntilde'; 242: r := 'ograve'; 243: r := 'oacute'; 244: r := 'ocirc'; 245: r := 'otilde'; 246: r := 'ouml'; 247: r := 'divide'; 248: r := 'oslash'; 249: r := 'ugrave'; 250: r := 'uacute'; 251: r := 'ucirc'; 252: r := 'uuml'; 253: r := 'yacute'; 254: r := 'thorn'; 255: r := 'yuml'; end; if r <> '' then begin repl(s, '&' + r + ';', I); end; Inc(I) end; Result := s; end; end.