mirror of
				https://gitlab.com/freepascal.org/lazarus/lazarus.git
				synced 2025-11-04 15:32:00 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			433 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			ObjectPascal
		
	
	
	
	
	
			
		
		
	
	
			433 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			ObjectPascal
		
	
	
	
	
	
{
 | 
						|
 ***************************************************************************
 | 
						|
 *                                                                         *
 | 
						|
 *   This source 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 2 of the License, or     *
 | 
						|
 *   (at your option) any later version.                                   *
 | 
						|
 *                                                                         *
 | 
						|
 *   This code 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.                              *
 | 
						|
 *                                                                         *
 | 
						|
 *   A copy of the GNU General Public License is available on the World    *
 | 
						|
 *   Wide Web at <http://www.gnu.org/copyleft/gpl.html>. You can also      *
 | 
						|
 *   obtain it by writing to the Free Software Foundation,                 *
 | 
						|
 *   Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.        *
 | 
						|
 *                                                                         *
 | 
						|
 ***************************************************************************
 | 
						|
 | 
						|
  Author: Donald Ziesig
 | 
						|
 | 
						|
  Abstract
 | 
						|
 | 
						|
  TTemplateExpander provides xml templates to replace the hard-coded
 | 
						|
  pascal snippets used by TCodeCompletionCodeTool.
 | 
						|
 | 
						|
  The xml file fragment for the Setter method:
 | 
						|
 | 
						|
  <templates>
 | 
						|
     *
 | 
						|
     *
 | 
						|
     *
 | 
						|
  <template name="SetterMethod">
 | 
						|
  procedure <ClassName>.<AccessParam/>(<PropVarName/>: <PropType/>);<br/>
 | 
						|
  begin
 | 
						|
  <indent/>if <VarName/>=<PropVarName/> then Exit;<br/>
 | 
						|
  <indent/><VarName/>:=<PropVarName/>;<br/>
 | 
						|
  end;<br/>
 | 
						|
  </template>
 | 
						|
     *
 | 
						|
     *
 | 
						|
     *
 | 
						|
  </templates>
 | 
						|
 | 
						|
  produces pascal:
 | 
						|
 | 
						|
  procedure TMyClass.SetMyVar(AValue: MyType);
 | 
						|
  begin
 | 
						|
    if AValue=MyVar then Exit;
 | 
						|
    MyVar:=AValue;
 | 
						|
  end;
 | 
						|
 | 
						|
===============================================================================
 | 
						|
 | 
						|
  The following xml tags are implemented:
 | 
						|
 | 
						|
  <if var="SomeBool"> ... </if>      generates pascal code if string value of
 | 
						|
                                     the argument named "SomeBool" is "true".
 | 
						|
 | 
						|
  <ifnot var="SomeBool" ... </ifnot> generates pascal code if string value of
 | 
						|
                                     the argument named "SomeBool" is not "true"
 | 
						|
 | 
						|
  <else> ... </else>                 must immediately follow </if> or </ifnot>.
 | 
						|
                                     generates pascal code if the negation of
 | 
						|
                                     the previous tag is true.
 | 
						|
 | 
						|
  <count var="SomeVar"> ... </count> generates pascal code for zero or more
 | 
						|
                                     values of the string argument "SomeVar".
 | 
						|
                                     The values are encoded as a single string
 | 
						|
                                     of the form "Arg0?Arg1?...ArgN?" (Yeah, I
 | 
						|
                                     know.  See the comments below about a hack.)
 | 
						|
 | 
						|
  <indent/>                          is replaced by the appropriate Indent string.
 | 
						|
 | 
						|
  <br/>                              is replaced by the appropriate LineEnd string.
 | 
						|
 | 
						|
  <SomeVar/>                         is replaced by the string element of ArgVal
 | 
						|
                                     which corresponds to the string element of
 | 
						|
                                     ArgName.
 | 
						|
 | 
						|
}
 | 
						|
unit CodeCompletionTemplater;
 | 
						|
 | 
						|
{$mode objfpc}{$H+}
 | 
						|
 | 
						|
interface
 | 
						|
 | 
						|
uses
 | 
						|
  Classes, SysUtils, laz2_DOM, laz2_XMLRead, LazFileUtils, LazUTF8Classes,
 | 
						|
  CodeCache, FileProcs;
 | 
						|
 | 
						|
type
 | 
						|
 | 
						|
  { TTemplateExpander }
 | 
						|
 | 
						|
  TTemplateExpander = class
 | 
						|
  private
 | 
						|
    FCode: TCodeBuffer;
 | 
						|
    fCodeChangeStep: Integer;
 | 
						|
    procedure LoadCode;
 | 
						|
    procedure SetCode(AValue: TCodeBuffer);
 | 
						|
  protected
 | 
						|
    XMLDoc : TXMLDocument;
 | 
						|
    Root   : TDOMNode;
 | 
						|
 | 
						|
    function ArgCount(Args: String): Integer;
 | 
						|
    function ArgN(Args: String; Index : Integer) : String;
 | 
						|
 | 
						|
    function ExpandTemplate(Template: TDOMNode;
 | 
						|
                            LineEnd, Indent : String;
 | 
						|
                            ArgName: array of String;
 | 
						|
                            ArgVal: array of const;
 | 
						|
                            CountIndex: Integer = -1): String;
 | 
						|
 | 
						|
    function FindTemplate(TemplateName: String): TDOMNode;
 | 
						|
  public
 | 
						|
    constructor Create;
 | 
						|
    destructor  Destroy; override;
 | 
						|
 | 
						|
    function Expand(TemplateName : String;
 | 
						|
                    LineEnd, Indent : String;
 | 
						|
                    ArgName : array of String;
 | 
						|
                    ArgVal  : array of const): String;
 | 
						|
 | 
						|
    function TemplateExists(TemplateName: String): Boolean;
 | 
						|
 | 
						|
    property Doc: TXMLDocument read XMLDoc;
 | 
						|
    property Code: TCodeBuffer read FCode write SetCode;
 | 
						|
    procedure ReloadCode;
 | 
						|
  end;
 | 
						|
 | 
						|
var
 | 
						|
  CTTemplateExpander : TTemplateExpander; // will be set by CodeToolBoss
 | 
						|
 | 
						|
implementation
 | 
						|
 | 
						|
{ TTemplateExpander }
 | 
						|
 | 
						|
//
 | 
						|
// ArgCount and ArgN are part of a hack to overcome the fact that
 | 
						|
// "array of const" can not contain another array.
 | 
						|
//
 | 
						|
// instead of a convenient ['a',...,['x1,'x2'],...,'z'] we must use something
 | 
						|
// like ['a',...,'x1?x2?',...'z'] (which is what I chose just for simplicity)
 | 
						|
//
 | 
						|
 | 
						|
function TTemplateExpander.ArgCount(Args: String): Integer;
 | 
						|
var
 | 
						|
  I : Integer;
 | 
						|
begin
 | 
						|
  Result := 0;
 | 
						|
  for I := 1 to Length(Args) do
 | 
						|
    if Args[I] = '?' then
 | 
						|
      Inc(Result);
 | 
						|
end;
 | 
						|
 | 
						|
function TTemplateExpander.ArgN(Args: String; Index: Integer): String;
 | 
						|
var
 | 
						|
  I : Integer;
 | 
						|
  P : Integer;
 | 
						|
  S : String;
 | 
						|
begin
 | 
						|
  S := Args;
 | 
						|
  for I := 0 to pred(Index) do
 | 
						|
    begin
 | 
						|
      P := Pos('?',S);
 | 
						|
      S := Copy(S,P+1,65535);
 | 
						|
    end;
 | 
						|
  P := Pos('?',S);
 | 
						|
  Result := Copy(S,1,P-1);
 | 
						|
end;
 | 
						|
 | 
						|
constructor TTemplateExpander.Create;
 | 
						|
begin
 | 
						|
  fCodeChangeStep:=CTInvalidChangeStamp;
 | 
						|
end;
 | 
						|
 | 
						|
destructor TTemplateExpander.Destroy;
 | 
						|
begin
 | 
						|
  FCode:=nil;
 | 
						|
  FreeAndNil(XMLDoc);
 | 
						|
end;
 | 
						|
 | 
						|
function TTemplateExpander.Expand(TemplateName: String; LineEnd,
 | 
						|
  Indent: String; ArgName: array of String; ArgVal: array of const): String;
 | 
						|
var
 | 
						|
  Template : TDOMNode;
 | 
						|
begin
 | 
						|
  Template := FindTemplate(TemplateName);
 | 
						|
  if Template = nil then
 | 
						|
    raise Exception.Create('Template "' + TemplateName + '" not found in TemplateExpander.');
 | 
						|
  Result := ExpandTemplate(Template, #13#10, '  ', ArgName, ArgVal);
 | 
						|
end;
 | 
						|
 | 
						|
function TTemplateExpander.ExpandTemplate(Template: TDOMNode; LineEnd,
 | 
						|
  Indent: String; ArgName: array of String; ArgVal: array of const;
 | 
						|
  CountIndex : Integer): String;
 | 
						|
 | 
						|
// Sequential search of ArgName array to return corresponding element
 | 
						|
// of the ArgVal array (appropriately processed if it simulates being
 | 
						|
// an array itself.
 | 
						|
function GetArgValue(Name : String; Index : Integer = -1): String;
 | 
						|
var
 | 
						|
  I : Integer;
 | 
						|
  S : String;
 | 
						|
begin
 | 
						|
  for I := 0 to pred(Length(ArgName)) do
 | 
						|
    if ArgName[I] = Name then
 | 
						|
      begin
 | 
						|
        S := AnsiString(ArgVal[I].VAnsiString);
 | 
						|
        if (Index < 0) or (Pos('?',S) = 0) then
 | 
						|
          Result := S
 | 
						|
        else
 | 
						|
          Result :=  ArgN(S, Index);
 | 
						|
        exit;
 | 
						|
      end;
 | 
						|
  raise Exception.Create('ExpandTemplate could not find Argument named "' + Name + '"');
 | 
						|
end;
 | 
						|
 | 
						|
function GetBoolArgValue(Name : String; Index : Integer = -1): Boolean;
 | 
						|
var
 | 
						|
  I : Integer;
 | 
						|
begin
 | 
						|
  for I := 0 to pred(Length(ArgName)) do
 | 
						|
    if ArgName[I] = Name then
 | 
						|
      begin
 | 
						|
        Result :=  ArgVal[I].VBoolean;
 | 
						|
        exit;
 | 
						|
      end;
 | 
						|
  raise Exception.Create('ExpandTemplate could not find Argument named "' + Name + '"');
 | 
						|
end;
 | 
						|
 | 
						|
function GetNodeValue(Node : TDOMNode; Required : Boolean = True): String;
 | 
						|
var
 | 
						|
  Len : Integer;
 | 
						|
begin
 | 
						|
  Result := '';
 | 
						|
  Len := Node.Attributes.Length;
 | 
						|
  if Required then
 | 
						|
    if Len = 0 then
 | 
						|
      raise Exception.Create('Missing attribute tag for node "' + Node.NodeName + '"');
 | 
						|
  if Len > 0 then
 | 
						|
    Result := Node.Attributes.Item[0].NodeValue;
 | 
						|
end;
 | 
						|
 | 
						|
var
 | 
						|
  Node : TDOMNode;
 | 
						|
  N : String;
 | 
						|
  S : String;
 | 
						|
  Name : String;
 | 
						|
  R : String; // for debugger
 | 
						|
  PrevNode : TDOMNode;
 | 
						|
  CommentFlag : Boolean;
 | 
						|
  CountArgs : String;
 | 
						|
  NArgs : Integer;
 | 
						|
  I     : Integer;
 | 
						|
begin
 | 
						|
  R := '';
 | 
						|
  PrevNode := nil;
 | 
						|
  Node := Template.FirstChild;
 | 
						|
  while Node <> nil do
 | 
						|
    begin
 | 
						|
      N := Node.NodeName;
 | 
						|
      S := Node.NodeValue;
 | 
						|
      CommentFlag := False;
 | 
						|
// plain text in the xml file is copied directly to the output (almost).
 | 
						|
      if N = '#text' then
 | 
						|
        begin
 | 
						|
          if Pos(#10, S) = 1 then  // Hack to work around XML parser that leaves
 | 
						|
            S := Copy(S,2,65535);  // A new-line when text appears in first
 | 
						|
          R := R + S;              // column of the XML file.
 | 
						|
        end
 | 
						|
// indent the text using the string argument Indent
 | 
						|
      else if N = 'indent' then
 | 
						|
        begin
 | 
						|
          Name := GetNodeValue(Node, False);
 | 
						|
          if Name = '' then
 | 
						|
            R := R + Indent
 | 
						|
          else
 | 
						|
            R := R + GetArgValue(Name);
 | 
						|
        end
 | 
						|
// add the line break using the string argument LineEnd
 | 
						|
      else if N = 'br' then
 | 
						|
         R := R + LineEnd
 | 
						|
// process the xml 'if' tag
 | 
						|
      else if (N = 'if') then
 | 
						|
        begin
 | 
						|
          Name := GetNodeValue(Node); //Node.Attributes.Item[0].NodeValue;
 | 
						|
          if GetBoolArgValue(Name) then
 | 
						|
            R := R + ExpandTemplate(Node, LineEnd,Indent,ArgName,ArgVal);
 | 
						|
        end
 | 
						|
// process the xml 'ifnot' tag
 | 
						|
      else if (N = 'ifnot') then
 | 
						|
        begin
 | 
						|
          Name := GetNodeValue(Node); //Node.Attributes.Item[0].NodeValue;
 | 
						|
          if not GetBoolArgValue(Name) then
 | 
						|
            R := R + ExpandTemplate(Node, LineEnd,Indent,ArgName,ArgVal);
 | 
						|
        end
 | 
						|
// process the xml 'else' tag.  This is sneaky.  The else tag must (almost)
 | 
						|
// immediately follow the closing of either the 'if' or 'ifnot' tags.  (The
 | 
						|
// exception allows comments to intervene)
 | 
						|
//
 | 
						|
// The original implementation used separate 'else' and 'elsenot' tags, but
 | 
						|
// the xml file got so confusing at times that it was better to add some
 | 
						|
// nasty looking code here to make the xml neater.
 | 
						|
      else if N = 'else' then
 | 
						|
        begin
 | 
						|
          if PrevNode = nil then
 | 
						|
            raise Exception.Create('Expander: "else" without "if" or "ifnot"');
 | 
						|
          if PrevNode.NodeName = 'if' then
 | 
						|
            begin
 | 
						|
              Name := GetNodeValue(PrevNode); //PrevNode.Attributes.Item[0].NodeValue;
 | 
						|
              if GetBoolArgValue(Name) then
 | 
						|
                R := R + ExpandTemplate(Node, LineEnd,Indent,ArgName,ArgVal);
 | 
						|
            end
 | 
						|
          else if PrevNode.NodeName = 'ifnot' then
 | 
						|
            begin
 | 
						|
              Name := GetNodeValue(PrevNode); //PrevNode.Attributes.Item[0].NodeValue;
 | 
						|
              if not GetBoolArgValue(Name) then
 | 
						|
                R := R + ExpandTemplate(Node, LineEnd,Indent,ArgName,ArgVal);
 | 
						|
            end
 | 
						|
          else
 | 
						|
            raise Exception.Create('Expander:  mis-placed "else" following ' + PrevNode.NodeName);
 | 
						|
 | 
						|
         end
 | 
						|
// process the xml 'count' tag.  This implements multiple lines to be generated
 | 
						|
// from array or list data in the pascal code.  This was originally needed to
 | 
						|
// implement the 'AssignMethod' template.
 | 
						|
      else if N = 'count' then
 | 
						|
        begin
 | 
						|
          Name := GetNodeValue(Node); //Node.Attributes.Item[0].NodeValue;
 | 
						|
          CountArgs := GetArgValue(Name);
 | 
						|
          NArgs := ArgCount(CountArgs);
 | 
						|
          for I := 0 to pred(Nargs) do
 | 
						|
            R := R + ExpandTemplate(Node, LineEnd,Indent,ArgName,ArgVal, I);
 | 
						|
        end
 | 
						|
// process all other xml tags (less comment) as requests for the pascal variable
 | 
						|
// specified by the tag name:  e.g., <ClassName/>  will look for an argument name
 | 
						|
// of ClassName in the ArgNames array and get the corresponding value from the
 | 
						|
// ArgVals array;
 | 
						|
      else if N <> '#comment' then
 | 
						|
        R := R +  GetArgValue(N, CountIndex)
 | 
						|
{$IFDEF DebugTemplate }
 | 
						|
      else
 | 
						|
        begin
 | 
						|
          R := R + '{ ' + Node.NodeValue + ' }';
 | 
						|
          CommentFlag := True;
 | 
						|
        end;
 | 
						|
{$ELSE DebugTemplate}
 | 
						|
      else
 | 
						|
        CommentFlag := True;
 | 
						|
{$ENDIF DebugTemplate}
 | 
						|
// ignore the comment nodes in subsequent processing.
 | 
						|
      if not CommentFlag then PrevNode := Node;
 | 
						|
      Node := Node.NextSibling;
 | 
						|
    end;
 | 
						|
  Result := R;
 | 
						|
end;
 | 
						|
 | 
						|
function TTemplateExpander.FindTemplate(TemplateName : String): TDOMNode;
 | 
						|
var
 | 
						|
  N : String;
 | 
						|
begin
 | 
						|
  if not Assigned(Root) then
 | 
						|
    begin
 | 
						|
      Result := nil;
 | 
						|
      exit;
 | 
						|
    end;
 | 
						|
  if Root.NodeName <> 'templates' then
 | 
						|
    raise Exception.Create('Root node of codetools TemplateExpander = "' + Root.NodeName + '", "templates" expected.');
 | 
						|
 | 
						|
// Sequential search of list of templates.
 | 
						|
  Result := Root.FirstChild;
 | 
						|
  while Result <> nil do
 | 
						|
    begin
 | 
						|
      N := Result.NodeName;
 | 
						|
      if N <> '#comment' then  // ignores first level comments
 | 
						|
        begin
 | 
						|
          if N <> 'template' then
 | 
						|
            raise Exception.Create('template node of codetools TemplateExpander = "' + N + '", "template" expected.');
 | 
						|
          if Result.Attributes.Item[0].NodeValue = TemplateName then
 | 
						|
            break;
 | 
						|
        end;
 | 
						|
      Result := Result.NextSibling;
 | 
						|
    end;
 | 
						|
end;
 | 
						|
 | 
						|
procedure TTemplateExpander.LoadCode;
 | 
						|
var
 | 
						|
  ms: TMemoryStream;
 | 
						|
begin
 | 
						|
  if Code=nil then begin
 | 
						|
    fCodeChangeStep:=CTInvalidChangeStamp;
 | 
						|
    exit;
 | 
						|
  end;
 | 
						|
  fCodeChangeStep:=Code.ChangeStep;
 | 
						|
  Root:=nil;
 | 
						|
  FreeAndNil(XMLDoc);
 | 
						|
  ms:=TMemoryStream.Create;
 | 
						|
  try
 | 
						|
    Code.SaveToStream(ms);
 | 
						|
    ms.Position:=0;
 | 
						|
    ReadXMLFile(XMLDoc, ms);
 | 
						|
    Root := XMLDoc.DocumentElement;
 | 
						|
  finally
 | 
						|
    ms.Free;
 | 
						|
  end;
 | 
						|
end;
 | 
						|
 | 
						|
procedure TTemplateExpander.SetCode(AValue: TCodeBuffer);
 | 
						|
begin
 | 
						|
  if FCode=AValue then Exit;
 | 
						|
  FCode:=AValue;
 | 
						|
  LoadCode;
 | 
						|
end;
 | 
						|
 | 
						|
function TTemplateExpander.TemplateExists(TemplateName: String): Boolean;
 | 
						|
begin
 | 
						|
  ReloadCode;
 | 
						|
  Result := FindTemplate(TemplateName) <> nil;
 | 
						|
end;
 | 
						|
 | 
						|
procedure TTemplateExpander.ReloadCode;
 | 
						|
begin
 | 
						|
  if Code=nil then exit;
 | 
						|
  if Code.ChangeStep=fCodeChangeStep then exit;
 | 
						|
  LoadCode;
 | 
						|
end;
 | 
						|
 | 
						|
end.
 | 
						|
 |