lazarus/examples/fpdocmanager/umanager.pas
dodi 04affae6fc DocMgr: reduced support for FCL packages
git-svn-id: trunk@36550 -
2012-04-03 20:38:07 +00:00

1496 lines
41 KiB
ObjectPascal

unit uManager;
(* Manager object for FPDoc GUI, by DoDi
Holds configuration and packages.
Packages (shall) contain extended descriptions for:
- default OSTarget (FPCDocs: Unix/Linux)
- inputs: by OSTarget
- directories: project(file), InputDir, DescrDir[by language?]
- FPCVersion, LazVersion: variations of inputs
- Skeleton and Output options, depending on DocType/Level and Format.
Units can be described in multiple XML docs, so that it's possible to
have specific parts depending on Laz/FPC version, OSTarget, Language, Widgetset.
This version is decoupled from the fpdoc classes, introduces the classes
TFPDocManager for all packages
TDocPackage for a single package
TFPDocHelper for fpdoc projects
*)
(* Currently registered writers:
TFPDocWriter in 'dwriter.pp'
template: TTemplateWriter(TFPDocWriter) in 'dw_tmpl.pp'
man: TMANWriter(TFPDocWriter) in 'dw_man.pp' --> <pkg>.man /unit.
dxml: TDXMLWriter(TFPDocWriter) in 'dw_dxml.pp'
xml: TXMLWriter(TFPDocWriter) in 'dw_xml.pp'
html: THTMLWriter(TFPDocWriter) in 'dw_html.pp'
htm: THTMWriter(THTMLWriter)
chm: TCHMHTMLWriter(THTMLWriter)
TLinearWriter in 'dwlinear.pp'
template: TTemplateWriter(TLinearWriter) in 'dw_lintmpl.pp'
ipf: TIPFNewWriter(TLinearWriter) in 'dw_ipflin.pas'
latex: TLaTeXWriter(TLinearWriter) in 'dw_latex.pp'
rtf: TRTFWriter(TLinearWriter) in 'dw_linrtf.pp'
txt: TTXTWriter(TLinearWriter) in 'dw_txt.pp'
TLinearWriter based writers create an single output file for a package:
<path>/pkg .<ext>
TFPDocWriter based writers create an file for every module:
<path>/pkg /unit.<ext>
*)
{$mode objfpc}{$H+}
{$DEFINE EasyImports} //EasyImports.patch applied?
interface
uses
Classes, SysUtils,
umakeskel, ConfigFile, fpdocproj, dw_HTML;
type
TFPDocHelper = class;
{ TDocPackage }
(* TDocPackage describes a package documentation project.
*)
TDocPackage = class
private
FAllDirs: boolean;
FAltDir: string;
FCompOpts: string;
FDescrDir: string;
FDescriptions: TStrings;
FIncludePath: string;
FInputDir: string;
FLazPkg: string;
FLoaded: boolean;
FName: string;
FProjectDir: string;
FProjectFile: string;
FSrcDirs: TStrings;
FRequires: TStrings;
FUnitPath: string;
FUnits: TStrings;
function GetAltDir: string;
procedure SetAllDirs(AValue: boolean);
procedure SetAltDir(AValue: string);
procedure SetCompOpts(AValue: string);
procedure SetDescrDir(AValue: string);
procedure SetDescriptions(AValue: TStrings);
procedure SetIncludePath(AValue: string);
procedure SetInputDir(AValue: string);
procedure SetLazPkg(AValue: string);
procedure SetLoaded(AValue: boolean);
procedure SetName(AValue: string);
procedure SetProjectDir(AValue: string);
procedure SetProjectFile(AValue: string);
procedure SetRequires(AValue: TStrings);
procedure SetUnitPath(AValue: string);
procedure SetUnits(AValue: TStrings);
protected
Config: TConfigFile;
procedure ReadConfig; virtual;
public
constructor Create; virtual;
destructor Destroy; override;
function IniFileName: string;
function CreateProject(APrj: TFPDocHelper; const AFile: string): boolean; virtual; //new package project
function ImportProject(APrj: TFPDocHelper; APkg: TFPDocPackage; const AFile: string): boolean;
procedure UpdateConfig;
procedure EnumUnits(AList: TStrings); virtual;
function DescrFileName(const AUnit: string): string;
property Name: string read FName write SetName;
property Loaded: boolean read FLoaded write SetLoaded;
property ProjectFile: string read FProjectFile write SetProjectFile; //xml?
//from LazPkg
procedure AddUnit(const AFile: string);
property AllDirs: boolean read FAllDirs write SetAllDirs;
property CompOpts: string read FCompOpts write SetCompOpts;
property LazPkg: string read FLazPkg write SetLazPkg; //LPK name?
property ProjectDir: string read FProjectDir write SetProjectDir;
property DescrDir: string read FDescrDir write SetDescrDir;
property Descriptions: TStrings read FDescriptions write SetDescriptions;
property AltDir: string read GetAltDir write SetAltDir;
property InputDir: string read FInputDir write SetInputDir;
property SrcDirs: TStrings read FSrcDirs;
property Units: TStrings read FUnits write SetUnits;
property Requires: TStrings read FRequires write SetRequires; //only string?
property IncludePath: string read FIncludePath write SetIncludePath; //-Fi
property UnitPath: string read FUnitPath write SetUnitPath; //-Fu
end;
{ TFCLDocPackage }
TFCLDocPackage = class(TDocPackage)
protected
procedure ReadConfig; override;
public
function CreateProject(APrj: TFPDocHelper; const AFile: string): boolean; override;
end;
{ TFPDocHelper }
//holds temporary project
TFPDocHelper = class(TFPDocMaker)
private
FProjectDir: string;
procedure SetProjectDir(AValue: string);
public
InputList, DescrList: TStringList; //still required?
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
function BeginTest(APkg: TDocPackage): boolean;
function BeginTest(ADir: string): boolean;
procedure EndTest;
function CmdToPrj(const AFileName: string): boolean;
function TestRun(APkg: TDocPackage; AUnit: string): boolean;
function Update(APkg: TDocPackage; const AUnit: string): boolean;
function MakeDocs(APkg: TDocPackage; const AUnit: string; AOutput: string): boolean;
property ProjectDir: string read FProjectDir write SetProjectDir;
end;
TLogHandler = Procedure (Sender : TObject; Const Msg : String) of object;
{ TFPDocManager }
(* Holds configuration and package projects.
*)
TFPDocManager = class(TComponent)
private
FExcludedUnits: boolean;
FFpcDir: string;
FFPDocDir: string;
FLazarusDir: string;
FModified: boolean;
FNoParseUnits: TStringList;
FOnChange: TNotifyEvent;
FOnLog: TLogHandler;
FOptions: TCmdOptions;
FPackage: TDocPackage;
FPackages: TStrings;
FProfile: string;
FProfiles: string; //CSV list of profile names
FRootDir: string;
UpdateCount: integer;
function GetNoParseUnits: TStrings;
procedure SetExcludedUnits(AValue: boolean);
procedure SetFpcDir(AValue: string);
procedure SetFPDocDir(AValue: string);
procedure SetLazarusDir(AValue: string);
procedure SetNoParseUnits(AValue: TStrings);
procedure SetOnChange(AValue: TNotifyEvent);
procedure SetPackage(AValue: TDocPackage);
procedure SetProfile(AValue: string);
procedure SetRootDir(AValue: string);
protected
Helper: TFPDocHelper; //temporary
procedure Changed;
function BeginTest(const ADir: string): boolean;
procedure EndTest;
function RegisterPackage(APkg: TDocPackage): integer;
Procedure DoLog(Const Msg : String);
public
Config: TConfigFile; //extend class
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure BeginUpdate;
procedure EndUpdate;
function LoadConfig(const ADir: string; Force: boolean = False): boolean;
function SaveConfig: boolean;
procedure AddProfile(const AName: string);
function AddProject(const APkg, AFile: string): boolean; //from config
function CreateProject(const AFileName: string; APkg: TDocPackage): boolean;
function AddPackage(AName: string): TDocPackage;
function IsExtended(const APkg: string): string;
function ImportLpk(const AFile: string): TDocPackage;
procedure ImportProject(APkg: TFPDocPackage; const AFile: string);
function ImportCmd(const AFile: string): boolean;
procedure UpdatePackage(const AName: string);
function UpdateFCL(enabled: boolean): boolean;
//actions
function MakeDoc(APkg: TDocPackage; const AUnit, AOutput: string): boolean;
function TestRun(APkg: TDocPackage; AUnit: string): boolean;
function Update(APkg: TDocPackage; const AUnit: string): boolean;
public //published?
property ExcludeUnits: boolean read FExcludedUnits write SetExcludedUnits;
property NoParseUnits: TStrings read GetNoParseUnits write SetNoParseUnits;
property FpcDir: string read FFpcDir write SetFpcDir;
property FpcDocDir: string read FFPDocDir write SetFPDocDir;
property LazarusDir: string read FLazarusDir write SetLazarusDir;
property RootDir: string read FRootDir write SetRootDir;
property Options: TCmdOptions read FOptions;
property Profile: string read FProfile write SetProfile;
property Profiles: string read FProfiles;
property Packages: TStrings read FPackages;
property Package: TDocPackage read FPackage write SetPackage;
property Modified: boolean read FModified; //app
property OnChange: TNotifyEvent read FOnChange write SetOnChange;
Property OnLog : TLogHandler Read FOnLog Write FOnLog;
end;
var
Manager: TFPDocManager = nil; //init by application
function FixPath(const s: string): string;
procedure ListDirs(const ARoot: string; AList: TStrings);
procedure ListUnits(const AMask: string; AList: TStrings);
function MatchUnits(const ADir: string; AList: TStrings): integer;
implementation
uses
uLpk, PParser;
const
ConfigName = 'docmgr.ini';
SecProjects = 'projects';
SecGen = 'dirs';
SecDoc = 'project';
function FixPath(const s: string): string;
var
c: string;
begin
if DirectorySeparator = '/' then
c := '\'
else
c := '/';
Result := StringReplace(s, c, DirectorySeparator, [rfReplaceAll]);
end;
procedure ListDirs(const ARoot: string; AList: TStrings);
var
Info : TSearchRec;
s: string;
begin
if FindFirst (ARoot+'/*',faDirectory,Info)=0 then begin
repeat
if not ((Info.Attr and faDirectory) = faDirectory) then
continue;
s := Info.Name;
if (s[1] <> '.')
and (AList.IndexOf(s) < 0) then //exclude dupes
AList.Add(s); //name only, allow to create relative refs
until FindNext(info)<>0;
end;
FindClose(Info);
end;
procedure ListUnits(const AMask: string; AList: TStrings);
var
Info : TSearchRec;
s, f: string;
begin
if FindFirst (AMask,faArchive,Info)=0 then begin
repeat
s := Info.Name;
if s[1] <> '.' then begin
f := ChangeFileExt(s, ''); //unit name only
if Manager.ExcludeUnits and (Manager.NoParseUnits.IndexOf(f) >= 0) then
continue; //excluded unit!
AList.Add(f);
end;
until FindNext(info)<>0;
end;
FindClose(Info);
end;
function MatchUnits(const ADir: string; AList: TStrings): integer;
var
Info : TSearchRec;
s, ext: string;
begin
Result := -1;
if FindFirst(ADir+DirectorySeparator+'*',faArchive,Info)=0 then begin
repeat
s := Info.Name;
ext := ExtractFileExt(s);
if (ext = '.pas') or (ext = '.pp') then begin
ext := ChangeFileExt(s, '');
Result := AList.IndexOf(ext); //ChangeFileExt(s, '.xml'));
if Result >= 0 then begin
//AList.Delete(Result); //don't search any more
break;
end;
end;
Until FindNext(info)<>0;
end;
FindClose(Info);
end;
{ TFCLDocPackage }
procedure TFCLDocPackage.ReadConfig;
begin
inherited ReadConfig;
if FSrcDirs = nil then
FSrcDirs := TStringList.Create;
Config.ReadSection('SrcDirs', FSrcDirs);
end;
function TFCLDocPackage.CreateProject(APrj: TFPDocHelper; const AFile: string
): boolean;
var
i: integer;
s, d, f: string;
dirs, descs: TStringList;
incl, excl: boolean;
begin
(* Add Lazarus FCL, and explicit or added or all FCL dirs
*)
if APrj.Package <> nil then
exit(True); //already configured
Result:=inherited CreateProject(APrj, AFile);
descs := TStringList.Create;
//add lazdir
if AltDir <> '' then begin
(* Add inputs for all descrs found in AltDir.
For *MakeSkel* add all units in the selected(!) fcl packages to inputs.
How to distinguish both modes?
*)
s := Manager.LazarusDir + 'docs' + DirectorySeparator + 'xml' + DirectorySeparator + 'fcl';
//APrj.ParseFPDocOption(Format('--descr-dir="%s"', [s])); //todo: add includes
//APrj.AddDirToFileList(descs, s, '*.xml');
ListUnits(s+ DirectorySeparator+ '*.xml', descs); //exclude NoParseUnits
descs.Sorted := True;
end;
//scan fcl dirs
dirs := TStringList.Create; //use prepared list?
s := Manager.FFpcDir + 'packages' + DirectorySeparator;
ListDirs(s, dirs);
//now match all files in the source dirs
for i := dirs.Count - 1 downto 0 do begin
d := s + dirs[i] + DirectorySeparator + 'src';
if not DirectoryExists(d) then continue; //can this happen?
(* skip explicitly excluded packages, and exclude selected units.
*)
excl := not FAllDirs
and assigned(FSrcDirs)
and (SrcDirs.IndexOfName(dirs[i]) >= 0)
and (SrcDirs.Values[dirs[i]] <= '0');
if excl then begin
//Manager.DoLog('Skipping directory ' + dirs[i]);
end else begin
incl := FAllDirs
or (assigned(FSrcDirs)
and (SrcDirs.IndexOfName(dirs[i]) >= 0)
and (SrcDirs.Values[dirs[i]] > '0'))
or (MatchUnits(d, descs) >= 0); //!!! descs now is empty!
if incl then begin
//add dir
Manager.DoLog('Adding directory ' + dirs[i]);
APrj.ParseFPDocOption(Format('--input-dir="%s"', [d])); //todo: add includes?
end;
end;
end;
//exclude explicit units - only for FCL?
if Manager.ExcludeUnits and (Manager.NoParseUnits.Count > 0) then begin
//exclude inputs
for i := APrj.InputList.Count - 1 downto 0 do begin
s := ExtractUnitName(APrj.InputList, i);
if Manager.NoParseUnits.IndexOf(s) >= 0 then //case?
APrj.InputList.Delete(i);
end;
end;
//re-create project? The normal project was already created by inherited!
if AFile <> '' then begin
f := ChangeFileExt(AFile, '_ext.xml'); //preserve unmodified project?
APrj.CreateProjectFile(f);
end; // else APrj.CreateProjectFile(Manager.RootDir + 'fcl_ext.xml'); //preserve unmodified project?
//finally
dirs.Free;
descs.Free;
end;
{ TDocPackage }
procedure TDocPackage.SetDescrDir(AValue: string);
begin
if FDescrDir=AValue then Exit;
FDescrDir:=AValue;
end;
procedure TDocPackage.SetCompOpts(AValue: string);
begin
(* collect all compiler options
*)
if FCompOpts=AValue then Exit;
if AValue = '' then exit;
if FCompOpts = '' then
FCompOpts:=AValue
else
FCompOpts:= FCompOpts + ' ' + AValue;
end;
procedure TDocPackage.SetAltDir(AValue: string);
begin
AValue:=FixPath(AValue);
if FAltDir=AValue then Exit;
FAltDir:=AValue;
//we must signal config updated
Config.WriteString(SecDoc, 'AltDir', AltDir);
end;
procedure TDocPackage.SetAllDirs(AValue: boolean);
begin
if FAllDirs=AValue then Exit;
FAllDirs:=AValue;
end;
function TDocPackage.GetAltDir: string;
begin
{$IFDEF FCLadds}
Result := FAltDir;
{$ELSE}
Result := '';
{$ENDIF}
end;
procedure TDocPackage.SetDescriptions(AValue: TStrings);
(* Shall we allow for multiple descriptions? (general + OS specific!?)
*)
begin
if FDescriptions=AValue then Exit;
if AValue = nil then exit; //clear?
if AValue.Count = 0 then exit;
FDescriptions.Assign(AValue);
end;
(* Requires[] only contain package names.
Internal use: Get/Set CommaText
*)
procedure TDocPackage.SetRequires(AValue: TStrings);
procedure Import;
var
i: integer;
s: string;
begin
FRequires.Clear; //assume full replace
for i := 0 to AValue.Count - 1 do begin
s := AValue[i]; //<name.xct>,<prefix>
FRequires.Add(ExtractImportName(s)); // + '=' + s);
end;
end;
begin
if FRequires=AValue then Exit;
if AValue = nil then exit;
if AValue.Count = 0 then exit;
Import;
end;
procedure TDocPackage.SetUnits(AValue: TStrings);
procedure Import;
var
i: integer;
s: string;
begin
FUnits.Clear; //assume full replace
for i := 0 to AValue.Count - 1 do begin
s := AValue[i]; //filespec
FUnits.Add(ExtractUnitName(AValue, i) + '=' + s);
end;
end;
begin
if FUnits=AValue then Exit;
if AValue = nil then exit;
if AValue.Count = 0 then exit;
//import formatted: <unit>=<descr file> (multiple???)
if Pos('=', AValue[0]) > 0 then
FUnits.Assign(AValue) //clears previous content
else //if AValue.Count > 0 then
Import;
end;
procedure TDocPackage.SetIncludePath(AValue: string);
begin
if FIncludePath=AValue then Exit;
FIncludePath:=AValue;
end;
procedure TDocPackage.SetInputDir(AValue: string);
begin
if FInputDir=AValue then Exit;
FInputDir:=AValue;
end;
procedure TDocPackage.SetLazPkg(AValue: string);
begin
if FLazPkg=AValue then Exit;
if AValue = '' then exit;
FLazPkg:=AValue;
FProjectDir := ExtractFilePath(AValue);
//todo: import
end;
procedure TDocPackage.SetLoaded(AValue: boolean);
begin
if FLoaded=AValue then Exit;
FLoaded:=AValue;
if not FLoaded then
exit; //???
if Manager.RegisterPackage(self) < 0 then //now definitely loaded
exit; //really exit?
if Config = nil then
UpdateConfig; //create INI file when loaded
end;
procedure TDocPackage.SetName(AValue: string);
begin
if FName=AValue then Exit;
FName:=AValue;
ReadConfig;
end;
procedure TDocPackage.SetProjectDir(AValue: string);
begin
if FProjectDir=AValue then Exit;
FProjectDir:=AValue;
end;
procedure TDocPackage.SetProjectFile(AValue: string);
begin
if FProjectFile=AValue then Exit;
FProjectFile:=AValue;
//really do more?
if FProjectFile = '' then
exit;
ProjectDir:=ExtractFilePath(FProjectFile);
if ExtractFileExt(FProjectFile) <> '.xml' then
; //really change here???
//import requires fpdocproject - must be created by Manager!
end;
procedure TDocPackage.SetUnitPath(AValue: string);
begin
if FUnitPath=AValue then Exit;
FUnitPath:=AValue;
//save to config?
end;
constructor TDocPackage.Create;
begin
FUnits := TStringList.Create;
FDescriptions := TStringList.Create;
FRequires := TStringList.Create;
//Config requires valid Name -> in SetName
end;
destructor TDocPackage.Destroy;
begin
FreeAndNil(Config);
FreeAndNil(FUnits);
FreeAndNil(FDescriptions);
FreeAndNil(FRequires);
FreeAndNil(FSrcDirs);
inherited Destroy;
end;
(* Create new(?) project.
Usage: after LoadLpk, in general for configured project (user options!)
(more options to come)
*)
function TDocPackage.CreateProject(APrj: TFPDocHelper; const AFile: string): boolean;
var
s, imp: string;
pkg: TFPDocPackage;
i: integer;
lst: TStringList;
begin
Result := APrj.Package <> nil; //already configured?
if Result then
exit;
Result := ProjectDir <> '';
if not Result then
exit; //dir must be known
//create pkg
APrj.ParseFPDocOption('--package=' + Name); //selects or creates the pkg
pkg := APrj.SelectedPackage;
//add Inputs
//todo: common options? OS options?
for i := 0 to Units.Count - 1 do begin
s := Units.ValueFromIndex[i];
if CompOpts <> '' then
s := s + ' ' + CompOpts;
//add further options?
pkg.Inputs.Add(s);
end;
//add Descriptions - either explicit or implicit
if (DescrDir <> '') and (Descriptions.Count = 0) then begin
//first check for existing directory
if not DirectoryExists(DescrDir) then begin
MkDir(DescrDir); //exclude \?
end else if Descriptions.Count = 0 then begin
APrj.ParseFPDocOption('--descr-dir=' + DescrDir); //adds all XML files
end;
end else begin
APrj.DescrDir := DescrDir; //needed by Update
for i := 0 to Descriptions.Count - 1 do begin
s := Descriptions[i];
if Pos('=', s) > 0 then
pkg.Descriptions.Add(Descriptions.ValueFromIndex[i])
else
pkg.Descriptions.Add(s);
end;
end;
if AltDir <> '' then begin
//add descr files
s := Manager.LazarusDir + AltDir;
s := FixPath(s);
if ForceDirectories(s) then begin
//exclude NoParse units
//APrj.ParseFPDocOption(Format('--descr-dir="%s"', [s]));
lst := TStringList.Create;
ListUnits(AltDir + '*.xml', lst); //unit names only
(* add the unit names from lst to the pkg/project
*)
pkg := APrj.SelectedPackage;
for i := 0 to lst.Count - 1 do begin
s := AltDir + lst[i] + '.xml';
pkg.Descriptions.Add(s);
end;
end;
//add source files!?
end;
//add Imports
for i := 0 to Requires.Count - 1 do begin
s := Requires[i];
{$IFDEF EasyImports}
imp := Manager.RootDir + s;
{$ELSE}
imp := Manager.RootDir + s + '.xct,../' + s + '/'; //valid for HTML, not for CHM!
{$ENDIF}
APrj.ParseFPDocOption('--import=' + imp);
end;
//add options
APrj.Options.Assign(Manager.Options);
//debug, looks good here!?
if APrj.Options.Backend = '' then
Manager.DoLog('No format, should be ' + Manager.Options.Backend);
pkg.Output := Manager.RootDir + Name; //???
pkg.ContentFile := Manager.RootDir + Name + '.xct';
//now create project file
if AFile <> '' then begin
if ExtractFileExt(AFile) <> '.xml' then
FProjectFile := ExtractFilePath(AFile) + Name + '_prj.xml'
else
FProjectFile := AFile;
APrj.CreateProjectFile(ProjectFile);
end;
Result := True; //assume okay
end;
(* Init from TFPDocPackage, into which AFile has been loaded.
*)
function TDocPackage.ImportProject(APrj: TFPDocHelper; APkg: TFPDocPackage; const AFile: string): boolean;
var
s: string;
begin
//check loaded
Result := Loaded;
if Result then
exit;
//init...
s := UnitFile(APkg.Inputs, 0);
if s <> '' then
FUnitPath := ExtractFilePath(s);
s := UnitFile(APkg.Descriptions, 0);
if s <> '' then
FDescrDir := ExtractFilePath(s);
//project file - empty if not applicable (multi-package project?!)
if (AFile <> '') and (APrj.Packages.Count = 1) then
ProjectFile := AFile //only if immediately applicable!
else
ProjectDir := ExtractFilePath(AFile);
//init lists
Units := APkg.Inputs;
Descriptions := APkg.Descriptions;
Requires := APkg.Imports;
//more?
//save config!
UpdateConfig;
//finish
Result := Loaded;
end;
procedure TDocPackage.ReadConfig;
var
s: string;
begin
if Loaded then
exit;
if Config = nil then
Config := TConfigFile.Create(IniFileName);
//check config
s := Config.ReadString(SecDoc, 'projectdir', '');
if s = '' then begin
FreeAndNil(Config); //must create and fill later!
exit; //project directory MUST be known
end;
ProjectFile := Config.ReadString(SecDoc, 'projectfile', '');
FInputDir := Config.ReadString(SecDoc, 'inputdir', '');
FCompOpts := Config.ReadString(SecDoc, 'options', '');
FDescrDir := Config.ReadString(SecDoc, 'descrdir', '');
FAltDir := Config.ReadString(SecDoc, 'AltDir', '');
FAllDirs:= Config.ReadBool(SecDoc, 'AllDirs', False);
Requires.CommaText := Config.ReadString(SecDoc, 'requires', '');
//units
Config.ReadSection('units', Units);
Config.ReadSection('descrs', Descriptions);
//more?
//all done
Loaded := True;
end;
(* Initialize the package, write global config (+local?)
*)
procedure TDocPackage.UpdateConfig;
begin
//create ini file, if not already created
if Config = nil then
Config := TConfigFile.Create(IniFileName); //in document RootDir
//general information
Config.WriteString(SecDoc, 'projectdir', ProjectDir);
Config.WriteString(SecDoc, 'projectfile', ProjectFile);
Config.WriteString(SecDoc, 'inputdir', InputDir);
Config.WriteString(SecDoc, 'options', CompOpts);
Config.WriteString(SecDoc, 'descrdir', DescrDir);
Config.WriteString(SecDoc, 'AltDir', FAltDir);
Config.WriteBool(SecDoc, 'AllDirs', AllDirs);
Config.WriteString(SecDoc, 'requires', Requires.CommaText);
//units
Config.WriteSectionValues('units', Units);
Config.WriteSectionValues('descrs', Descriptions);
Config.WriteSectionValues('SrcDirs', SrcDirs);
//all done
Config.Flush;
Loaded := True;
end;
procedure TDocPackage.EnumUnits(AList: TStrings);
var
i: integer;
begin
//override to add further units (from AltDir...)
for i := 0 to Units.Count - 1 do
AList.Add(Units.Names[i]);
end;
function TDocPackage.DescrFileName(const AUnit: string): string;
begin
(* [ProjectDir +] DescrDir + AUnit + .xml
*)
Result := DescrDir;
if (Result = '') or (Result[1] = '.') then
Result := ProjectDir + Result;
Result := Result + DirectorySeparator + AUnit + '.xml';
end;
function TDocPackage.IniFileName: string;
begin
Result := Manager.RootDir + Name + '.ini';
end;
procedure TDocPackage.AddUnit(const AFile: string);
var
s: string;
begin
s := ExtractUnitName(AFile);
if s = '' then
Manager.DoLog('No unit: ' + AFile)
else
Units.Add(s + '=' + AFile);
end;
{ TFPDocManager }
constructor TFPDocManager.Create(AOwner: TComponent);
var
lst: TStringList;
begin
inherited Create(AOwner);
lst := TStringList.Create;
lst.OwnsObjects := True;
FPackages := lst;
FOptions := TCmdOptions.Create;
FNoParseUnits := TStringList.Create;
end;
destructor TFPDocManager.Destroy;
begin
SaveConfig;
FreeAndNil(Config);
//FPackages.Clear; //destructor seems NOT to clear/destroy owned object!?
FreeAndNil(FPackages);
FreeAndNil(FOptions);
FreeAndNil(FNoParseUnits);
inherited Destroy;
end;
procedure TFPDocManager.SetFPDocDir(AValue: string);
begin
if FFPDocDir=AValue then Exit;
FFPDocDir:=AValue;
Config.WriteString(SecGen, 'FpcDocDir', FpcDocDir);
end;
procedure TFPDocManager.SetFpcDir(AValue: string);
begin
if FFpcDir=AValue then Exit;
FFpcDir:=AValue;
Config.WriteString(SecGen, 'FpcDir', FpcDir);
end;
procedure TFPDocManager.SetExcludedUnits(AValue: boolean);
begin
if FExcludedUnits=AValue then Exit;
FExcludedUnits:=AValue;
Config.WriteBool(SecGen, 'ExcludeUnits', AValue);
end;
procedure TFPDocManager.UpdatePackage(const AName: string);
var
pkg: TDocPackage;
i: integer;
s: string;
begin
if LazarusDir = '' then exit;
s := {LazarusDir +} 'docs/xml/'+AName;
if not DirectoryExists(FixPath(LazarusDir + s)) then
exit;
i := Packages.IndexOfName(AName);
if i < 0 then
exit;
pkg := Packages.Objects[i] as TDocPackage;
pkg.AltDir := s; //add descriptors when configuring the project/helper
end;
function TFPDocManager.UpdateFCL(enabled: boolean): boolean;
var
pkg: TFCLDocPackage;
begin
(* Adding to the FCL requires valid FPC and Lazarus directories (caller checks).
Then laz/docs/xml/fcl/ is added to fpc descr-dirs.
The related units have to be added as input-dirs.
Scan fpc/packages/ for candidates.
*)
//todo: implement
pkg := AddPackage('fcl') as TFCLDocPackage;
if pkg = nil then
exit(False);
if enabled then
pkg.AltDir := 'docs/xml/fcl'
else
pkg.AltDir := '';
Result := True;
end;
procedure TFPDocManager.SetLazarusDir(AValue: string);
begin
if FLazarusDir=AValue then Exit;
FLazarusDir:=AValue;
Config.WriteString(SecGen, 'LazarusDir', FLazarusDir);
//update RTL and FCL - if exist and Dir exists
UpdatePackage('rtl');
UpdatePackage('fcl');
end;
function TFPDocManager.GetNoParseUnits: TStrings;
begin
Result := FNoParseUnits;
end;
procedure TFPDocManager.SetNoParseUnits(AValue: TStrings);
begin
FNoParseUnits.Assign(AValue);
FNoParseUnits.Sorted := True;
Config.WriteSection('NoParseUnits', NoParseUnits);
end;
procedure TFPDocManager.SetOnChange(AValue: TNotifyEvent);
begin
if FOnChange=AValue then Exit;
FOnChange:=AValue;
end;
procedure TFPDocManager.SetPackage(AValue: TDocPackage);
begin
if FPackage=AValue then Exit;
FPackage:=AValue;
end;
procedure TFPDocManager.SetProfile(AValue: string);
begin
if AValue = '' then exit;
if FProfile=AValue then Exit;
if Options.Modified then
Options.SaveConfig(Config, FProfile);
FProfile:=AValue;
if not Config.SectionExists(AValue) then begin
FProfiles := FProfiles + ',' + AValue;
Config.WriteString(SecGen, 'Profiles', FProfiles);
end;
Config.WriteString(SecGen, 'Profile', FProfile);
Options.LoadConfig(Config, Profile);
end;
(* Try load config from new dir - this may fail on the first run.
*)
procedure TFPDocManager.SetRootDir(AValue: string);
var
s: string;
begin
s := IncludeTrailingPathDelimiter(AValue);
if FRootDir=s then Exit; //prevent recursion
FRootDir:=s;
//load config - not here!
end;
procedure TFPDocManager.Changed;
begin
if not Modified or (UpdateCount > 0) then
exit; //should not be called directly
FModified := False;
if Assigned(OnChange) then
FOnChange(self);
end;
function TFPDocManager.BeginTest(const ADir: string): boolean;
begin
Helper.Free; //should have been done
Helper := TFPDocHelper.Create(nil);
Helper.OnLog := OnLog;
Result := Helper.BeginTest(ADir);
if Result then
Helper.CmdOptions := Options; //set reference AND propagate!?
end;
procedure TFPDocManager.EndTest;
begin
SetCurrentDir(ExtractFileDir(RootDir));
FreeAndNil(Helper);
end;
procedure TFPDocManager.BeginUpdate;
begin
inc(UpdateCount);
end;
procedure TFPDocManager.EndUpdate;
begin
dec(UpdateCount);
if UpdateCount <= 0 then begin
UpdateCount := 0;
if Modified then
Changed;
end;
end;
(* Try load config.
Init RootDir (only when config found?)
Try load packages from their INI files
*)
function TFPDocManager.LoadConfig(const ADir: string; Force: boolean): boolean;
var
s, pf, cf: string;
i: integer;
begin
s := IncludeTrailingPathDelimiter(ADir);
cf := s + ConfigName;
Result := FileExists(cf);
if not Result and not Force then
exit;
RootDir:=s; //recurse if RootDir changed
//sanity check: only one config file!
if assigned(Config) then begin
if (Config.FileName = cf) then
exit(false) //nothing new?
else
Config.Free;
//clear packages???
end;
Config := TConfigFile.Create(cf);
//Config.CacheUpdates := True;
if not Result then
exit; //nothing to read
//read directories
FFpcDir := Config.ReadString(SecGen, 'FpcDir', '');
FFPDocDir := Config.ReadString(SecGen, 'FpcDocDir', '');
FLazarusDir:=Config.ReadString(SecGen, 'LazarusDir', '');
//read packages
Config.ReadSection(SecProjects, FPackages); //<prj>=<file>
//read detailed package information - possibly multiple packages per project!
BeginUpdate; //turn of app notification!
for i := 0 to Packages.Count - 1 do begin
//read package config (=project file name?)
s := Packages.Names[i];
pf := Packages.ValueFromIndex[i];
if pf <> '' then begin
AddProject(s, pf); //add and load project file, don't update config!
FModified := True; //force app notification
end;
end;
//more? (preferences?)
FProfiles:=Config.ReadString(SecGen, 'Profiles', 'default');
FProfile := Config.ReadString(SecGen,'Profile', 'default');
Options.LoadConfig(Config, Profile);
FExcludedUnits := Config.ReadBool(SecGen, 'ExcludeUnits', True);
Config.ReadSection('NoParseUnits', NoParseUnits);
//done, nothing modified
EndUpdate;
end;
function TFPDocManager.SaveConfig: boolean;
begin
//Options? assume saved by application?
if Options.Modified then begin
Options.SaveConfig(Config, Profile);
end;
Config.Flush;
Result := True; //for now
end;
procedure TFPDocManager.AddProfile(const AName: string);
begin
//add and select - obsolete!
Profile := AName;
end;
(* Add a DocPackage to Packages and INI.
Return package Index.
For exclusive use by Package.SetLoaded!
*)
function TFPDocManager.RegisterPackage(APkg: TDocPackage): integer;
begin
Result := Packages.IndexOfName(APkg.Name);
if Result < 0 then begin
//add package
Result := Packages.AddObject(APkg.Name + '=' + APkg.ProjectFile, APkg);
end else if Packages.Objects[Result] = nil then
Packages.Objects[Result] := APkg;
if APkg.Loaded then begin
//check/create project file?
if APkg.ProjectFile = '' then begin
if APkg.ProjectDir = '' then begin
DoLog('Missing project directory for package ' + APkg.Name);
exit(-1); //???
end;
APkg.ProjectFile := APkg.ProjectDir + APkg.Name; //to be fixed by pkg
end;
if (ExtractFileExt(APkg.ProjectFile) <> '.xml') then begin
//create project file
APkg.ProjectFile := ChangeFileExt(APkg.ProjectFile, '_prj.xml');
CreateProject(APkg.ProjectFile, APkg);
//update Packages[] string
Packages[Result] := APkg.Name + '=' + APkg.ProjectFile;
end;
Config.WriteString(SecProjects, APkg.Name, APkg.ProjectFile);
end;
FModified := True;
end;
(* Load FPDoc (XML) project file.
Called by
- init - not Dirty!
*)
function TFPDocManager.AddProject(const APkg, AFile: string): boolean;
var
pkg: TDocPackage;
i: integer;
begin
//create DocPackage
pkg := AddPackage(APkg);
if pkg.Loaded then
exit(True); //assume registered!?
//check project file
if ExtractFileExt(AFile) <> '.xml' then begin
DoLog('Not a project file: ' + AFile);
Exit(False);
end;
if not FileExists(AFile) then begin
DoLog('Missing project file: ' + AFile);
exit(False);
end;
//create helper
BeginTest(AFile);
try
//load the project file into Helper
Helper.LoadProjectFile(AFile);
if Helper.Packages.Count = 1 then begin
Helper.Package := Helper.Packages[0]; //in LoadProject?
Result := pkg.ImportProject(Helper, Helper.Package, AFile);
exit;
end;
//load all packages
for i := 0 to Helper.Packages.Count - 1 do begin
Helper.Package := Helper.Packages[i];
pkg := AddPackage(Helper.Package.Name);
if pkg.Loaded then
continue; //already initialized
pkg.ImportProject(Helper, Helper.Package, '');
end;
finally
EndTest;
end;
end;
(* Ask DocPackage to create an projectfile.
Overwrite if exists???
AFileName is any file in the project directory, required for CD!
!!! prevent recursive calls, destroying Helper !!!
*)
function TFPDocManager.CreateProject(const AFileName: string; APkg: TDocPackage
): boolean;
begin
if Helper = nil then begin
BeginTest(AFileName); //CD into project dir
try
Result := APkg.CreateProject(Helper, AFileName);
finally
EndTest;
end;
end else begin
//assume that Helper IS for APkg
Result := APkg.CreateProject(Helper, AFileName);
end;
end;
(* Return the named package, create if not found.
Rename: GetPackage?
*)
function TFPDocManager.AddPackage(AName: string): TDocPackage;
var
i: integer;
begin
AName := LowerCase(AName);
i := FPackages.IndexOfName(AName);
if i < 0 then
Result := nil
else
Result := FPackages.Objects[i] as TDocPackage;
if Result = nil then begin
{$IFDEF FCLadds}
if AName = 'fcl' then
Result := TFCLDocPackage.Create
else
{$ELSE}
{$ENDIF}
Result := TDocPackage.Create;
Result.Name := AName; //triggers load config --> register
i := FPackages.IndexOfName(AName); //already registered?
end;
if i < 0 then begin
//we MUST create an entry
Packages.AddObject(AName + '=' + Result.ProjectFile, Result);
end;
end;
function TFPDocManager.IsExtended(const APkg: string): string;
var
pkg: TDocPackage;
begin
{$IFDEF FCLadds}
pkg := AddPackage(APkg);
if pkg = nil then
Result := ''
else
Result := pkg.AltDir;
{$ELSE}
Result := '';
{$ENDIF}
end;
function TFPDocManager.ImportLpk(const AFile: string): TDocPackage;
begin
BeginUpdate;
//import the LPK file into? Here: TDocPackage, could be FPDocProject?
Result := uLpk.ImportLpk(AFile);
if Result = nil then
DoLog('Import failed on ' + AFile)
else begin
Result.Loaded := True; //import and write config file
end;
EndUpdate;
end;
(* Add the project, just created from cmdline or projectfile
*)
procedure TFPDocManager.ImportProject(APkg: TFPDocPackage; const AFile: string);
var
pkg: TDocPackage;
begin
pkg := AddPackage(APkg.Name);
pkg.ImportProject(Helper, APkg, AFile);
//update config?
Config.WriteString(SecProjects, pkg.Name, AFile);
FModified := true;
//notify app?
//Changed;
end;
function TFPDocManager.ImportCmd(const AFile: string): boolean;
var
pkg: TDocPackage;
begin
Result := False;
BeginTest(AFile); //directory!!!
try
Result := Helper.CmdToPrj(AFile);
if not Result then
exit;
pkg := AddPackage(Helper.SelectedPackage.Name); //create [and register]
pkg.Loaded := False; //force reload
if not pkg.Loaded then begin
Result := pkg.ImportProject(Helper, Helper.Package, AFile);
end;
finally
EndTest;
end;
if Result then
Changed;
end;
function TFPDocManager.MakeDoc(APkg: TDocPackage; const AUnit, AOutput: string): boolean;
begin
Result := assigned(APkg)
and BeginTest(APkg.ProjectDir)
and APkg.CreateProject(Helper, ''); //only configure, don't create file
if not Result then
exit;
try
Helper.ParseFPDocOption(Format('--output="%s"', [AOutput]));
if Options.Backend = 'chm' then begin
Helper.ParseFPDocOption('--auto-toc');
Helper.ParseFPDocOption('--auto-index');
end;
Helper.ParseFPDocOption('--make-searchable'); //always?
//Result :=
Helper.CreateUnitDocumentation(AUnit, False);
finally
EndTest;
end;
end;
function TFPDocManager.TestRun(APkg: TDocPackage; AUnit: string): boolean;
begin
BeginTest(APkg.ProjectFile);
try
try
Result := Helper.TestRun(APkg, AUnit);
except
on E: EParserError do
DoLog(Format('%s(%d,%d): %s',[e.Filename, e.Row, e.Column, e.Message]));
on E: Exception do
DoLog(E.Message);
end;
finally
EndTest;
end;
end;
function TFPDocManager.Update(APkg: TDocPackage; const AUnit: string): boolean;
begin
Result := assigned(APkg)
and BeginTest(APkg.ProjectFile);
if not Result then
exit;
try
Result := APkg.CreateProject(Helper, ''); //only configure, don't create file
if not Result then
exit;
Result := Helper.Update(APkg, AUnit);
finally
EndTest;
end;
end;
procedure TFPDocManager.DoLog(const Msg: String);
begin
if Assigned(FOnLog) then
FOnLog(self, msg);
end;
{ TFPDocHelper }
constructor TFPDocHelper.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
InputList := TStringList.Create;
DescrList := TStringList.Create;
end;
destructor TFPDocHelper.Destroy;
begin
FreeAndNil(InputList);
FreeAndNil(DescrList);
inherited Destroy;
end;
(* Prepare MakeSkel on temporary FPDocPackage
*)
function TFPDocHelper.BeginTest(APkg: TDocPackage): boolean;
begin
if not assigned(APkg) then
exit(False);
Result := BeginTest(APkg.ProjectFile); //directory would be sufficient!
if not Result then
exit;
APkg.CreateProject(self, ''); //create project file?
Package := Packages.FindPackage(APkg.Name);
//Options?
//okay, so far
Result := assigned(Package);
end;
procedure TFPDocHelper.EndTest;
begin
//???
end;
function TFPDocHelper.BeginTest(ADir: string): boolean;
begin
Result := ADir <> '';
if not Result then
exit;
//remember dir!
if ExtractFileExt(ADir) <> '' then //todo: better check for directory!?
ADir := ExtractFileDir(ADir);
ProjectDir:=ADir;
SetCurrentDir(ProjectDir);
end;
(* Create a project from an FPDoc commandline.
Do NOT create an project file!(?)
*)
function TFPDocHelper.CmdToPrj(const AFileName: string): boolean;
var
l, w: string;
i: integer;
begin
Result := False; //in case of errors
//read the commandline
InputList.LoadFromFile(AFileName);
for i := 0 to InputList.Count - 1 do begin
l := InputList[i];
w := GetNextWord(l);
if w = 'fpdoc' then begin //contains!?
Result := True; //so far
break; //fpdoc command found
end;
end;
InputList.Clear;
if not Result then
exit;
//parse commandline
while l <> '' do begin
w := GetNextWord(l);
ParseFPDocOption(w);
end;
Result := True;
end;
function TFPDocHelper.MakeDocs(APkg: TDocPackage; const AUnit: string;
AOutput: string): boolean;
begin
Result := BeginTest(APkg); //configure and select package
if not Result then
exit;
try
ParseFPDocOption(Format('--output="%s"', [AOutput]));
CreateDocumentation(Package, False);
finally
EndTest;
end;
end;
function TFPDocHelper.TestRun(APkg: TDocPackage; AUnit: string): boolean;
begin
(* more detailed error handling?
Must CD to the project file directory!?
*)
Result := BeginTest(APkg);
if not Result then
exit;
try
//override options for test
ParseFPDocOption('--format=html');
ParseFPDocOption('-v');
ParseFPDocOption('-n');
//verbose?
CreateUnitDocumentation(AUnit, True);
finally
EndTest;
end;
end;
(* MakeSkel functionality - create skeleton or update file
using temporary Project
*)
function TFPDocHelper.Update(APkg: TDocPackage; const AUnit: string): boolean;
function DocumentUnit(const AUnit: string): boolean;
var
OutName, msg: string;
begin
if Manager.NoParseUnits.IndexOf(AUnit) >= 0 then begin
DoLog('NoParse ' + AUnit);
exit(False);
end;
InputList.Clear;
InputList.Add(UnitSpec(AUnit));
DescrList.Clear;
OutName := AUnit + '.xml';
if DescrDir <> '' then
OutName := IncludeTrailingBackslash(DescrDir) + OutName;
CmdOptions.UpdateMode := FileExists(OutName);
if CmdOptions.UpdateMode then begin
DescrList.Add(OutName);
OutName:=Manager.RootDir + 'upd.' + AUnit + '.xml';
DoLog('Update ' + OutName);
end else begin
DoLog('Create ' + OutName);
end;
msg := DocumentPackage(APkg.Name, OutName, InputList, DescrList);
Result := msg = '';
if not Result then
DoLog(msg) //+unit?
else if CmdOptions.UpdateMode then begin
CleanXML(OutName);
end;
end;
var
i: integer;
u: string;
begin
Result := BeginTest(APkg);
if not Result then
exit;
if AUnit <> '' then begin
Result := DocumentUnit(AUnit);
end else begin
for i := 0 to Package.Inputs.Count - 1 do begin
u := ExtractUnitName(Package.Inputs, i);
DocumentUnit(u);
end;
end;
EndTest;
end;
procedure TFPDocHelper.SetProjectDir(AValue: string);
begin
if FProjectDir=AValue then Exit;
FProjectDir:=AValue;
end;
end.