{

    FPDoc  -  Free Pascal Documentation Tool
    Copyright (C) 2000 - 2003 by
      Areca Systems GmbH / Sebastian Guenther, sg@freepascal.org

    * Skeleton XML description file generator

    See the file COPYING, included in this distribution,
    for details about the copyright.

    This program 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.
}

(* --- Version 1.0 ---
uMakeSkel merges the FPDoc and MakeSkel functionality, for use in applications.

The TFPDocMaker class supports the following functionality:
- Project generation from a commandline, lpk or lpi file.
- FPDoc documentation generation, optionally syntax check only.
- MakeSkel skeleton generation or update.
- Processing of single units or entire packages.
- Added and extended commandline options.

Everything else is done in a separate documentation manager.
The documentation manager maintains its own projects
and creates temporary TFPDocProjects and TFPDocPackages on demand.
*)

unit umakeskel;

interface

{$mode objfpc}
{$h+}

{$IF FPC_FULLVERSION<20701}
  {$ERROR requires FPC 2.7.1 at least}
{$ENDIF}

uses
  SysUtils, Classes, Gettext,
  dGlobals, PasTree, PParser,PScanner,
  ConfigFile,
  mkfpdoc, fpdocproj;

resourcestring
  STitle = 'MakeSkel - FPDoc skeleton XML description file generator';
  SVersion = 'Version %s [%s]';
  SCopyright = '(c) 2000 - 2003 Areca Systems GmbH / Sebastian Guenther, sg@freepascal.org';
  SCmdLineHelp = 'See documentation for usage.';
  SCmdLineInvalidOption = 'Ignoring unknown option "%s"';
  SNoPackageNameProvided = 'Please specify a package name with --package=<name>';
  SOutputMustNotBeDescr = 'Output file must be different from description filenames.';
  SCreatingNewNode = 'Creating documentation for new node : %s';
  SNodeNotReferenced = 'Documentation node "%s" no longer used';
  SDone = 'Done.';
//from fpdocxmlopts
  SErrInvalidRootNode = 'Invalid options root node: Got "%s", expected "docproject"';
  SErrNoPackagesNode = 'No "packages" node found in docproject';

type
  TCmdLineAction = (actionHelp, actionConvert);

(* EngineOptions plus MakeSkel options.
  Used in the commandline parsers, passed to the Engine.
  Project.Options are ignored by TFDocMaker.(?)
*)

  { TCmdOptions }

  TCmdOptions = class(TEngineOptions)
  public
    WriteDeclaration,
    UpdateMode,
    SortNodes,
    DisableOverride,
    DisableErrors,
    DisableSeealso,
    DisableArguments,
    DisableProtected,
    DisablePrivate,
    DisableFunctionResults: Boolean;
    EmitClassSeparator: Boolean;
    Verbose,
    Modified: boolean;
    procedure Assign(Source: TPersistent); override;
    procedure LoadConfig(cf: TConfigFile; AProfile: string);
    procedure SaveConfig(cf: TConfigFile; AProfile: string);
    procedure BackendToPairs(Dest: TStrings);
    procedure BackendFromPairs(Source: TStrings);
  end;

  { TSkelEngine }

  TSkelEngine = class(TFPDocEngine)
  Private
    FEmittedList, 
    FNodeList,
    FModules : TStringList;
    FOptions: TCmdOptions;
    Procedure  DoWriteUnReferencedNodes(N : TDocNode; NodePath : String);
    procedure SetOptions(AValue: TCmdOptions);
  public
    Destructor Destroy; override;
    Function MustWriteElement(El : TPasElement; Full : Boolean) : Boolean;
    Function WriteElement(Var F : Text; El : TPasElement; ADocNode : TDocNode) : Boolean;
    function FindModule(const AName: String): TPasModule; override;
    function CreateElement(AClass: TPTreeElement; const AName: String;
      AParent: TPasElement; AVisibility :TPasMemberVisibility;
      const ASourceFilename: String; ASourceLinenumber: Integer): TPasElement; override;
    procedure WriteUnReferencedNodes;
    Procedure WriteNodes(Var F : Text; AModule : TPasModule; List : TStrings);
    Procedure DocumentFile(Var F : Text; Const AFileName,ATarget,ACPU : String);
    Property NodeList : TStringList Read FNodeList;
    Property EmittedList : TStringList Read FEmittedList;
    property Options: TCmdOptions read FOptions write SetOptions;
  end;

  THandleOption = function(const Cmd, Arg: string): boolean;

  TCreatorAction = (
    caDefault,
    caDryRun,
    caUsage, //explicit or on all errors?
    caInvalid,
    caWriteProject
  );

  { TFPDocMaker }
(* MakeSkel functionality as a class.
*)
  TFPDocMaker = class(TFPDocCreator)
  private
    FDescrDir: string;
    FInputDir: string;
    FOnOption: THandleOption;
    FOptions: TCmdOptions;
    function GetDescrDir: string;
    function GetInputDir: string;
    procedure SetDescrDir(AValue: string);
    procedure SetInputDir(AValue: string);
    procedure SetOnOption(AValue: THandleOption);
    procedure SetOptions(AValue: TCmdOptions);
  protected
    FCmdAction: TCreatorAction;
    FDryRun: boolean;
    FPackage: TFPDocPackage;
    FProjectFile: boolean;
    FWriteProjectFile: string;
    FTranslated: string;
    procedure SetCmdAction(AValue: TCreatorAction);
    procedure SetDryRun(AValue: boolean);
    procedure SetPackage(AValue: TFPDocPackage);
    procedure SetWriteProjectFile(AValue: string);
    function  ParseCommon(var Cmd, Arg: string): TCreatorAction;
  public
    Function  DocumentPackage(Const APackageName,AOutputName: string; InputFiles, DescrFiles : TStrings) : String;
    procedure CreateUnitDocumentation(const AUnit: string; ParseOnly: Boolean);
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    procedure AddDirToFileList(List: TStrings; const ADirName, AMask: String);
    procedure AddToFileList(List: TStrings; const FileName: String);
    function  UnitSpec(AUnit: string): string;
    function  ImportName(AIndex: integer): string;
    procedure LogToStdOut(Sender: TObject; const msg: string);
    procedure LogToStdErr(Sender: TObject; const msg: string);
  //parsing
    function  ParseFPDocOption(const S: string):  TCreatorAction;
    function  ParseUpdateOption(const S: string):  TCreatorAction;
    function  CheckSkelOptions: string;
    function  CleanXML(const FileName: string): boolean;
    function  SelectedPackage: TFPDocPackage;
    //property Package: TFPDocPackage read SelectedPackage write SetPackage;
    property Package: TFPDocPackage read FPackage write SetPackage; //without message
    property CmdAction: TCreatorAction read FCmdAction write SetCmdAction;
    property DryRun: boolean read FDryRun write SetDryRun;
    property ReadProject: boolean read FProjectFile;
    property WriteProjectFile: string read FWriteProjectFile write SetWriteProjectFile;
    property OnOption: THandleOption read FOnOption write SetOnOption;
    property InputDir: string read GetInputDir write SetInputDir;
    property DescrDir: string read GetDescrDir write SetDescrDir;
    property CmdOptions: TCmdOptions read FOptions write SetOptions;
  end;

//Extract next commandline option from a string
Function GetNextWord(Var s : string) : String;

//Get package name from Imports spec
function ExtractImportName(const s: string): string;
//Get Unit filename from Inputs or Descriptions
function UnitFile(AList: TStrings; AIndex: integer): string;
//Get Unit name from Inputs or Descriptions
function ExtractUnitName(AList: TStrings; AIndex: integer): string;
function ExtractUnitName(s: string): string;

implementation

uses
  dom,
  dWriter;

(* Extract (remove!) next commandline option from a string.
  Handle quoted arguments, but do not unquote.
  Option may be partially quoted, e.g. -opt="arg with blanks"
*)
Function GetNextWord(Var s : string) : String;
Const
  WhiteSpace = [' ',#9,#10,#13];
var
  i,j: integer;
  quoted: boolean;
begin
  I:=1;
  quoted := False;
  While (I<=Length(S)) and (S[i] in WhiteSpace) do
    Inc(I);
  J:=I;
{
    While (J<=Length(S)) and (not (S[J] in WhiteSpace)) do
      Inc(J);
}
  While (J<=Length(S)) do begin
    if (s[j] = '"') then begin
      if quoted then
        break;
      quoted := True;
    end else if not quoted and (S[J] in WhiteSpace) then
      break;
    Inc(J);
  end;
  if (I<=Length(S)) then
    Result:=Copy(S,I,J-I);
  Delete(S,1,J);
end;

function ExtractImportName(const s: string): string;
var
  i: integer;
begin
  Result := s;
  i := Pos(',', Result);
  if i > 1 then
    SetLength(Result, i-1);
  Result := ChangeFileExt(ExtractFileName(Result), '');
end;

function ExtractUnitName(s: string): string;
begin
  Result := ChangeFileExt(ExtractFileName(s), '');
end;

(* Unit name from Inputs[i] or Descriptions[i]
  Package name from Imports?
*)
function ExtractUnitName(AList: TStrings; AIndex: integer): string;
begin
  Result := UnitFile(AList, AIndex);
  if Result <> '' then
    Result := ChangeFileExt(ExtractFileName(Result), '');
end;

(* Extract a file reference from Inputs or Descriptions list.
  Check for existing list and item.
*)
function UnitFile(AList: TStrings; AIndex: integer): string;
var
  s: string;
begin
  if assigned(AList) and (AIndex < AList.Count) then begin
    s := AList[AIndex];
    while s <> '' do begin
      Result := GetNextWord(s);
      if (Result <> '') and (Result[1] <> '-') then
        exit; //found a non-option
    end;
  end;
  Result := ''; //should never happen!
end;

type

  TNodePair = Class(TObject)
  Private
    FEl : TPasElement;
    FNode : TDocNode;
  Public
    Constructor Create(AnElement : TPasElement; ADocNode : TDocNode);
    Property Element : TPasElement Read FEl;
    Property DocNode : TDocNode Read FNode;
  end;

{ TCmdOptions }

procedure TCmdOptions.Assign(Source: TPersistent);
var
  s: TCmdOptions absolute Source;
begin
  inherited Assign(Source); //writes to the local copy!
  if Source is TCmdOptions then begin
    WriteDeclaration := s.WriteDeclaration;
    DisableOverride := s.DisableOverride;
    DisableErrors:=s.DisableErrors;
    DisableSeealso:=s.DisableSeealso;
    DisableArguments:=s.DisableArguments;
    DisableFunctionResults := s.DisableFunctionResults;
    ShowPrivate := s.ShowPrivate;
    DisableProtected:=s.DisableProtected;
    SortNodes := s.SortNodes;
    Verbose:=s.Verbose;
  end;
end;

const SecOpts = 'default';

procedure TCmdOptions.LoadConfig(cf: TConfigFile; AProfile: string);
var
  s, sec: string;
begin
//MakeSkel
  WriteDeclaration := cf.ReadBool(SecOpts, 'WriteDeclaration', True);
  DisableOverride := cf.ReadBool(SecOpts, 'DisableOverride', False);
  DisableErrors := cf.ReadBool(SecOpts, 'DisableErrors', False);
  DisableSeealso := cf.ReadBool(SecOpts, 'DisableSeealso', False);
  DisableArguments := cf.ReadBool(SecOpts, 'DisableArguments', False);
  DisableFunctionResults := cf.ReadBool(SecOpts, 'DisableFunctionResults', False);
  ShowPrivate := cf.ReadBool(SecOpts, 'ShowPrivate', True);
  DisableProtected := cf.ReadBool(SecOpts, 'DisableProtected', False);
  SortNodes := cf.ReadBool(SecOpts, 'SortNodes', False);
//Engine
  StopOnParseError :=  cf.ReadBool(SecOpts, 'StopOnParseError', False);
  WarnNoNode :=  cf.ReadBool(SecOpts, 'WarnNoNode', True);
  InterfaceOnly :=  cf.ReadBool(SecOpts, 'InterfaceOnly', True);
  if AProfile = '' then
    AProfile := SecOpts;
  OSTarget := cf.ReadString(AProfile, 'OSTarget', DefOSTarget);
  CPUTarget := cf.ReadString(AProfile, 'CPUTarget', DefCPUTarget);
  Language := cf.ReadString(AProfile, 'Language', '');
  Backend :=  cf.ReadString(AProfile, 'Backend', 'html');
  MoDir :=  cf.ReadString(AProfile, 'MoDir', '');
  HideProtected :=  cf.ReadBool(AProfile, 'HideProtected', False);
  ShowPrivate :=  cf.ReadBool(AProfile, 'ShowPrivate', False);
  DontTrim := cf.ReadBool(AProfile, 'DontTrim', False);
//Backend
  s := cf.ReadString(AProfile, 'BackendOptions', '');
  BackendOptions.CommaText := s;
//finally
  Modified := False;
end;

procedure TCmdOptions.SaveConfig(cf: TConfigFile; AProfile: string);
begin
//MakeSkel
  cf.WriteBool(SecOpts, 'WriteDeclaration', WriteDeclaration);
  cf.WriteBool(SecOpts, 'DisableOverride', DisableOverride);
  cf.WriteBool(SecOpts, 'DisableErrors', DisableErrors);
  cf.WriteBool(SecOpts, 'DisableSeealso', DisableSeealso);
  cf.WriteBool(SecOpts, 'DisableArguments', DisableArguments);
  cf.WriteBool(SecOpts, 'DisableFunctionResults', DisableFunctionResults);
  cf.WriteBool(SecOpts, 'DisablePrivate', DisablePrivate);
  cf.WriteBool(SecOpts, 'DisableProtected', DisableProtected);
  cf.WriteBool(SecOpts, 'SortNodes', SortNodes);
//Engine
  cf.WriteBool(SecOpts, 'StopOnParseError', StopOnParseError);
  cf.WriteBool(SecOpts, 'WarnNoNode', WarnNoNode);
  cf.WriteBool(SecOpts, 'DontTrim', DontTrim);
  if AProfile = '' then
    AProfile := SecOpts;
  cf.WriteString(AProfile, 'OSTarget', OSTarget);
  cf.WriteString(AProfile, 'CPUTarget', CPUTarget);
  cf.WriteString(AProfile, 'Language', Language);
  cf.WriteString(AProfile, 'Backend', Backend);
  cf.WriteString(AProfile, 'MoDir', MoDir);
  cf.WriteBool(AProfile, 'HideProtected', HideProtected);
  cf.WriteBool(AProfile, 'ShowPrivate', ShowPrivate);
  cf.WriteBool(AProfile, 'InterfaceOnly', InterfaceOnly);
//Backend
  if BackendOptions.Count > 0 then
    cf.WriteString(AProfile, 'BackendOptions', BackendOptions.CommaText);
//finally
  Modified := False;
end;

procedure TCmdOptions.BackendToPairs(Dest: TStrings);
var
  i, n: integer;
begin
  Dest.Clear;
  n := BackendOptions.Count div 2;
  if n = 0 then
    exit;
  Dest.Capacity := n;
  for i := 0 to n-1 do begin
    Dest.Add(BackendOptions[i*2] + '=' + BackendOptions[i*2 + 1]);
  end;
end;

procedure TCmdOptions.BackendFromPairs(Source: TStrings);
var
  i: integer;
begin
  BackendOptions.Clear;
  BackendOptions.Capacity:=Source.Count * 2;
  for i := 0 to Source.Count - 1 do begin
    BackendOptions.Add(Source.Names[i]);
    BackendOptions.Add(Source.ValueFromIndex[i]);
  end;
  Modified := True; //todo: only if really changed?
end;

Constructor TNodePair.Create(AnElement : TPasElement; ADocNode : TDocNode);

begin
  Fel:=Anelement;
  FNode:=ADocNode;
end;

function TSkelEngine.FindModule(const AName: String): TPasModule; 

Var
  I : Integer;

begin
  Result:=Inherited FindModule(AName);
  If (Result=Nil) then
    begin // Create dummy list and search in that.
    If (FModules=Nil) then
      begin
      FModules:=TStringList.Create;
      FModules.Sorted:=True;
      FModules.OwnsObjects := True; //auto destroy
      end;
    I:=FModules.IndexOf(AName);
    IF (I=-1) then
      begin
      Result:=TPasModule.Create(AName,Nil);
      FModules.AddObject(AName,Result);
      end
    else
      Result:=FModules.Objects[i] as TPasModule;  
    end;  
end;

Destructor TSkelEngine.Destroy; 
Var
  I : Integer;
begin
{$IFDEF old}
  If Assigned(FModules) then
    begin
    For I:=0 to FModules.Count-1 do
      FModules.Objects[i].Free;
    FreeAndNil(FModules);    
    end;
{$ELSE}
  FreeAndNil(FModules);
{$ENDIF}
end;

Function TSkelEngine.MustWriteElement(El : TPasElement; Full : Boolean) : Boolean;

Var
  ParentVisible:Boolean;
  PT,PP : TPasElement;
begin
  ParentVisible:=True;
  If (El is TPasArgument) or (El is TPasResultElement) then
    begin
    PT:=El.Parent;
    // Skip ProcedureType or PasFunctionType
    If (PT<>Nil) then
      begin
      if (PT is TPasProcedureType) or (PT is TPasFunctionType) then
        PT:=PT.Parent;
      If (PT<>Nil) and ((PT is TPasProcedure) or (PT is TPasProcedure))   then
        PP:=PT.Parent
      else
        PP:=Nil;
      If (PP<>Nil) and (PP is TPasClassType) then
        begin
        ParentVisible:=((not Options.DisablePrivate or (PT.Visibility<>visPrivate)) and
                       (not Options.DisableProtected or (PT.Visibility<>visProtected)));
        end;
      end;
    end;
  Result:=Assigned(El.Parent) and (Length(El.Name) > 0) and
          (ParentVisible and (not Options.DisableArguments or (El.ClassType <> TPasArgument))) and
          (ParentVisible and (not Options.DisableFunctionResults or (El.ClassType <> TPasResultElement))) and
          (not Options.DisablePrivate or (el.Visibility<>visPrivate)) and
          (not Options.DisableProtected or (el.Visibility<>visProtected));
  If Result and Full then
    begin
    Result:=(Not Assigned(FEmittedList) or (FEmittedList.IndexOf(El.FullName)=-1));
    If Options.DisableOverride and (El is TPasProcedure) then
      Result:=Not TPasProcedure(El).IsOverride;
    end;  
end;


function TSkelEngine.CreateElement(AClass: TPTreeElement; const AName: String;
  AParent: TPasElement; AVisibility : TPasMemberVisibility;
  const ASourceFilename: String; ASourceLinenumber: Integer): TPasElement;

Var
  DN : TDocNode;

begin
  Result := AClass.Create(AName, AParent);
  Result.Visibility:=AVisibility;
  if AClass.InheritsFrom(TPasModule) then
    CurModule := TPasModule(Result);
  // Track this element
  If Options.UpdateMode then
    begin
    DN:=FindDocNode(Result);    
    If Assigned(DN) then
      DN.IncRefCount;
    end
  else
    DN:=Nil;  
  // See if we need to write documentation for it
  If MustWriteElement(Result,False) then
    FNodeList.AddObject(Result.PathName,TNodePair.Create(Result,DN));
end;

Function TSkelEngine.WriteElement(Var F : Text;El : TPasElement; ADocNode : TDocNode) : Boolean;

  Function WriteOnlyShort(APasElement : TPasElement) : Boolean;

  begin
    Result:=(APasElement.ClassType=TPasArgument) or
            (APasElement.ClassType=TPasResultElement) or
            (APasElement.ClassType=TPasEnumValue);
  end;

  Function IsTypeVarConst(APasElement : TPasElement) : Boolean;

  begin
    With APasElement do
      Result:=(InheritsFrom(TPasType) and not InheritsFrom(TPasClassType)) or
              (InheritsFrom(TPasResString)) or
              (InheritsFrom(TPasVariable));
  end;
  
  Function NeedDeclaration(El : TPasElement) : boolean;
  
  begin
    Result:=IsTypeVarConst(El) 
            or WriteOnlyShort(El) 
            or EL.InheritsFrom(TPasProcedure) 
  end;
    
begin
  // Check again, this time with full declaration.
  Result:=MustWriteElement(El,True);
  If Result and Options.UpdateMode then
     Result:=(ADocNode=Nil);
  If Not Result Then
    Exit;
  If Options.UpdateMode then
    DoLog(Format(ScreatingNewNode,[el.PathName]));
  FEmittedList.Add(El.FullName); // So we don't emit again.
  WriteLn(f);
  if Options.EmitClassSeparator and (El.ClassType = TPasClassType) then
    begin
    WriteLn(f, '<!--');
    WriteLn(f, '  ********************************************************************');
    WriteLn(f, '    ', El.PathName);
    WriteLn(f, '  ********************************************************************');
    WriteLn(f, '-->');
    WriteLn(f);
    end;
  If Not (Options.WriteDeclaration and NeedDeclaration(El)) then
    Writeln(F,'<!-- ', El.ElementTypeName,' Visibility: ',VisibilityNames[El.Visibility], ' -->')
  else  
    begin
    Writeln(F,'<!-- ',El.ElementTypeName,' Visibility: ',VisibilityNames[El.Visibility]);
    Writeln(F,'     Declaration: ',El.GetDeclaration(True),' -->');
    end;
  WriteLn(f,'<element name="', El.FullName, '">');
  WriteLn(f, '<short></short>');
  if Not WriteOnlyShort(El) then
    begin
    WriteLn(f, '<descr>');
    WriteLn(f, '</descr>');
    if not (Options.DisableErrors or IsTypeVarConst(El)) then
      begin
      WriteLn(f, '<errors>');
      WriteLn(f, '</errors>');
      end;
    if not Options.DisableSeealso then
      begin
      WriteLn(f, '<seealso>');
      WriteLn(f, '</seealso>');
      end;
    end;
  WriteLn(f, '</element>');
end;

Procedure  TSkelEngine.DoWriteUnReferencedNodes(N : TDocNode; NodePath : String);

begin
  If (N<>Nil) then
    begin
    If (NodePath<>'') then
      NodePath:=NodePath+'.';
    DoWriteUnReferencedNodes(N.FirstChild,NodePath+N.Name);
    While (N<>Nil) do
      begin
      if (N.RefCount=0) and (N.Node<>Nil) and (Not N.TopicNode) then
        DoLog(Format(SNodeNotReferenced,[NodePath+N.Name]));
      N:=N.NextSibling;
      end;
    end;
end;

procedure TSkelEngine.SetOptions(AValue: TCmdOptions);
begin
  if FOptions=AValue then Exit;
  FOptions:=AValue;
end;

procedure TSkelEngine.WriteUnReferencedNodes;

begin
  DoWriteUnReferencedNodes(RootDocNode,'');
end;

Procedure TSkelEngine.WriteNodes(Var F : Text; AModule : TPasModule; List : TStrings);

Var
  P : TNodePair;
  I : integer;

begin
  WriteLn(f);
  WriteLn(f, '<!--');
  WriteLn(f, '  ====================================================================');
  WriteLn(f, '    ', Amodule.Name);
  WriteLn(f, '  ====================================================================');
  WriteLn(f, '-->');
  WriteLn(f);
  WriteLn(f, '<module name="', AModule.Name, '">');
  if not Options.UpdateMode then
    begin
    WriteLn(f, '<short></short>');
    WriteLn(f, '<descr>');
    WriteLn(f, '</descr>');
    end;
  Try 
    For I:=0 to List.Count-1 do
      begin
      P:=List.Objects[i] as TNodePair;
      If (P.Element<>AModule) then
        WriteElement(F,P.Element,P.DocNode);
      end;
  Finally
    WriteLn(f, '');
    WriteLn(f, '</module> <!-- ', AModule.Name, ' -->');
    WriteLn(f, '');
  end;
end;

Procedure TSkelEngine.DocumentFile(Var F : Text; Const AFileName,ATarget,ACPU : String);

Var
  Module : TPasModule;
  I : Integer;
  N : TDocNode;
     
begin
  FNodeList:=TStringList.Create;
  Try
    FEmittedList:=TStringList.Create;
    FEmittedList.Sorted:=True;
    try
      Module:=ParseSource(Self,AFileName,ATarget,ACPU, True); //use streams
      If Options.UpdateMode then
        begin
        N:=FindDocNode(Module);
        If Assigned(N) then
           N.IncRefCount;
         end;
      If Options.SortNodes then
        FNodelist.Sorted:=True;   
      WriteNodes(F,Module,FNodeList);  
      If Options.UpdateMode then
        WriteUnReferencedNodes;
    Finally
      FEmittedList.Free;
    end;  
  Finally  
    For I:=0 to FNodeList.Count-1 do
      FNodeList.Objects[i].Free;
    FNodeList.Free;  
  end;  
end;

{ ---------------------------------------------------------------------
  Main program. Document all units.    
  ---------------------------------------------------------------------}

{ TFPDocMaker }

constructor TFPDocMaker.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FOptions := TCmdOptions.Create;
end;

destructor TFPDocMaker.Destroy;
begin
  FreeAndNil(FOptions);
  inherited Destroy;
end;

function TFPDocMaker.SelectedPackage: TFPDocPackage;
begin
  Result:=FPackage;
  if (FPackage=Nil) or (FPackage.Name='') then
    begin
    DoLog(SNeedPackageName);
    //Usage(1); - in application
    end;
end;

procedure TFPDocMaker.SetOnOption(AValue: THandleOption);
begin
  if FOnOption=AValue then Exit;
  FOnOption:=AValue;
end;

procedure TFPDocMaker.SetDescrDir(AValue: string);
begin
  if FDescrDir=AValue then Exit;
  FDescrDir:=AValue;
  AddDirToFileList(SelectedPackage.Descriptions, AValue, '*.xml');
end;

function TFPDocMaker.GetDescrDir: string;
begin
  if FDescrDir = '' then begin
    if SelectedPackage.Descriptions.Count > 0 then begin
      Result := FPackage.Descriptions[0];
      FDescrDir := ExtractFilePath(Result); //include separator
    end;
  end;
  Result := FDescrDir;
end;

function TFPDocMaker.UnitSpec(AUnit: string): string;
var
  i: integer;
  w: string;
begin
  for i := 0 to SelectedPackage.Inputs.Count - 1 do begin
    w := ExtractUnitName(FPackage.Inputs, i);
    if CompareText(w, AUnit) = 0 then begin
      Result := FPackage.Inputs[i];
      exit;
    end;
  end;
  Result := '';
end;

function TFPDocMaker.ImportName(AIndex: integer): string;
begin
  Result := ExtractImportName(SelectedPackage.Imports[AIndex]);
end;

function TFPDocMaker.GetInputDir: string;
var
  W: string;
begin
  if (FInputDir = '') and (SelectedPackage.Inputs.Count > 0) then begin
    Result := FPackage.Inputs[0];
    while Result <> '' do begin
      w := GetNextWord(Result);
      if (w <> '') and (w[1] <> '-') then begin
        FInputDir := ExtractFilePath(W); //include separator
        break;
      end;
    end;
  end;
  Result := FInputDir;
end;

procedure TFPDocMaker.SetInputDir(AValue: string);
begin
  if FInputDir=AValue then Exit;
  FInputDir:=AValue;
  AddDirToFileList(SelectedPackage.Inputs, AValue, '*.pp');
  AddDirToFileList(SelectedPackage.Inputs, AValue, '*.pas');
end;

procedure TFPDocMaker.SetOptions(AValue: TCmdOptions);
begin
  //if FOptions=AValue then Exit;
  FOptions.Assign(AValue);  //the local MakeSkel options
  Options.Assign(AValue);   //the FPDoc Engine options
  Verbose := AValue.Verbose; //not in Options
end;

(* Check the options, return errors as message strings.
*)
function TFPDocMaker.CheckSkelOptions: string;

Const
{$IFDEF Unix}
  MoFileTemplate = '/usr/local/share/locale/%s/LC_MESSAGES/makeskel.mo';
{$ELSE}
  MoFileTemplate ='intl/makeskel.%s.mo';
{$ENDIF}

Var
  MOFilename: string;

begin
  Result := '';
//translate strings - only once?
  If (Options.Language<>FTranslated) then begin
    MOFilename:=Format(MOFileTemplate,[Options.Language]);
    if FileExists(MOFilename) then
      gettext.TranslateResourceStrings(MoFileName)
    else begin
      Result := ('NOTE: unable to find translation file ' + MOFilename);
      exit;
    end;
    // Translate internal documentation strings
    TranslateDocStrings(Options.Language);
    FTranslated:=Options.Language;
  end;
  // Action is to create the XML skeleton
  if (Package.Name = '') and (CmdAction<>caUsage) then begin
    Result := (SNoPackageNameProvided);
    exit;
  end;
  if CmdOptions.UpdateMode
  and (SelectedPackage.Descriptions.IndexOf(Package.Output)<>-1) then begin
    Result := (SOutputMustNotBeDescr);
    exit;
  end;
end;

procedure TFPDocMaker.SetCmdAction(AValue: TCreatorAction);
begin
  if FCmdAction=AValue then Exit;
  FCmdAction:=AValue;
end;

procedure TFPDocMaker.SetDryRun(AValue: boolean);
begin
  if FDryRun=AValue then Exit;
  FDryRun:=AValue;
end;

procedure TFPDocMaker.SetPackage(AValue: TFPDocPackage);
begin
  if FPackage=AValue then Exit;
  FPackage:=AValue;
end;

procedure TFPDocMaker.SetWriteProjectFile(AValue: string);
begin
  if FWriteProjectFile=AValue then Exit;
  FWriteProjectFile:=AValue;
end;

procedure TFPDocMaker.AddDirToFileList(List: TStrings; const ADirName, AMask: String);

Var
  Info : TSearchRec;
  D, opts : String;
  i: integer;
begin
  i := Pos(',', ADirName);
  if i > 0 then begin
    opts := ' ' + Copy(ADirName, i+1, Length(ADirName));
    D := Copy(ADirName, 1, i-1);
  end else begin
    D := ADirName;
    opts := '';
  end;
  if (D<>'') and not DirectoryExists(D) then
     DoLog('Directory '+D+' does not exist')
  else
    begin
    if (D='.') then
      D:=''
    else
      D:=IncludeTrailingPathDelimiter(D);
    If (FindFirst(D+AMask,0,Info)=0) then
      try
        Repeat
          If (Info.Attr and faDirectory)=0 then
            List.Add(D+Info.name + opts);
        Until FindNext(Info)<>0;
      finally
        FindClose(Info);
      end;
    end;
end;

procedure TFPDocMaker.AddToFileList(List: TStrings; const FileName: String);
var
  f: Text;
  s, opts: String;
  i: integer;
begin
  i := Pos(',', FileName);
  if i > 0 then begin
    opts := ' ' + Copy(FileName, i+1, Length(FileName));
    s := Copy(FileName, 1, i-1);
  end else begin
    s := FileName;
    opts := '';
  end;
  if s[1] = '@' then
  begin
    AssignFile(f, Copy(s, 2, Length(s)));
    Reset(f);
    while not EOF(f) do
    begin
      ReadLn(f, s);
      List.Add(s + opts);
    end;
    Close(f);
  end else
    List.Add(s + opts);
end;

function TFPDocMaker.ParseCommon(var Cmd, Arg: string):  TCreatorAction;
var
  i: Integer;
begin
  if (Cmd = '-h') or (Cmd = '--help') then begin
    //Usage(0)
    CmdAction := caUsage;
    exit(caUsage);
  end;
  if Cmd = '--update' then
    CmdOptions.UpdateMode := True
  else if (Cmd = '-n') or (Cmd = '--dry-run') then
    begin
    DryRun:=True;
    CmdAction := caDryRun;
    end
//project options
  else if Cmd = '--hide-protected' then
    Options.HideProtected := True
  else if Cmd = '--warn-no-node' then
    Options.WarnNoNode := True
  else if Cmd = '--show-private' then
    Options.ShowPrivate := True  //DoDi: was False???
  else if Cmd = '--stop-on-parser-error' then
    Options.StopOnParseError := True
  else if Cmd = '--dont-trim' then
    Options.DontTrim := True
  else if Cmd = '--parse-impl' then
    Options.InterfaceOnly:=false //is default really True???
  else begin
  //split option
    i := Pos('=', Cmd);
    if i > 0 then begin
      Arg := Copy(Cmd, i + 1, Length(Cmd));
      SetLength(Cmd, i - 1);
      if (Arg <> '') and (Arg[1] = '"') then begin
      //remove quotes
        Arg := StringReplace(Arg, '"', '', [rfReplaceAll]);
      end;
    end else begin
      SetLength(Arg, 0);
      exit(caInvalid); //options without values unhandled here!
    end;
  //more options
    Result := caDefault; //assume succ
    if (Cmd = '--project') or (Cmd='-p') then begin
      FProjectFile:=True; //means: project loaded
      WriteProjectFile := Arg; //do *not* normally overwrite!
      LoadProjectFile(Arg);
    end else if (Cmd = '--descr') then begin
      if FileExists(Arg) then
        AddToFileList(SelectedPackage.Descriptions, Arg)
    end else if (Cmd = '--descr-dir') then
      DescrDir:=Arg
    else if (Cmd = '-i') or (Cmd = '--input') then
      AddToFileList(SelectedPackage.Inputs, Arg)
    else if (Cmd = '--input-dir') then
      InputDir:=Arg
    else if Cmd = '--package' then begin
      If FProjectFile then
        FPackage:=Packages.FindPackage(Arg)
      else begin
        if FPackage = nil then
          FPackage := (Packages.Add) as TFPDocPackage;
        FPackage.Name:=Arg;
      end
    end else if Cmd = '--ostarget' then
      Options.OSTarget := Arg
    else if Cmd = '--cputarget' then
      Options.CPUTarget := Arg
    else if (Cmd = '-l') or (Cmd = '--lang') then
      Options.Language := Arg
    else if Cmd = '--mo-dir' then
      Options.modir := Arg
    else if (Cmd = '-o') or (Cmd = '--output') then
      SelectedPackage.Output := Arg
    else if (Cmd = '-v') or (Cmd = '--verbose') then
      Verbose:=true
    else if Cmd = '--write-project' then begin
      CmdAction := caWriteProject;
      WriteProjectFile:=Arg
    end
  //else no match
    else
      Result := caInvalid;
  end;
end;

function TFPDocMaker.ParseFPDocOption(const S: string):  TCreatorAction;
var
  Cmd, Arg: String;
begin
  Cmd:=S;
  Arg := ''; //make compiler happy
  Result := ParseCommon(Cmd, Arg);
  if Result <> caInvalid then
    exit;
  Result := caDefault; //assume succ
  if (Cmd = '-t') or (Cmd = '--emit-notes') then
    Options.EmitNotes := True
  else if Cmd = '--content' then
    SelectedPackage.ContentFile := Arg
  else if Cmd = '--import' then
    SelectedPackage.Imports.Add(Arg)
//this should not be a project option
  else if (Cmd = '-f') or (Cmd = '--format') then
    begin
    Arg:=UpperCase(Arg);
    If FindWriterClass(Arg)=-1 then
      WriteLn(StdErr, Format(SCmdLineInvalidFormat, [Arg]))
    else
      Options.BackEnd:=Arg;
    end
  else
    begin
    Options.BackendOptions.Add(Cmd);
    Options.BackendOptions.Add(Arg);
    end;
end;

procedure TFPDocMaker.LogToStdOut(Sender: TObject; const msg: string);
begin
  WriteLn(msg);
end;

procedure TFPDocMaker.LogToStdErr(Sender: TObject; const msg: string);
begin
  WriteLn(stderr, msg);
end;

(* Write *all* updates into AOutputName (=DescrFile for Create, UpdFile for Update).
*)
Function TFPDocMaker.DocumentPackage(Const APackageName,AOutputName: string; InputFiles, DescrFiles : TStrings) : String;
Var
  F : Text;
  I,J : Integer;
  Engine: TSkelEngine;
begin
  Result:='';
  AssignFile(f, AOutputName);
  Rewrite(f);
  Try
    WriteLn(f, '<?xml version="1.0" encoding="ISO-8859-1"?>');
    WriteLn(f, '<fpdoc-descriptions>');
    WriteLn(f, '<package name="', APackageName, '">');
    I:=0;
    While (Result='') And (I<InputFiles.Count) do
      begin
      Engine := TSkelEngine.Create;
    //configure engine
      Engine.OnLog:=Self.OnLog;
      Engine.ScannerLogEvents:=Self.ScannerLogEvents;
      Engine.ParserLogEvents:=Self.ParserLogEvents;
      Engine.Options := CmdOptions;
      Try
        Engine.SetPackageName(APackageName);
        if CmdOptions.UpdateMode then
          For J:=0 to DescrFiles.Count-1 do
            Engine.AddDocFile(DescrFiles[J]);
        Try
          Engine.DocumentFile(F,InputFiles[i],Options.OSTarget,Options.CPUTarget);
        except
          on E:Exception do
          begin
            Result:='Error while documenting: '+E.message;
          end;
        end;
      Finally
        Engine.Free;
      end;
      Inc(I);
      end;
  Finally
    WriteLn(f, '</package>');
    WriteLn(f, '</fpdoc-descriptions>');
    Close(f);
    if Result <> '' then begin
      DeleteFile(AOutputName); //remove invalid file
    end;
  end;
end;

procedure TFPDocMaker.CreateUnitDocumentation(const AUnit: string; ParseOnly: Boolean);
var
  il: TStringList;
  spec: string;
begin
  if AUnit <> '' then begin
  //selected unit only
    spec := UnitSpec(AUnit);
    il := TStringList.Create;
    il.Assign(Package.Inputs);
    Package.Inputs.Clear;
    Package.Inputs.Add(spec);
    try
      inherited CreateDocumentation(Package, ParseOnly);
    finally
      Package.Inputs.Assign(il);
      il.Free;
    end;
  end else begin
    CreateDocumentation(Package,ParseOnly);
  end;
end;

(* Return True and (try) kill file if no "<element" found.
*)
function TFPDocMaker.CleanXML(const FileName: string): boolean;
var
  f: TextFile;
  s: string;
begin
  AssignFile(f, FileName);
  Reset(f);
  try
    while not EOF(f) do begin
      ReadLn(f, s);
      if Pos('<element ', s) > 0 then
        exit(False); //file not empty
    end;
  finally
    CloseFile(f);
  end;
//nothing found, delete the file
  if DeleteFile(FileName) then
    DoLog('File ' + FileName + ' has no elements. Deleted.')
  else
    DoLog('File ' + FileName + ' has no elements. Delete failed.');
  Result := True;
end;


function TFPDocMaker.ParseUpdateOption(const s: String):  TCreatorAction;
var
  Cmd, Arg: String;
begin
  Cmd:=S;
  Arg := ''; //make compiler happy
  Result := ParseCommon(Cmd, Arg);
  if Result <> caInvalid then
    exit;
  Result := caDefault; //assume succ
  if s = '--disable-arguments' then
    CmdOptions.DisableArguments := True
  else if s = '--disable-errors' then
    CmdOptions.DisableErrors := True
  else if s = '--disable-function-results' then
    CmdOptions.DisableFunctionResults := True
  else if s = '--disable-seealso' then
    CmdOptions.DisableSeealso := True
  else if s = '--disable-private' then
    CmdOptions.DisablePrivate := True
  else if s = '--disable-override' then
    CmdOptions.DisableOverride := True
  else if s = '--disable-protected' then
    begin
    CmdOptions.DisableProtected := True;
    CmdOptions.DisablePrivate :=True;
    end
  else if (s = '--emitclassseparator') or (s='--emit-class-separator') then
    CmdOptions.EmitClassSeparator := True
  else if (s = '--emit-declaration') then
    CmdOptions.WriteDeclaration := True
  else if (s = '--sort-nodes') then
    CmdOptions.SortNodes := True
  else if (Cmd = '-i') or (Cmd = '--input') then
    AddToFileList(SelectedPackage.Inputs, Arg)
  else if not assigned(OnOption) or not OnOption(Cmd, Arg) then begin
    DoLog(SCmdLineInvalidOption, [s]);
    CmdAction := caInvalid;
    Result := caInvalid;
  end;
end;

end.

