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' --> .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: /pkg . TFPDocWriter based writers create an file for every module: /pkg /unit. *) {$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]; //, 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: = (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); //= //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.