{-----------------------------------------------------------------------------
The contents of this file are subject to the Mozilla Public License
Version 1.1 (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.mozilla.org/MPL/MPL-1.1.html

Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either expressed or implied. See the License for
the specific language governing rights and limitations under the License.

The Original Code is: Parser.pas, released on 2003-10-04.

The Initial Developer of the Original Code is Andreas Hausladen [Andreas dott Hausladen att gmx dott de]
Portions created by Andreas Hausladen are Copyright (C) 2003 Andreas Hausladen.
All Rights Reserved.

Contributor(s): -

Last Modified: 2003-10-04

You may retrieve the latest version of this file at the Project JEDI's JVCL home page,
located at http://jvcl.delphi-jedi.org

Known Issues:
-----------------------------------------------------------------------------}

unit Parser;

{$I jvcl.inc}

interface

uses
  SysUtils, Classes, Contnrs, dpp_PascalParser, Dialogs;

type
  TProgressEvent = procedure(Sender: TObject; const Text: string; Percentage: Integer) of object;
  TGetResNameEvent = procedure(Sender: TObject; var ResName: string; const Value: string) of object;

  TResStrings = class;

  TParser = class(TObject)
  private
    FOnProgress: TProgressEvent;
    FOnGetResName: TGetResNameEvent;
    FSubDirs: Boolean;
    FSingleResFile: Boolean;
    FResStrings: TResStrings;

    procedure DoProgress(const Text: string; Percentage: Integer);
    function GetResName(const Value: string): string;
  private
    function ReadString(Parser: TPascalParser): string;
    procedure ParseUnderscore(Parser: TPascalParser);
    function Parse(const FileName: string): Boolean;
  public
    constructor Create;
    destructor Destroy; override;

    function ParseFiles(const Dir: string): Integer;
     // returns the number of parsed files

    property SubDirs: Boolean read FSubDirs write FSubDirs;
    property SingleResFile: Boolean read FSingleResFile write FSingleResFile;

    property OnProgress: TProgressEvent read FOnProgress write FOnProgress;
    property OnGetResName: TGetResNameEvent read FOnGetResName write FOnGetResName;
  end;

  TResItem = class(TObject)
  public
    Name: string;
    Value: string;
    RefCount: Integer;

    procedure AddRef;
  end;

  TResStrings = class(TObject)
  private
    FItems: TObjectList;
    function GetItems(Index: Integer): TResItem;
    function GetCount: Integer;
  public
    constructor Create;
    destructor Destroy; override;

    function HasResValue(const Value: string): TResItem;
    function NewResString(const Name, Value: string): TResItem;
    procedure Clear;

    function GetAsString: string;

    procedure SaveToFile(const Filename: string);

    property Count: Integer read GetCount;
    property Items[Index: Integer]: TResItem read GetItems;
  end;

implementation

uses
  JvConsts; // (rom) for sLineBreak  no dependencies to packages

function SubStr(const Value: string; StartIndex, EndIndex: Integer): string;
begin
  Result := Copy(Value, StartIndex, EndIndex - StartIndex + 1);
end;

function GetCleanValue(const Value: string): string;
// removes all white chars from the string for better comparision
var
  Token: PTokenInfo;
  Parser: TPascalParser;
begin
  Result := '';
  Parser := TPascalParser.Create('', Value);
  while Parser.GetToken(Token) do
    Result := Result + Token.Value;
end;

function RemoveWhiteChars(const Value: string): string;
var i, j: Integer;
begin
  SetLength(Result, Length(Value));
  j := 0;
  for i := 1 to Length(Value) do
  begin
    if (Value[i] in ['a'..'z', 'A'..'Z']) then
    begin
      Inc(j);
      Result[j] := Value[i];
    end;
  end;
  SetLength(Result, j);
end;

function ProperOut(const Indent, Value: string; MaxLen: Integer): string;
begin
  Result := Value;
end;

function ReadFileToString(const FileName: string): AnsiString;
var Stream: TFileStream;
begin
  Stream := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
  try
    SetLength(Result, Stream.Size);
    if Result <> '' then
      Stream.Read(Result[1], Length(Result));
  finally
    Stream.Free;
  end;
end;

procedure WriteStringToFile(const FileName: string; const Data: AnsiString);
var Stream: TFileStream;
begin
  Stream := TFileStream.Create(FileName, fmCreate);
  try
    if Data <> '' then
      Stream.Write(Data[1], Length(Data));
  finally
    Stream.Free;
  end;
end;

function IsInList(const S: string; const A: array of string): Boolean;
var i: Integer;
begin
  Result := True;
  for i := 0 to High(A) do
    if CompareText(A[i], S) = 0 then
      Exit;
  Result := False;
end;

procedure FindFiles(const Dir: string; Files: TStrings; SubDirs: Boolean);
var sr: TSearchRec;
begin
  if FindFirst(Dir + '\*.*', faAnyFile or faDirectory, sr) = 0 then
  try
    repeat
      if sr.Attr and faDirectory <> 0 then
      begin
        if (SubDirs) and (sr.Name <> '.') and (sr.Name <> '..') then
          FindFiles(Dir, Files, SubDirs);
      end
      else
        Files.Add(Dir + '\' + sr.Name);
    until FindNext(sr) <> 0;
  finally
    FindClose(sr);
  end;
end;

{ TParser }

constructor TParser.Create;
begin
  inherited Create;
  FResStrings := TResStrings.Create;
end;

destructor TParser.Destroy;
begin
  FResStrings.Free;
  inherited Destroy;
end;

procedure TParser.DoProgress(const Text: string; Percentage: Integer);
begin
  if Assigned(FOnProgress) then
    FOnProgress(Self, Text, Percentage);
end;

function TParser.GetResName(const Value: string): string;
var
  Parser: TPascalParser;
  Token: PTokenInfo;
  S: string;
  P: PChar;
begin
  Result := '';
  Parser := TPascalParser.Create('', Value);
  try
    if Parser.GetToken(Token) then
    begin
      if Token.Kind = tkString then
      begin
        S := Token.Value;
        Delete(S, 1, 1);
        Delete(S, Length(S), 1);

        P := Pointer(S);
        if P <> nil then
        begin
          P[0] := UpCase(P[0]);
          Inc(P);
          while P[0] <> #0 do
          begin
            if (P[0] = ' ') and (P[1] in ['a'..'z']) then
              P[1] := UpCase(P[1]);
            Inc(P);
          end;
          Result := 's' + Copy(RemoveWhiteChars(S), 1, 35);
        end;
      end;
    end;
  finally
    Parser.Free;
  end;

  if Assigned(FOnGetResName) then
    FOnGetResName(Self, Result, Value);
end;

function TParser.ParseFiles(const Dir: string): Integer;
var
  i: Integer;
  Files: TStrings;
begin
  FResStrings.Clear;
  Result := 0;
  Files := TStringList.Create;
  try
    DoProgress('Starting...', 0);
    FindFiles(ExcludeTrailingPathDelimiter(Dir), Files, FSubDirs);

    for i := Files.Count - 1 downto 0 do
      if not IsInList(ExtractFileExt(Files[i]), ['.pas']) then
        Files.Delete(i);

    for i := 0 to Files.Count - 1 do
    begin
      DoProgress(Files[i], i * 100 div Files.Count);
      if Parse(Files[i]) then
        Inc(Result);
    end;
    DoProgress('Finished.', 100);
  finally
    Files.Free;
  end;

  if FSingleResFile and (FResStrings.Count > 0) then
    FResStrings.SaveToFile(ExcludeTrailingPathDelimiter(Dir) + 'Strings.res.pas');
end;

function TParser.Parse(const FileName: string): Boolean;
var
  Parser: TPascalParser;
  Token: PTokenInfo;
  InsertPos: Integer;
  S: string;
begin
  if not FSingleResFile then
    FResStrings.Clear;

  InsertPos := 0;

  Result := False;
  Parser := TPascalParser.Create('', ReadFileToString(FileName));
  try
    while Parser.GetToken(Token) do
    begin
      if Token.Kind = tkIdent then
      begin
        if Token.Value = '_' then
        begin
          ParseUnderscore(Parser);
          Result := True;
        end
        else if CompareText(Token.Value, 'implementation') = 0 then
        begin
          InsertPos := Token.StartIndex;
        end;
      end;
    end;

    if Result and (FResStrings.Count > 0) then
    begin
      S := Parser.Text;
      if not FSingleResFile then
      begin
//        FResStrings.SaveToFile(ChangeFileExt(FileName, '.res.inc'));
        if InsertPos > 0 then
          Insert(sLineBreak + FResStrings.GetAsString + sLineBreak,
            S, InsertPos)
        else
        begin
          ShowMessage('no "implementation" in unit ' + ExtractFileName(FileName));
          Exit;
        end;
      end;
//      WriteStringToFile(ChangeFileExt(FileName, '.pas.txt'), S);
      WriteStringToFile(FileName, S);
      S := '';
    end;
  finally
    Parser.Free;
  end;
end;

procedure TParser.ParseUnderscore(Parser: TPascalParser);
var
  Token: PTokenInfo;
  Item: TResItem;
  Value: string;
  StartIndex, EndIndex: Integer;
begin // CurToken='_'
  StartIndex := Parser.CurToken.StartIndex;
  if (not Parser.GetToken(Token)) or (Token.Value <> '(') then // -> '('
    Exit;
  if not Parser.GetToken(Token) then // -> string
    Exit;
  if Token.Kind = tkString then
  begin
    Value := ReadString(Parser);
    if Value <> '' then
    begin
      Item := FResStrings.HasResValue(Value);
      if Item = nil then
        Item := FResStrings.NewResString(GetResName(Value), Value);
    end
    else
      Exit;
    Token := Parser.CurToken;
    if Token = nil then
      Exit;
    if Token.Value = ',' then
    begin
     // needs Format()
      EndIndex := Parser.PreToken.EndIndex;
      Parser.Replace(StartIndex, EndIndex - StartIndex + 1, 'Format(' + Item.Name);
    end
    else if Token.Value = ')' then
    begin
      EndIndex := Token.EndIndex;
      Parser.Replace(StartIndex, EndIndex - StartIndex + 1, Item.Name);
      Parser.Index := StartIndex; // rescan
    end;
  end;
// CurToken=',' or ')'
end;

function TParser.ReadString(Parser: TPascalParser): string;
var
  Token: PTokenInfo;
  Bracket1, // ()
  Bracket2: Integer; // []
  StartIndex, EndIndex: Integer;
begin // CurToken='string'
  Token := Parser.CurToken;

  StartIndex := Token.StartIndex;
  EndIndex := Token.EndIndex;
  Result := Token.Value;

  Bracket1 := 1;
  Bracket2 := 0;
  while Parser.GetToken(Token) do
  begin
    if (Token.Kind = tkSymbol) then
    begin
      if Token.Value = '(' then Inc(Bracket1)
      else if Token.Value = ')' then Dec(Bracket1)
      else if Token.Value = '[' then Inc(Bracket2)
      else if Token.Value = ']' then Inc(Bracket2);

      if Bracket2 = 0 then
      begin
        if ((Bracket1 = 1) and (Token.Value = ',')) or
           ((Bracket1 = 0) and (Token.Value = ')')) then
        begin
          Result := SubStr(Parser.Text, StartIndex, EndIndex);
          Break;
        end;
      end;
    end;
    EndIndex := Token.EndIndex;
  end;
// CurToken=',' oder ')'
end;

{ TResStrings }

constructor TResStrings.Create;
begin
  inherited Create;
  FItems := TObjectList.Create;
end;

destructor TResStrings.Destroy;
begin
  FItems.Free;
  inherited Destroy;
end;

procedure TResStrings.Clear;
begin
  FItems.Clear;
end;

function TResStrings.GetCount: Integer;
begin
  Result := FItems.Count;
end;

function TResStrings.GetItems(Index: Integer): TResItem;
begin
  Result := TResItem(FItems[Index]);
end;

function TResStrings.HasResValue(const Value: string): TResItem;
var
  i: Integer;
  V: string;
begin
  V := GetCleanValue(Value);
  for i := 0 to Count - 1 do
  begin
    Result := Items[i];
    if GetCleanValue(Result.Value) = V then
      Exit;
  end;
  Result := nil;
end;

function TResStrings.NewResString(const Name, Value: string): TResItem;
var i: Integer;
begin
  if Trim(Name) = '' then
    raise Exception.CreateFmt('Invalid resourcestring name "%s"', [Name]);

  Result := TResItem.Create;
  Result.Name := Name;
  for i := 0 to Count - 1 do
    if SameText(Items[i].Name, Name) then
    begin
      Result.Name := Result.Name + '_';
      Break;
    end;
  Result.Value := Value;
  Result.RefCount := 1;
  FItems.Add(Result);
end;

procedure TResStrings.SaveToFile(const Filename: string);
var
  Lines: TStrings;
  i: Integer;
begin
  Lines := TStringList.Create;
  try
    Lines.Add('resourcestring');
    for i := 0 to Count - 1 do
      Lines.Add('  ' + Items[i].Name + ' = ' + ProperOut('    ', Items[i].Value + ';', 70));
    Lines.SaveToFile(Filename);
  finally
    Lines.Free;
  end;
end;

function TResStrings.GetAsString: string;
var
  Lines: TStrings;
  i: Integer;
begin
  Lines := TStringList.Create;
  try
    Lines.Add('resourcestring');
    for i := 0 to Count - 1 do
      Lines.Add('  ' + Items[i].Name + ' = ' + ProperOut('    ', Items[i].Value + ';', 70));
    Result := Lines.Text;
  finally
    Lines.Free;
  end;
end;

{ TResItem }

procedure TResItem.AddRef;
begin
  Inc(RefCount);
end;

end.