unit dcUtils;

{$I jvcl.inc}

interface

procedure Run;

implementation

uses
  SysUtils, Classes, JTools, TypInfo,
  {$IFNDEF DELPHI6_UP}
  Consts,
  {$ELSE}
  RTLConsts,
  {$ENDIF}
  JvConsts; // (rom) for sLineBreak  no dependencies to packages

{ format of skiplist file:
<classname>.<property>

example:
*.DesignSize // applies to DesignSize in all classes
TPageControl.TabIndex, // TabIndex for TPageControl only
}

function IsBinDFM(Stream: TStream): boolean;
var
  ASignature: byte;
begin
  Stream.Read(ASignature, sizeof(ASignature));
  Result := ASignature = $FF;
  Stream.Seek(-sizeof(ASignature), soFromCurrent);
end;

function CleanDFM(Input, Output: TStream; SkipList: TStrings; SkipUnicode: boolean): boolean;
var
  NestingLevel: Integer;
  SaveSeparator: Char;
  Reader: TReader;
  Writer: TWriter;
  ClassName, ObjectName, PropName: string;
  tmpStream: TMemoryStream;

  procedure WriteIndent;
  const
    Blanks: array[0..1] of Char = '  ';
  var
    I: Integer;
  begin
    for I := 1 to NestingLevel do
      Writer.Write(Blanks, SizeOf(Blanks));
  end;

  procedure WriteStr(const S: string);
  begin
    Writer.Write(S[1], Length(S));
  end;

  procedure NewLine;
  begin
    WriteStr(sLineBreak);
    WriteIndent;
  end;

  procedure ConvertValue; forward;

  procedure ConvertHeader;
  var
    Flags: TFilerFlags;
    Position: Integer;
  begin
    Reader.ReadPrefix(Flags, Position);
    ClassName := Reader.ReadStr;
    ObjectName := Reader.ReadStr;
    WriteIndent;
    if ffInherited in Flags then
      WriteStr('inherited ')
    else if ffInline in Flags then
      WriteStr('inline ')
    else
      WriteStr('object ');
    if ObjectName <> '' then
    begin
      WriteStr(ObjectName);
      WriteStr(': ');
    end;
    WriteStr(ClassName);
    if ffChildPos in Flags then
    begin
      WriteStr(' [');
      WriteStr(IntToStr(Position));
      WriteStr(']');
    end;

    if ObjectName = '' then
      ObjectName := ClassName; // save for error reporting

    WriteStr(sLineBreak);
  end;

  procedure ConvertBinary;
  const
    BytesPerLine = 32;
  var
    MultiLine: Boolean;
    I: Integer;
    Count: Longint;
    Buffer: array[0..BytesPerLine - 1] of Char;
    Text: array[0..BytesPerLine * 2 - 1] of Char;
  begin
    Reader.ReadValue;
    WriteStr('{');
    Inc(NestingLevel);
    Reader.Read(Count, SizeOf(Count));
    MultiLine := Count >= BytesPerLine;
    while Count > 0 do
    begin
      if MultiLine then
        NewLine;
      if Count >= 32 then
        I := 32
      else
        I := Count;
      Reader.Read(Buffer, I);
      BinToHex(Buffer, Text, I);
      Writer.Write(Text, I * 2);
      Dec(Count, I);
    end;
    Dec(NestingLevel);
    WriteStr('}');
  end;

  procedure ConvertProperty; forward;

  procedure ConvertValue;
  const
    LineLength = 64;
  var
    I, J, K, L: Integer;
    S: string;
    W: WideString;
    LineBreak: Boolean;
  begin
    case Reader.NextValue of
      vaList:
        begin
          Reader.ReadValue;
          WriteStr('(');
          Inc(NestingLevel);
          while not Reader.EndOfList do
          begin
            NewLine;
            ConvertValue;
          end;
          Reader.ReadListEnd;
          Dec(NestingLevel);
          WriteStr(')');
        end;
      vaInt8, vaInt16, vaInt32:
        WriteStr(IntToStr(Reader.ReadInteger));
      vaExtended:
        WriteStr(FloatToStr(Reader.ReadFloat));
      vaSingle:
        WriteStr(FloatToStr(Reader.ReadSingle) + 's');
      vaCurrency:
        WriteStr(FloatToStr(Reader.ReadCurrency * 10000) + 'c');
      vaDate:
        WriteStr(FloatToStr(Reader.ReadDate) + 'd');
      vaWString{$IFDEF DELPHI6_UP}, vaUTF8String{$ENDIF}:
        begin
          W := Reader.ReadWideString;
          L := Length(W);
          if L = 0 then
            WriteStr('''''')
          else
          begin
            I := 1;
            Inc(NestingLevel);
            try
              if L > LineLength then
                NewLine;
              K := I;
              repeat
                LineBreak := False;
                if (W[I] >= ' ') and (W[I] <> '''') and (Ord(W[i]) <= 127) then
                begin
                  J := I;
                  repeat
                    Inc(I)
                  until (I > L) or (W[I] < ' ') or (W[I] = '''') or
                    ((I - K) >= LineLength) or (Ord(W[i]) > 127);
                  if ((I - K) >= LineLength) then
                    LineBreak := True;
                  WriteStr('''');
                  while J < I do
                  begin
                    WriteStr(Char(W[J]));
                    Inc(J);
                  end;
                  WriteStr('''');
                end
                else
                begin
                  WriteStr('#');
                  if (Ord(W[I]) > 255) and SkipUnicode then
                  begin
                    Result := True;
                    WriteStr('32');
                  end
                  else
                    WriteStr(IntToStr(Ord(W[I])));
                  Inc(I);
                  if ((I - K) >= LineLength) then
                    LineBreak := True;
                end;
                if LineBreak and (I <= L) then
                begin
                  WriteStr(' +');
                  NewLine;
                  K := I;
                end;
              until I > L;
            finally
              Dec(NestingLevel);
            end;
          end;
        end;
      vaString, vaLString:
        begin
          S := Reader.ReadString;
          L := Length(S);
          if L = 0 then
            WriteStr('''''')
          else
          begin
            I := 1;
            Inc(NestingLevel);
            try
              if L > LineLength then
                NewLine;
              K := I;
              repeat
                LineBreak := False;
                if (S[I] >= ' ') and (S[I] <> '''') then
                begin
                  J := I;
                  repeat
                    Inc(I)
                  until (I > L) or (S[I] < ' ') or (S[I] = '''') or
                    ((I - K) >= LineLength);
                  if ((I - K) >= LineLength) then
                  begin
                    LIneBreak := True;
                    if ByteType(S, I) = mbTrailByte then
                      Dec(I);
                  end;
                  WriteStr('''');
                  Writer.Write(S[J], I - J);
                  WriteStr('''');
                end
                else
                begin
                  WriteStr('#');
                  WriteStr(IntToStr(Ord(S[I])));
                  Inc(I);
                  if ((I - K) >= LineLength) then
                    LineBreak := True;
                end;
                if LineBreak and (I <= L) then
                begin
                  WriteStr(' +');
                  NewLine;
                  K := I;
                end;
              until I > L;
            finally
              Dec(NestingLevel);
            end;
          end;
        end;
      vaIdent, vaFalse, vaTrue, vaNil, vaNull:
        WriteStr(Reader.ReadIdent);
      vaBinary:
        ConvertBinary;
      vaSet:
        begin
          Reader.ReadValue;
          WriteStr('[');
          I := 0;
          while True do
          begin
            S := Reader.ReadStr;
            if S = '' then
              Break;
            if I > 0 then
              WriteStr(', ');
            WriteStr(S);
            Inc(I);
          end;
          WriteStr(']');
        end;
      vaCollection:
        begin
          Reader.ReadValue;
          WriteStr('<');
          Inc(NestingLevel);
          while not Reader.EndOfList do
          begin
            NewLine;
            WriteStr('item');
            if Reader.NextValue in [vaInt8, vaInt16, vaInt32] then
            begin
              WriteStr(' [');
              ConvertValue;
              WriteStr(']');
            end;
            WriteStr(sLineBreak);
            Reader.CheckValue(vaList);
            Inc(NestingLevel);
            while not Reader.EndOfList do
              ConvertProperty;
            Reader.ReadListEnd;
            Dec(NestingLevel);
            WriteIndent;
            WriteStr('end');
          end;
          Reader.ReadListEnd;
          Dec(NestingLevel);
          WriteStr('>');
        end;
      vaInt64:
        WriteStr(IntToStr(Reader.ReadInt64));
    else
      raise EReadError.CreateResFmt(@sPropertyException,
        [ObjectName, DotSep, PropName, IntToStr(Ord(Reader.NextValue))]);
    end;
  end;

  procedure ConvertProperty;
  var
    APos: integer;
  begin
    // sve current position
    APos := Writer.Position;
    WriteIndent;
    PropName := Reader.ReadStr; // save for error reporting
    WriteStr(PropName);
    WriteStr(' = ');
    ConvertValue;
    WriteStr(sLineBreak);
    // Check if the current property should be removed
    if (SkipList <> nil) and ((SkipList.IndexOf(ClassName + '.' + PropName) >= 0) or
      (SkipList.IndexOf('*.' + PropName) >= 0)) then
    begin
      Result := true;
      Writer.Position := APos; // go back to previous position
    end;
  end;

  procedure ConvertObject;
  begin
    ConvertHeader;
    Inc(NestingLevel);
    while not Reader.EndOfList do
      ConvertProperty;
    Reader.ReadListEnd;
    while not Reader.EndOfList do
      ConvertObject;
    Reader.ReadListEnd;
    Dec(NestingLevel);
    WriteIndent;
    WriteStr('end' + sLineBreak);
  end;
begin
  Result := false; // result is set to true in ConvertProperty if we skip anything
  tmpStream := TMemoryStream.Create;
  try
    // we don't want to rewrite everything in CleanDFM,
    // so convert text-> binary if necessary
    // (the other option is to convert Output to text after writing it...)
    if not IsBinDFM(Input) then
    begin
      ObjectTextToResource(Input, tmpStream);
      tmpStream.Seek(0, soFromBeginning);
      Input := tmpStream;
    end;

    Input.ReadResHeader;
    NestingLevel := 0;
    Reader := TReader.Create(Input, 4096);
    SaveSeparator := DecimalSeparator;
    DecimalSeparator := '.';
    try
      Writer := TWriter.Create(Output, 4096);
      try
        Reader.ReadSignature;
        ConvertObject;
      finally
        Writer.Free;
      end;
    finally
      DecimalSeparator := SaveSeparator;
      Reader.Free;
    end;
  finally
    tmpStream.Free;
  end;
end;

procedure ShowHeader;
begin
  writeln('');
  writeln('JEDI DFMCleaner 0.1: DFM property cleaner.');
  writeln('=========================================');
end;

procedure ShowHelp;
begin
  writeln('');
  writeln('Usage:');
  writeln('dc <options> <filemask> <filemask>...');
  writeln('');
  writeln('where <options> are');
  writeln('-i - replaces in-line (output overwrites input)');
  writeln('-s - recurse into subfolders');
  writeln('-u - skip unicode chars');  
  writeln('-f<filename> - where <filename> is a text file that contains the');
  writeln('names of properties to remove.');
  writeln('');
  writeln('<filemask> - a space-delimited list of filenames with optional wildcards.');
  writeln('');
  writeln('');
end;

function ParseDFM(const Filename: string; ASkipList: TStrings; ReplaceInline,
  SkipUnicode: boolean): boolean;
var
  F: TFileStream;
  F2:TMemoryStream;
begin
  Result := false;
  try
    F := TFileStream.Create(Filename, fmOpenReadWrite or fmShareExclusive);
    F2 := TMemoryStream.Create;
    try
      if CleanDFM(F, F2, ASkipList, SkipUnicode) then // only write if something changed
      begin
        Result := true;
        if ReplaceInline then
        begin
          F.Size := 0;
          F.CopyFrom(F2,0);
          writeln('Writing ',Filename,'...');
        end
        else
        begin
          F2.SaveToFile(ChangeFileExt(Filename, '.txt'));
          writeln('Writing ',ChangeFileExt(Filename, '.txt'),'...');
        end;
      end;
    finally
      F.Free;
      F2.Free;
    end;
  except
    on E: Exception do
      writeln('ERROR: ', E.Message, ' (', Filename, ')');
  end;
end;

function ParseFiles(const Filemask: string; ASkipList: TStrings; ReplaceInline,
  Recurse, SkipUnicode: boolean; var FilesFound: integer): integer;
var
  F: TSearchRec;
begin
  Result := 0;
  if FindFirst(Filemask, faAnyFile, F) = 0 then
  begin
    repeat
      Inc(FilesFound);
      if ParseDFM(ExtractFilePath(Filemask) + F.Name, ASkipList, ReplaceInline, SkipUnicode) then
        Inc(Result)
    until FindNext(F) <> 0;
    FindClose(F);
  end;
  if Recurse then
  begin
    if FindFirst(ExtractFilePath(Filemask) + '*.*', faDirectory, F) = 0 then
    begin
      repeat
        if (F.Attr and faDirectory = faDirectory) and (F.Name <> '.') and (F.Name <> '..') then
          Result := Result + ParseFiles(ExtractFilePath(Filemask) + F.Name + '\' + ExtractFileName(Filemask), ASkipList, ReplaceInline, Recurse, SkipUnicode, FilesFound);
      until FindNext(F) <> 0;
      FindClose(F);
    end;
  end;
end;

procedure Run;
var
  i,ACount,FilesFound: integer;
  CmdSwitch: string;
  ReplaceInline, Recurse, SkipUnicode: boolean;
  SkipList: TStringlist;
begin
  ShowHeader;
  if (ParamCount = 0) or GetCmdSwitchValue('h', ['-', '/'], CmdSwitch, true) or GetCmdSwitchValue('?', ['-', '/'], CmdSwitch, true) then
  begin
    ShowHelp;
    Exit;
  end;
  try
    ACount := 0;
    FilesFound := 0;
    Recurse := GetCmdSwitchValue('s', ['-', '/'], CmdSwitch, true);
    ReplaceInline := GetCmdSwitchValue('i', ['-', '/'], CmdSwitch, true);
    SkipUnicode := GetCmdSwitchValue('u', ['-', '/'], CmdSwitch, true);
    if not GetCmdSwitchValue('f', ['-', '/'], CmdSwitch, true) or not FileExists(ExpandUNCFileName(CmdSwitch)) then
      raise Exception.Create('Config file not found!');
    // done: add handling of skiplist and subfolders
    SkipList := TStringlist.Create;
    try
      SkipList.LoadFromFile(ExpandUNCFileName(CmdSwitch));
      SkipList.Sorted := true; // faster lookup
      for i := 1 to ParamCount do
        if not (ParamStr(i)[1] in ['-', '/']) then
          ACount := ACount + ParseFiles(ExpandUNCFileName(ParamStr(i)), SkipList, ReplaceInline, Recurse, SkipUnicode, FilesFound);
    finally
      SkipList.Free;
    end;
    writeln('Done: ',FilesFound,' files found, ', ACount,' files cleaned');
  except
    on E: Exception do
    begin
      writeln('ERROR: ',E.Message);
    end;
  end;
end;

end.

