{ FPClass chart - Free Pascal class chart generation tool Copyright (c) 2008 - Michael Van Canneyt, michael@freepascal.org * Free Pascal class chart generation tool 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. } {$mode objfpc} {$h+} program fpclasschart; uses SysUtils, Classes, Typinfo, Gettext, dom, xmlread, dGlobals, PasTree, PParser,PScanner, xmlwrite, fpdocclasstree; resourcestring STitle = 'fpClassTree - Create class tree from pascal sources'; SVersion = 'Version %s [%s]'; SCopyright = '(c) 2008 - Michael Van Canneyt, michael@freepascal.org'; SCmdLineInvalidOption = 'Ignoring unknown option "%s"'; SDone = 'Done.'; SSkipMerge = 'Cannot merge %s into %s tree.'; SErrNoSuchMergeFile = 'Merge file %s does not exist.'; SMergedFile = 'Merged %d classes from file %s.'; SClassesAdded = 'Added %d classes from %d files.'; type { TClassTreeEngine } TClassTreeEngine = class(TFPDocEngine) Private FTree : TClassTreeBuilder; FObjects : TStringList; public Constructor Create(AClassTree : TXMLDocument; AObjectKindSet : TPasObjKindSet); Destructor Destroy; override; function CreateElement(AClass: TPTreeElement; const AName: String; AParent: TPasElement; AVisibility :TPasMemberVisibility; const ASourceFilename: String; ASourceLinenumber: Integer): TPasElement; override; end; { TClassChartFormatter } TClassMode = (cmNormal,cmSubClass,cmheadClass,cmFirstClass); TClassChartFormatter = Class (TObject) private FClassMode: TClassMode; FClassTree: TXMLDocument; FCurrentColCount: Integer; FCurrentRowCount: Integer; FFileName: String; FLargeHeadClassObjects: TStrings; FLevel: Integer; FMaxObjectsPerColumn: Integer; FStartColumnObjects: TStrings; Protected procedure FirstClass(E : TDomElement); virtual; procedure DoEmitClass(E : TDomElement); virtual; procedure DoHeadClass(E: TDomElement); virtual; procedure DoNextColumn(E: TDomElement); virtual; procedure EndSubClass(E: TDomElement; HasSiblings : Boolean); virtual; procedure StartSubClass(E: TDomElement); virtual; Procedure StartChart; virtual; Procedure EndChart; virtual; procedure EmitClass(E : TDomElement; HasSiblings : Boolean); Public Constructor Create (AXML : TXMLDocument); virtual; Destructor Destroy; override; Procedure CreateChart; Property CurrentColCount : Integer Read FCurrentColCount; Property CurrentRowCount : Integer Read FCurrentRowCount; Property ClassTree : TXMLDocument Read FClassTree; Property Level : Integer Read FLevel Write FLevel; Property ClassMode : TClassMode Read FClassMode; Published Property FileName : String Read FFileName Write FFilename; Property StartColumnObjects : TStrings Read FStartColumnObjects; Property LargeHeadClassObjects : TStrings Read FLargeHeadClassObjects; Property MaxObjectsPerColumn : Integer Read FMaxObjectsPerColumn Write FMaxObjectsPerColumn; end; { TClassTreeBuilder } { TChartFormatter } constructor TClassChartFormatter.Create(AXML: TXMLDocument); begin FClassTree:=AXML; MaxObjectsPerColumn:=60; FStartColumnObjects:=TStringList.Create; FLargeHeadClassObjects:=TStringList.Create; FLargeHeadClassObjects.Add('TPersistent'); FLargeHeadClassObjects.Add('TComponent'); end; destructor TClassChartFormatter.Destroy; begin FreeAndNil(FStartColumnObjects); FreeAndNil(FLargeHeadClassObjects); Inherited; end; procedure TClassChartFormatter.CreateChart; Var N : TDomNode; E : TDomElement; I : Integer; L : TFPList; begin (FStartColumnObjects as TStringList).Sorted:=False; (FLargeHeadClassObjects as TStringList).Sorted:=False; StartChart; try N:=FClassTree.DocumentElement.FirstChild; FCurrentColCount:=0; FCurrentRowCount:=0; FLevel:=0; L:=TFPList.Create; try While (N<>nil) do begin If (N.NodeType=ELEMENT_NODE) then L.Add(N); N:=N.NextSibling; end; If (L.Count>0) then begin FirstClass(TDomElement(L[0])); For I:=0 to L.Count-1 do EmitClass(TDomElement(L[i]),II) or ((FCurrentRowCount>MaxObjectsPerColumn) and (FLevel=2)) then DoNextColumn(E) else begin I:=FLargeHeadClassObjects.IndexOf(E.NodeName); if (-1<>I) then begin FLargeHeadClassObjects.Objects[i]:=E; Exit; // Must be picked up later. end; end; DoEmitClass(E); N:=E.FirstChild; L:=TFPList.Create; try While (N<>Nil) do begin if (N.NodeType=ELEMENT_NODE) then L.Add(N); N:=N.NextSibling; end; If L.Count>0 then begin StartSubClass(TDomElement(L[0])); For I:=0 to L.Count-1 do begin EmitClass(TDomElement(L[i]),I0 then Dec(Findent); end; procedure TPostScriptClassChartFormatter.StartSubClass(E: TDomElement); begin inherited StartSubClass(E); Inc(Findent); end; procedure TPostScriptClassChartFormatter.StartChart; begin Assign(FFile,FileName); Rewrite(FFile); end; procedure TPostScriptClassChartFormatter.EndChart; begin Close(FFile); end; type { TGraphVizClassChartFormatter } TGraphVizClassChartFormatter = class(TClassChartFormatter) FFile : Text; FMode : TClassMode; FIndent : integer; Procedure EmitLine(S : string); Protected procedure DoEmitClass(E : TDomElement); override; procedure DoNextColumn(E: TDomElement); override; procedure DoHeadClass(E: TDomElement); override; procedure StartSubClass(E: TDomElement); override; procedure EndSubClass(E: TDomElement; HasSiblings : Boolean); override; Procedure StartChart; override; Procedure EndChart; override; end; { TGraphVizClassChartFormatter } procedure TGraphVizClassChartFormatter.EmitLine(S: String); begin Writeln(FFile,StringofChar(' ',Findent*2),S); end; procedure TGraphVizClassChartFormatter.DoEmitClass(E: TDomElement); begin Case ClassMode of cmFirstClass : EmitLine(Format('%s -> %s', [E.ParentNode.NodeName, E.NodeName])); cmNormal : EmitLine(Format('%s -> %s', [E.ParentNode.NodeName, E.NodeName])); cmSubClass : EmitLine(Format('%s -> %s', [E.ParentNode.NodeName, E.NodeName])); cmHeadClass : EmitLine(Format('%s -> %s', [E.ParentNode.NodeName, E.NodeName])); end; end; procedure TGraphVizClassChartFormatter.DoNextColumn(E: TDomElement); begin Inherited; FIndent:=0; end; procedure TGraphVizClassChartFormatter.DoHeadClass(E: TDomElement); begin // DoNextColumn(E); inherited DoHeadClass(E); end; procedure TGraphVizClassChartFormatter.EndSubClass(E: TDomElement; HasSiblings : Boolean); begin If FIndent>0 then Dec(Findent); end; procedure TGraphVizClassChartFormatter.StartSubClass(E: TDomElement); begin inherited StartSubClass(E); Inc(Findent); end; procedure TGraphVizClassChartFormatter.StartChart; begin Assign(FFile,FileName); Rewrite(FFile); EmitLine('digraph G {'); end; procedure TGraphVizClassChartFormatter.EndChart; begin EmitLine('}'); Close(FFile); end; Type TOutputFormat = (ofXML,ofPostscript, ofGraphViz); Var OutputFormat : TOutputFormat = ofXML; const OSTarget: String = {$I %FPCTARGETOS%}; CPUTarget: String = {$I %FPCTARGETCPU%}; FPCVersion: String = {$I %FPCVERSION%}; FPCDate: String = {$I %FPCDATE%}; function TClassTreeEngine.CreateElement(AClass: TPTreeElement; const AName: String; AParent: TPasElement; AVisibility : TPasMemberVisibility; const ASourceFilename: String; ASourceLinenumber: Integer): TPasElement; begin Result := AClass.Create(AName, AParent); Result.Visibility:=AVisibility; if AClass.InheritsFrom(TPasModule) then CurModule := TPasModule(Result); If AClass.InheritsFrom(TPasClassType) then begin FObjects.AddObject(AName,Result); // Writeln('Added : ',AName); end; end; Constructor TClassTreeEngine.Create(AClassTree : TXMLDocument; AObjectKindSet : TPasObjKindSet); begin Inherited Create; FPackage:=TPasPackage.Create('dummy',Nil); FTree:=TClassTreeBuilder.Create(Self,FPackage,AObjectKindSet); FObjects:=TStringList.Create; end; destructor TClassTreeEngine.Destroy; begin FreeAndNil(FTree); FreeAndNil(FPackage); FreeAndNil(FObjects); inherited Destroy; end; { --------------------------------------------------------------------- Main program. Document all units. ---------------------------------------------------------------------} Function MergeNodes(Doc : TXMLDocument;Dest,Source : TDomElement) : Integer; Var N : TDomNode; S,E : TDomElement; begin Result:=0; N:=Source.FirstChild; While (N<>Nil) do begin if (N.NodeType=ELEMENT_NODE) then begin S:=N as TDomElement; E:=Dest.FindNode(N.NodeName) as TDomElement; If (E=Nil) then begin E:=Doc.CreateElement(N.NodeName); If S['unit']<>'' then E['Unit']:=S['unit']; Dest.AppendChild(E); Inc(Result); end; Result:=Result+MergeNodes(Doc,E,S); end; N:=N.NextSibling; end; end; Function MergeTrees (Dest,Source : TXMLDocument) : Integer; Var S,D : TDomElement; begin Result:=0; D:=Dest.DocumentElement; S:=Source.DocumentElement; If (S.NodeName=D.NodeName) then Result:=MergeNodes(Dest,D,S) else Writeln(StdErr,Format(SSkipMerge,[S.NodeName,D.NodeName])); end; Function MergeTrees (Dest : TXMLDocument; aRootNode : TPasElementNode) : Integer; Var aSrc : TXMLDocument; Procedure AppendChildClasses(aParent : TDomElement; aNode : TPasElementNode); Var El : TDomElement; aChild : TPasElementNode; I : Integer; M : TPasModule; begin If (ANode=Nil) or (aNode.ChildCount=0) then exit; for I:=0 to aNode.ChildCount-1 do begin aChild:=aNode.Children[I]; El:=aSrc.CreateElement(UTF8Decode(aChild.Element.Name)); M:=aChild.Element.GetModule; If M<>Nil then EL['unit']:=UTF8Decode(M.Name); aParent.AppendChild(El); AppendChildClasses(El,aChild); end; end; begin Result:= 0; aSrc:=TXMLDocument.Create(); try aSrc.AppendChild(aSrc.CreateElement('TObject')); AppendChildClasses(aSrc.DocumentElement,aRootNode); MergeTrees(Dest,aSrc); Inc(Result); finally aSrc.Free; end; end; Function AnalyseFiles(Const AOutputName : String; InputFiles,MergeFiles : TStrings; AObjectKind : TPasObjKind) : String; Var XML,XML2 : TXMLDocument; I,ACount : Integer; Engine: TClassTreeEngine; begin Result:=''; ACount:=0; XML:=TXMLDocument.Create; Try //XML. XML.AppendChild(XML.CreateElement('TObject')); For I:=0 to MergeFiles.Count-1 do begin XMl2:=TXMLDocument.Create; ReadXMLFile(XML2,MergeFiles[i]); try ACount:=ACount+MergeTrees(XML,XML2); WriteLn(StdErr,Format(SMergedFile,[ACount,MergeFiles[i]])); Finally FreeAndNil(XML2); end; end; For I:=0 to InputFiles.Count-1 do begin Engine := TClassTreeEngine.Create(XML,[AObjectKind]); Try ParseSource(Engine,InputFiles[I],OSTarget,CPUTarget); Engine.Ftree.BuildTree(Engine.FObjects); ACount:=ACount+MergeTrees(XML,Engine.FTree.RootNode); Finally FreeAndNil(Engine); end; end; Case OutputFormat of ofXML : WriteXMlFile(XML,AOutputName); ofPostScript : With TPostScriptClassChartFormatter.Create(XML) do try FileName:=AOutputName; CreateChart; finally Free; end; ofGraphViz : With TGraphVizClassChartFormatter.Create(XML) do try FileName:=AOutputName; CreateChart; finally Free; end; end; Writeln(StdErr,Format(SClassesAdded,[ACount,InputFiles.Count])); Finally XML.Free; end; end; { --------------------------------------------------------------------- Option management ---------------------------------------------------------------------} var cmdObjectKind : TPasObjKind; InputFiles, MergeFiles : TStringList; DocLang : String; OutputName: String; procedure InitOptions; begin InputFiles := TStringList.Create; MergeFiles := TStringList.Create; end; procedure FreeOptions; begin MergeFiles.Free; InputFiles.Free; end; { --------------------------------------------------------------------- Usage ---------------------------------------------------------------------} Procedure Usage; begin Writeln('Usage : ',ExtractFileName(Paramstr(0)),' [options]'); Writeln('Where [options] is one or more of :'); Writeln(' --merge=filename Filename with object tree to merge.'); Writeln(' --help Emit help.'); Writeln(' --input=cmdline Input file to create skeleton for.'); Writeln(' Use options are as for compiler.'); Writeln(' --kind=objectkind Specify object kind. One of object, class, interface.'); Writeln(' --lang=language Use selected language.'); Writeln(' --output=filename Send output to file.'); Writeln(' --format=name Kind of output to create: XML, PostScript, GraphViz.'); end; procedure ParseOption(const s: String); procedure AddToFileList(List: TStringList; const FileName: String); var f: Text; s: String; begin if Copy(FileName, 1, 1) = '@' then begin Assign(f, Copy(FileName, 2, Length(FileName))); Reset(f); while not EOF(f) do begin ReadLn(f, s); List.Add(s); end; Close(f); end else List.Add(FileName); end; var i: Integer; Cmd, Arg: String; begin cmdObjectKind:=okClass; if (s = '-h') or (s = '--help') then begin Usage; Halt(0); end; i := Pos('=', s); if i > 0 then begin Cmd := Copy(s, 1, i - 1); Arg := Copy(s, i + 1, Length(s)); end else begin Cmd := s; SetLength(Arg, 0); end; if (Cmd = '-i') or (Cmd = '--input') then AddToFileList(InputFiles, Arg) else if (Cmd = '-l') or (Cmd = '--lang') then DocLang := Arg else if (Cmd = '-o') or (Cmd = '--output') then OutputName := Arg else if (Cmd = '-k') or (Cmd = '--kind') then cmdObjectKind:=TPasObjKind(GetEnumValue(TypeInfo(TPasObjKind),'ok'+Arg)) else if (Cmd = '-f') or (Cmd = '--format') then OutputFormat:=TOutputFormat(GetEnumValue(TypeInfo(TOutputFormat),'of'+Arg)) else if Cmd = '--merge' then begin if FileExists(Arg) then MergeFiles.Add(Arg) else Writeln(StdErr,Format(SErrNoSuchMergeFile,[arg])); end else begin WriteLn(StdErr, Format(SCmdLineInvalidOption, [s])); Usage; Halt(1); end; end; Function ParseCommandLine : Integer; Const {$IFDEF Unix} MoFileTemplate = '/usr/local/share/locale/%s/LC_MESSAGES/makeskel.mo'; {$ELSE} MoFileTemplate ='intl/makeskel.%s.mo'; {$ENDIF} var MOFilename: string; i: Integer; begin Result:=0; if ParamCount=0 then begin Usage; Halt(0); end; DocLang:=''; for i := 1 to ParamCount do ParseOption(ParamStr(i)); If (DocLang<>'') then begin MOFilename:=Format(MOFileTemplate,[DocLang]); if FileExists(MOFilename) then gettext.TranslateResourceStrings(MoFileName) else writeln('NOTE: unable to find translation file ',MOFilename); // Translate internal documentation strings TranslateDocStrings(DocLang); end; end; { --------------------------------------------------------------------- Main Program ---------------------------------------------------------------------} Procedure Run; var E: Integer; begin WriteLn(STitle); WriteLn(Format(SVersion, [FPCVersion, FPCDate])); WriteLn(SCopyright); InitOptions; Try E:=ParseCommandLine; If E<>0 then Halt(E); WriteLn; AnalyseFiles(OutputName,InputFiles,MergeFiles,cmdObjectKind); WriteLn(StdErr,SDone); Finally FreeOptions; end; end; Begin Run; end.