/
glscene
/
pasdoc
Обзор
Документация
Войти
/
glscene
/
pasdoc
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
source/component/PasDoc_Gen.pas
4 236 строк
138 KB
Michalis Kamburelis
Fix begin / end indentation, we prefer to make them aligned
09 июл 2024, 14:55
09 июл 2024, 14:55
acba08e
Код
Авторство
О чём код?
{ Copyright 1998-2018 PasDoc developers. This file is part of "PasDoc". "PasDoc" is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. "PasDoc" 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. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with "PasDoc"; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA ---------------------------------------------------------------------------- } { @abstract(basic doc generator object) @author(Johannes Berg <johannes@sipsolutions.de>) @author(Ralf Junker (delphi@zeitungsjunge.de)) @author(Ivan Montes Velencoso (senbei@teleline.es)) @author(Marco Schmidt (marcoschmidt@geocities.com)) @author(Philippe Jean Dit Bailleul (jdb@abacom.com)) @author(Rodrigo Urubatan Ferreira Jardim (rodrigo@netscape.net)) @author(Grzegorz Skoczylas <gskoczylas@rekord.pl>) @author(Pierre Woestyn <pwoestyn@users.sourceforge.net>) @author(Michalis Kamburelis) @author(Richard B. Winston <rbwinst@usgs.gov>) @author(Ascanio Pressato) @author(Arno Garrels <first name.name@nospamgmx.de>) @created(30 Aug 1998) @name contains the basic documentation generator object @link(TDocGenerator). It is not sufficient by itself but the basis for all generators that produce documentation in a specific format like HTML or LaTex. They override @link(TDocGenerator)'s virtual methods. } unit PasDoc_Gen; {$I pasdoc_defines.inc} interface uses PasDoc_Items, PasDoc_Languages, PasDoc_StringVector, PasDoc_ObjectVector, PasDoc_HierarchyTree, PasDoc_Types, Classes, PasDoc_TagManager, PasDoc_Aspell, PasDoc_StreamUtils, PasDoc_StringPairVector; type { Overview files that pasdoc generates for multiple-document-formats like HTML (see @link(TGenericHTMLDocGenerator)). But not all of them are supposed to be generated by pasdoc, some must be generated by external programs by user, e.g. uses and class diagrams must be made by user using programs such as GraphViz. See type TCreatedOverviewFile for subrange type of TOverviewFile that specifies only overview files that are really supposed to be made by pasdoc. } TOverviewFile = ( ofUnits, ofClassHierarchy, ofCios, ofTypes, ofVariables, ofConstants, ofFunctionsAndProcedures, ofIdentifiers, ofGraphVizUses, ofGraphVizClasses ); TCreatedOverviewFile = Low(TOverviewFile) .. ofIdentifiers; TOverviewFileInfo = record BaseFileName: string; TranslationId: TTranslationId; TranslationHeadlineId: TTranslationId; NoItemsTranslationId: TTranslationId; end; const OverviewFilesInfo: array[TOverviewFile] of TOverviewFileInfo = ( (BaseFileName: 'AllUnits' ; TranslationId: trUnits ; TranslationHeadlineId: trHeadlineUnits ; NoItemsTranslationId: trNone { unused } ; ), (BaseFileName: 'ClassHierarchy'; TranslationId: trClassHierarchy ; TranslationHeadlineId: trClassHierarchy { no headline }; NoItemsTranslationId: trNoCIOs ; ), (BaseFileName: 'AllClasses' ; TranslationId: trCio ; TranslationHeadlineId: trHeadlineCio ; NoItemsTranslationId: trNoCIOs ; ), (BaseFileName: 'AllTypes' ; TranslationId: trTypes ; TranslationHeadlineId: trHeadlineTypes ; NoItemsTranslationId: trNoTypes ; ), (BaseFileName: 'AllVariables' ; TranslationId: trVariables ; TranslationHeadlineId: trHeadlineVariables ; NoItemsTranslationId: trNoVariables ; ), (BaseFileName: 'AllConstants' ; TranslationId: trConstants ; TranslationHeadlineId: trHeadlineConstants ; NoItemsTranslationId: trNoConstants ; ), (BaseFileName: 'AllFunctions' ; TranslationId: trFunctionsAndProcedures; TranslationHeadlineId: trHeadlineFunctionsAndProcedures; NoItemsTranslationId: trNoFunctions ; ), (BaseFileName: 'AllIdentifiers'; TranslationId: trIdentifiers ; TranslationHeadlineId: trHeadlineIdentifiers ; NoItemsTranslationId: trNoIdentifiers ; ), (BaseFileName: 'GVUses' ; TranslationId: trGvUses ; TranslationHeadlineId: trGvUses { no headline } ; NoItemsTranslationId: trNone { unused } ; ), (BaseFileName: 'GVClasses' ; TranslationId: trGvClasses ; TranslationHeadlineId: trGvClasses { no headline } ; NoItemsTranslationId: trNoCIOs { unused }; ) ); { Using High(TCreatedOverviewFile) or High(Overview) where Overview: TCreatedOverviewFile in PasDoc_GenHtml produces internal error in FPC 2.0.0. Same for Low(TCreatedOverviewFile). This is submitted as FPC bug 4140, [http://www.freepascal.org/bugs/showrec.php3?ID=4140]. Fixed in FPC 2.0.1 and FPC 2.1.1. } LowCreatedOverviewFile = Low(TCreatedOverviewFile); HighCreatedOverviewFile = High(TCreatedOverviewFile); type TLinkLook = (llDefault, llFull, llStripped); TLinkNotFoundAction = (lnfIgnore, lnfWarn, lnfWarnIfNotInternal); { This is used by @link(TDocGenerator.MakeItemLink) } TLinkContext = ( { This means that link is inside some larger code piece, e.g. within FullDeclaration of some item etc. This means that we @italic(may) be inside a context where used font has constant width. } lcCode, { This means that link is inside some "normal" description text. } lcNormal); TListType = (ltUnordered, ltOrdered, ltDefinition); TListItemSpacing = (lisCompact, lisParagraph); { Collected information about @@xxxList item. } TListItemData = class private FItemLabel: string; FText: string; FIndex: Integer; public constructor Create(AItemLabel, AText: string; AIndex: Integer); { This is only for @@definitionList: label for this list item, taken from @@itemLabel. Already in the processed form. For other lists this will always be ''. } property ItemLabel: string read FItemLabel; { This is content of this item, taken from @@item. Already in the processed form, after @link(TDocGenerator.ConvertString) etc. Ready to be included in final documentation. } property Text: string read FText; { Number of this item. This should be used for @@orderedList. When you iterate over @code(TListData.Items), you should be aware that Index of list item is @italic(not) necessarily equal to the position of item inside @code(TListData.Items). That's because of @@itemSetNumber tag. Normal list numbering (when no @@itemSetNumber tag was used) starts from 1. Using @@itemSetNumber user is able to change following item's Index. For unordered and definition lists this is simpler: Index is always equal to the position within @code(TListData.Items) (because @@itemSetNumber is not allowed there). And usually you will just ignore Index of items on unordered and definition lists. } property Index: Integer read FIndex; end; { Collected information about @@xxxList content. Passed to @link(TDocGenerator.FormatList). Every item of this list should be non-nil instance of @link(TListItemData). } TListData = class(TObjectVector) private { This is used inside list tags' handlers to calculate TListItemData.Index fields. } NextItemIndex: Integer; { This is only for @@definitionList. This is already expanded (by TTagManager.Execute) parameter of @@itemLabel tag, or '' if there is no pending (pending = not included in some @link(TListItemData)) @@itemLabel content. } LastItemLabel: string; FItemSpacing: TListItemSpacing; FListType: TListType; public property ItemSpacing: TListItemSpacing read FItemSpacing; property ListType: TListType read FListType; constructor Create(const AOwnsObject: boolean); override; end; { Collected information about @@row (or @@rowHead). } TRowData = class public { @true if this is for @@rowHead tag. } Head: boolean; { Each item on this list is already converted (with @@-tags parsed, converted by ConvertString etc.) content of given cell tag. } Cells: TStringList; constructor Create; destructor Destroy; override; end; { Collected information about @@table. Passed to @link(TDocGenerator.FormatTable). Every item of this list should be non-nil instance of @link(TRowData). } TTableData = class(TObjectVector) private FMaxCellCount: Cardinal; FMinCellCount: Cardinal; procedure CalculateCellCount; public { Maximum Cells.Count, considering all rows. } property MaxCellCount: Cardinal read FMaxCellCount; { Minimum Cells.Count, considering all rows. } property MinCellCount: Cardinal read FMinCellCount; end; { @abstract(basic documentation generator object) This abstract object will do the complete process of writing documentation files. It will be given the collection of units that was the result of the parsing process and a configuration object that was created from default values and program parameters. Depending on the output format, one or more files may be created (HTML will create several, Tex only one). } TDocGenerator = class(TComponent) private { Things related to spell checking } FCheckSpelling: boolean; FAspellLanguage: string; FAspellProcess: TAspellProcess; FSpellCheckIgnoreWords: TStringList; FLinkGraphVizUses: string; FLinkGraphVizClasses: string; FAutoAbstract: boolean; FLinkLook: TLinkLook; FConclusion: TExternalItem; FIntroduction: TExternalItem; FAdditionalFiles: TExternalItemList; FAbbreviations: TStringList; FGraphVizClasses: boolean; FGraphVizUses: boolean; FWriteUsesClause: boolean; FAutoLink: boolean; FAutoLinkExclude: TStringList; FMarkdown: boolean; { Name of the project to create. } FProjectName: string; { if true, no link to pasdoc homepage will be included at the bottom of HTML files; default is false } FExcludeGenerator: boolean; FIncludeCreationTime: boolean; { If false, the first character of literal tags like 'false' and 'nil' will be upcased. Otherwise all characters will be lowercased. default is false } FUseLowercaseKeywords: boolean; { the output stream that is currently written to; depending on the output format, more than one output stream will be necessary to store all documentation } {$IFDEF STRING_UNICODE} FCurrentStream: TStreamWriter; {$ELSE} FCurrentStream: TStream; {$ENDIF} { Title of documentation. } FTitle: string; { destination directory for documentation; must include terminating forward slash or backslash so that valid file names can be created by concatenating DestinationDirectory and a pathless file name } FDestDir: string; FOnMessage: TPasDocMessageEvent; { These fields are available only for tags OnExecute handlers. They are set in ExpandDescription. } FCurrentItem: TBaseItem; OrderedListTag, UnorderedListTag, DefinitionListTag, TableTag, RowTag, RowHeadTag: TTag; FExternalClassHierarchy: TStrings; procedure SetAbbreviations(const Value: TStringList); function GetLanguage: TLanguageID; procedure SetLanguage(const Value: TLanguageID); procedure SetDestDir(const Value: string); { This just calls OnMessage (if assigned), but it appends to AMessage FCurrentItem.QualifiedName. } procedure DoMessageFromExpandDescription( const MessageType: TPasDocMessageType; const AMessage: string; const AVerbosity: Cardinal); procedure TryAutoLink(TagManager: TTagManager; const QualifiedIdentifier: TNameParts; out QualifiedIdentifierReplacement: string; var AutoLinked: boolean); function SplitSectionTagParameters( ThisTag: TTag; const TagParameter: string; DoMessages: boolean; out HeadingLevel: integer; out AnchorName: string; out Caption: string): boolean; procedure HandleLinkTag(ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure HandleUrlTag(ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure HandleLongCodeTag(ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure HandleClassnameTag(ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure HandleHtmlTag(ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure HandleLatexTag(ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure HandleInheritedClassTag(ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure HandleInheritedTag(ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure HandleNameTag(ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure HandleCodeTag(ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure HandleWarningTag(ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure HandleNoteTag(ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure HandleLiteralTag(ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure HandleBrTag(ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure HandleGroupTag(ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure PreHandleSectionTag(ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure HandleSectionTag(ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure PreHandleAnchorTag(ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure HandleAnchorTag(ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure HandleBoldTag(ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure HandleItalicTag(ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure HandlePreformattedTag(ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure HandleImageTag(ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure HandleIncludeTag(ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure HandleIncludeCodeTag(ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure HandleOrderedListTag(ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure HandleUnorderedListTag(ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure HandleDefinitionListTag(ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure HandleItemTag(ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure HandleItemLabelTag(ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure HandleItemSpacingTag(ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure HandleItemSetNumberTag(ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure HandleTableTag(ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure HandleSomeRowTag(ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure HandleCellTag(ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure HandleNoAutoLinkTag(ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure HandleTableOfContentsTag(ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure SetSpellCheckIgnoreWords(Value: TStringList); procedure TagAllowedInsideLists( ThisTag: TTag; EnclosingTag: TTag; var Allowed: boolean); procedure ItemLabelTagAllowedInside( ThisTag: TTag; EnclosingTag: TTag; var Allowed: boolean); procedure TagAllowedInsideTable( ThisTag: TTag; EnclosingTag: TTag; var Allowed: boolean); procedure TagAllowedInsideRows( ThisTag: TTag; EnclosingTag: TTag; var Allowed: boolean); procedure SetExternalClassHierarchy(const Value: TStrings); function StoredExternalClassHierarchy: boolean; protected { the (human) output language of the documentation file(s) } FLanguage: TPasDocLanguages; FClassHierarchy: TStringCardinalTree; procedure DoError(const AMessage: string; const AArguments: array of const; const AExitCode: Word); procedure DoMessage(const AVerbosity: Cardinal; const MessageType: TPasDocMessageType; const AMessage: string; const AArguments: array of const); {$IFDEF STRING_UNICODE} property CurrentStream: TStreamWriter read FCurrentStream; {$ELSE} property CurrentStream: TStream read FCurrentStream; {$ENDIF} procedure CreateClassHierarchy; { Return a link to item Item which will be displayed as LinkCaption. Returned string may be directly inserted inside output documentation. LinkCaption will be always converted using ConvertString before writing, so don't worry about doing this yourself when calling this method. LinkContext may be used in some descendants to present the link differently, see @link(TLinkContext) for it's meaning. If some output format doesn't support this feature, it can return simply ConvertString(LinkCaption). This is the default implementation of this method in this class. } function MakeItemLink(const Item: TBaseItem; const LinkCaption: string; const LinkContext: TLinkContext): string; virtual; { This writes Code as a Pascal code. Links inside the code are resolved from Item. If WriteItemLink then Item.Name is made a link. Item.Name is printed between NameLinkBegin and NameLinkEnd. } procedure WriteCodeWithLinksCommon(const Item: TPasItem; const Code: string; WriteItemLink: boolean; const NameLinkBegin, NameLinkEnd: string); protected { list of all units that were successfully parsed } FUnits: TPasUnits; { If field @link(CurrentStream) is assigned, it is disposed and set to nil. } procedure CloseStream; { Return S formatted to look like code, e.g. <code>xxx</code> in HTML. Given S is already in the final output format (with characters converted using @link(ConvertString), @@-tags expanded etc.). } function CodeString(const s: string): string; virtual; abstract; { Converts for each character in S, thus assembling a String that is returned and can be written to the documentation file. The @@ character should not be converted, this will be done later on. } function ConvertString(const s: string): string; virtual; abstract; { Converts a character to its converted form. This method should always be called to add characters to a string. @@ should also be converted by this routine. } function ConvertChar(c: char): string; virtual; abstract; { This function is supposed to return a reference to an item, that is the name combined with some linking information like a hyperlink element in HTML or a page number in Tex. } function CreateLink(const Item: TBaseItem): string; virtual; { Open output stream in the destination directory. If @link(CurrentStream) still exists (<> nil), it is closed. Then, a new output stream in the destination directory is created and assigned to @link(CurrentStream). The file is overwritten if exists. Use this only for text files that you want to write using WriteXxx methods of this class (like WriteConverted). There's no point to use if for other files. Returns @true if creation was successful, @false otherwise. When it returns @false, the error message was already shown by DoMessage. } function CreateStream(const AName: string): Boolean; { Searches for an email address in String S. Searches for first appearance of the @@ character} function ExtractEmailAddress(s: string; out S1, S2, EmailAddress: string): Boolean; { Searches for an email address in PossibleEmailAddress and appends mailto: if it's an email address and mailto: wasn't provided. Otherwise it simply returns the input. Needed to link email addresses properly which doesn't start with mailto: } function FixEmailaddressWithoutMailTo(const PossibleEmailAddress: String): String; { Searches for a web address in String S. It must either contain a http:// or start with www. } function ExtractWebAddress(s: string; out S1, S2, WebAddress: string): Boolean; { Searches all items in all units (given by field @link(Units)) for item with NameParts. Returns a pointer to the item on success, nil otherwise. } function FindGlobal(const NameParts: TNameParts): TBaseItem; { Find a Pascal item, searching global namespace. Returns @nil if not found. } function FindGlobalPasItem(const NameParts: TNameParts): TPasItem; overload; { Find a Pascal item, searching global namespace. Assumes that Name is only one component (not something with dots inside). Returns @nil if not found. } function FindGlobalPasItem(const ItemName: String): TPasItem; overload; {@name returns ' abstract', or ' sealed' for classes that abstract or sealed respectively. @name is used by @link(TTexDocGenerator) and @link(TGenericHTMLDocGenerator) in writing the declaration of the class.} function GetClassDirectiveName(Directive: TClassDirective): string; {@name writes a translation of MyType based on the current language. However, 'record' and 'packed record' are not translated.} function GetCIOTypeName(MyType: TCIOType): string; { Loads descriptions from file N and replaces or fills the corresponding comment sections of items. } procedure LoadDescriptionFile(n: string); { Searches for item with name S. If S is not splittable by SplitNameParts, returns nil. If WarningIfNotSplittable, additionally does DoMessage with appropriate warning. Else (if S is "splittable"), seeks for S (first trying Item.FindName, if Item is not nil, then trying FindGlobal). Returns nil if not found. } function SearchItem(s: string; const Item: TBaseItem; WarningIfNotSplittable: boolean): TBaseItem; { Searches for an item of name S which was linked in the description of Item. Starts search within item, then does a search on all items in all units using @link(FindGlobal). Returns a link as String on success. If S is not splittable by SplitNameParts, it always does DoMessage with appropriate warning and returns something like 'UNKNOWN' (no matter what is the value of WarningIfLinkNotFound). FoundItem will be set to nil in this case. When item will not be found then: @unorderedList( @item( if WarningIfLinkNotFound is true then it returns CodeString(ConvertString(S)) and makes DoMessage with appropriate warning.) @item(else it returns '' (and does not do any DoMessage)) ) If LinkDisplay is not '', then it specifies explicite the display text for link. Else how exactly link does look like is controlled by @link(LinkLook) property. @param(FoundItem is the found item instance or nil if not found.) } function SearchLink(s: string; const Item: TBaseItem; const LinkDisplay: string; const WarningIfLinkNotFound: TLinkNotFoundAction; out FoundItem: TBaseItem): string; overload; { Just like previous overloaded version, but this doesn't return FoundItem (in case you don't need it). } function SearchLink(s: string; const Item: TBaseItem; const LinkDisplay: string; const WarningIfLinkNotFound: TLinkNotFoundAction): string; overload; procedure StoreDescription(ItemName: string; var t: string); { Writes S to CurrentStream, converting it using @link(ConvertString). Then optionally writes LineEnding. } procedure WriteConverted(const s: string; Newline: boolean); overload; { Writes S to CurrentStream, converting it using @link(ConvertString). No LineEnding at the end. } procedure WriteConverted(const s: string); overload; { Writes S to CurrentStream, converting it using @link(ConvertString). Then writes LineEnding. } procedure WriteConvertedLine(const s: string); { Simply writes T to CurrentStream, with optional LineEnding. } procedure WriteDirect(const t: string; Newline: boolean); overload; { Simply writes T to CurrentStream. } procedure WriteDirect(const t: string); overload; { Simply writes T followed by LineEnding to CurrentStream. } procedure WriteDirectLine(const t: string); { Abstract method that writes all documentation for a single unit U to output, starting at heading level HL. Implementation must be provided by descendant objects and is dependent on output format. } procedure WriteUnit(const HL: integer; const U: TPasUnit); virtual; abstract; { Writes documentation for all units, calling @link(WriteUnit) for each unit. } procedure WriteUnits(const HL: integer); procedure WriteStartOfCode; virtual; procedure WriteEndOfCode; virtual; { output graphviz uses tree } procedure WriteGVUses; { output graphviz class tree } procedure WriteGVClasses; { starts the spell checker } procedure StartSpellChecking(const AMode: string); { If CheckSpelling and spell checking was successfully started, this will run @link(TAspellProcess.CheckString FAspellProcess.CheckString) and will report all errors using DoMessage with mtWarning. Otherwise this just clears AErrors, which means that no errors were found. } procedure CheckString(const AString: string; const AErrors: TObjectVector); { closes the spellchecker } procedure EndSpellChecking; { FormatPascalCode will cause Line to be formatted in the way that Pascal code is formatted in Delphi. Note that given Line is taken directly from what user put inside @longcode(), it is not even processed by ConvertString. You should process it with ConvertString if you want. } function FormatPascalCode(const Line: string): string; virtual; { This will cause AString to be formatted in the way that normal Pascal statements (not keywords, strings, comments, etc.) look in Delphi. } function FormatNormalCode(AString: string): string; virtual; // FormatComment will cause AString to be formatted in // the way that comments other than compiler directives are // formatted in Delphi. See: @link(FormatCompilerComment). function FormatComment(AString: string): string; virtual; // FormatHex will cause AString to be formatted in // the way that Hex are formatted in Delphi. function FormatHex(AString: string): string; virtual; // FormatNumeric will cause AString to be formatted in // the way that Numeric are formatted in Delphi. function FormatNumeric(AString: string): string; virtual; // FormatFloat will cause AString to be formatted in // the way that Float are formatted in Delphi. function FormatFloat(AString: string): string; virtual; // FormatString will cause AString to be formatted in // the way that strings are formatted in Delphi. function FormatString(AString: string): string; virtual; // FormatKeyWord will cause AString to be formatted in // the way that reserved words are formatted in Delphi. function FormatKeyWord(AString: string): string; virtual; // FormatCompilerComment will cause AString to be formatted in // the way that compiler directives are formatted in Delphi. function FormatCompilerComment(AString: string): string; virtual; { This is paragraph marker in output documentation. Default implementation in this class simply returns ' ' (one space). } function Paragraph: string; virtual; { See @link(TTagManager.ShortDash). Default implementation in this class returns '-'. } function ShortDash: string; virtual; { See @link(TTagManager.EnDash). Default implementation in this class returns '@--'. } function EnDash: string; virtual; { See @link(TTagManager.EmDash). Default implementation in this class returns '@-@--'. } function EmDash: string; virtual; { S is guaranteed (guaranteed by the user) to be correct html content, this is taken directly from parameters of @html tag. Override this function to decide what to put in output on such thing. Note that S is not processed in any way, even with ConvertString. So you're able to copy user's input inside @@html() verbatim to the output. The default implementation is this class simply discards it, i.e. returns always ''. Generators that know what to do with HTML can override this with simple "Result := S". } function HtmlString(const S: string): string; virtual; { This is equivalent of @link(HtmlString) for @@latex tag. The default implementation is this class simply discards it, i.e. returns always ''. Generators that know what to do with raw LaTeX markup can override this with simple "Result := S". } function LatexString(const S: string): string; virtual; { @abstract(This returns markup that forces line break in given output format (e.g. '<br>' in html or '\\' in LaTeX).) It is used on @br tag (but may also be used on other occasions in the future). In this class it returns '', because it's valid for an output generator to simply ignore @br tags if linebreaks can't be expressed in given output format. } function LineBreak: string; virtual; { This should return markup upon finding URL in description. E.g. HTML generator will want to wrap this in <a href="...">...</a>. Note that passed here URL is @italic(not) processed by @link(ConvertString) (because sometimes it could be undesirable). If you want you can process URL with ConvertString when overriding this method. Default implementation in this class simply returns ConvertString(URL). This is good if your documentation format does not support anything like URL links. } function URLLink(const URL: string): string; overload; virtual; { This returns the Text which will be shown for an URL tag. URL is a link to a website or e-mail address. LinkDisplay is an optional parameter which will be used as the display name of the URL. } function URLLink(const URL, LinkDisplay: string): string; overload; virtual; {@name is used to write the introduction and conclusion of the project.} procedure WriteExternal(const ExternalItem: TExternalItem; const Id: TTranslationID); { This is called from @link(WriteExternal) when ExternalItem.Title and ShortTitle are already set, message about generating appropriate item is printed etc. This should write ExternalItem, including ExternalItem.DetailedDescription, ExternalItem.Authors, ExternalItem.Created, ExternalItem.LastMod. } procedure WriteExternalCore(const ExternalItem: TExternalItem; const Id: TTranslationID); virtual; abstract; {@name writes a conclusion for the project. See @link(WriteExternal).} procedure WriteConclusion; {@name writes an introduction for the project. See @link(WriteExternal).} procedure WriteIntroduction; {@name writes the other files for the project. See @link(WriteExternal).} procedure WriteAdditionalFiles; // @name writes a section heading and a link-anchor; function FormatSection(HL: integer; const Anchor: string; const Caption: string): string; virtual; abstract; // @name writes a link-anchor; function FormatAnchor(const Anchor: string): string; virtual; abstract; { This returns Text formatted using bold font. Given Text is already in the final output format (with characters converted using @link(ConvertString), @@-tags expanded etc.). Implementation of this method in this class simply returns @code(Result := Text). Output generators that can somehow express bold formatting (or at least emphasis of some text) should override this. @seealso(FormatItalic) } function FormatBold(const Text: string): string; virtual; { This returns Text formatted using italic font. Analogous to @link(FormatBold). } function FormatItalic(const Text: string): string; virtual; { This returns Text using bold font by calling FormatBold(Text). } function FormatWarning(const Text: string): string; virtual; { This returns Text using italic font by calling FormatItalic(Text). } function FormatNote(const Text: string): string; virtual; { This returns Text preserving spaces and line breaks. Note that Text passed here is not yet converted with ConvertString. The implementation of this method in this class just returns ConvertString(Text). } function FormatPreformatted(const Text: string): string; virtual; { Return markup to show an image. FileNames is a list of possible filenames of the image. FileNames always contains at least one item (i.e. FileNames.Count >= 1), never contains empty lines (i.e. Trim(FileNames[I]) <> ''), and contains only absolute filenames. E.g. HTML generator will want to choose the best format for HTML, then somehow copy the image from FileNames[Chosen] and wrap this in <img src="...">. Implementation of this method in this class simply shows @code(FileNames[0]). Output generators should override this. } function FormatImage(FileNames: TStringList): string; virtual; { Format a list from given ListData. } function FormatList(ListData: TListData): string; virtual; abstract; { This should return appropriate content for given Table. It's guaranteed that the Table passed here will have at least one row and in each row there will be at least one cell, so you don't have to check it within descendants. } function FormatTable(Table: TTableData): string; virtual; abstract; { Override this if you want to insert something on @@tableOfContents tag. As a parameter you get already prepared tree of sections that your table of contents should show. Each item of Sections is a section on the level 1. Item's Name is section name, item's Value is section caption, item's Data is a TStringPairVector instance that describes subsections (on level 2) below this section. And so on, recursively. Sections given here are never nil, and item's Data is never nil. But of course they may contain 0 items, and this should be a signal to you that given section doesn't have any subsections. Default implementation of this method in this class just returns empty string. } function FormatTableOfContents(Sections: TStringPairVector): string; virtual; public { Creates anchors and links for all items in all units. } procedure BuildLinks; virtual; { Expands description for each item in each unit of @link(Units). "Expands description" means that TTagManager.Execute is called, and item's DetailedDescription, AbstractDescription, AbstractDescriptionWasAutomatic (and many others, set by @@-tags handlers) properties are calculated. } procedure ExpandDescriptions; { Abstract function that provides file extension for documentation format. Must be overwritten by descendants. } function GetFileExtension: string; virtual; abstract; { Assumes C contains file names as PString variables. Calls @link(LoadDescriptionFile) with each file name. } procedure LoadDescriptionFiles(const c: TStringVector); { Must be overwritten, writes all documentation. Will create either a single file or one file for each unit and each class, interface or object, depending on output format. } procedure WriteDocumentation; virtual; property Units: TPasUnits read FUnits write FUnits; constructor Create(AOwner: TComponent); override; destructor Destroy; override; procedure ParseAbbreviationsFile(const AFileName: string); property Introduction: TExternalItem read FIntroduction write FIntroduction; property Conclusion: TExternalItem read FConclusion write FConclusion; property AdditionalFiles: TExternalItemList read FAdditionalFiles write FAdditionalFiles; { Callback receiving messages from generator. This is usually used internally by TPasDoc class, that assigns it's internal callback here when using this generator. Also, for the above reason, do not make this published. See TPasDoc.OnMessage for something more useful for final programs. } property OnMessage: TPasDocMessageEvent read FOnMessage write FOnMessage; published { the (human) output language of the documentation file(s) } property Language: TLanguageID read GetLanguage write SetLanguage default DEFAULT_LANGUAGE; { Name of the project to create. } property ProjectName: string read FProjectName write FProjectName; { "Generator info" are things that can change with each invocation of pasdoc, with different pasdoc binary etc. This includes @unorderedList( @item(pasdoc's compiler name and version,) @item(pasdoc's version and time of compilation) ) See [https://pasdoc.github.io/ExcludeGeneratorOption]. Default value is false (i.e. show them), as this information is generally considered useful. Setting this to true is useful for automatically comparing two versions of pasdoc's output (e.g. when trying to automate pasdoc's tests). } property ExcludeGenerator: Boolean read FExcludeGenerator write FExcludeGenerator default false; { Show creation time in the output. } property IncludeCreationTime: Boolean read FIncludeCreationTime write FIncludeCreationTime default false; { Setting to define how literal tag keywords should appear in documentaion. } property UseLowercaseKeywords: Boolean read FUseLowercaseKeywords write FUseLowercaseKeywords default false; { Title of the documentation, supplied by user. May be empty. See @link(TPasDoc.Title). } property Title: string read FTitle write FTitle; { Destination directory for documentation. Must include terminating forward slash or backslash so that valid file names can be created by concatenating DestinationDirectory and a pathless file name. } property DestinationDirectory: string read FDestDir write SetDestDir; { generate a GraphViz diagram for the units dependencies } property OutputGraphVizUses: boolean read FGraphVizUses write FGraphVizUses default false; { generate a GraphViz diagram for the Class hierarchy } property OutputGraphVizClassHierarchy: boolean read FGraphVizClasses write FGraphVizClasses default false; { link the GraphViz uses diagram } property LinkGraphVizUses: string read FLinkGraphVizUses write FLinkGraphVizUses; { link the GraphViz classes diagram } property LinkGraphVizClasses: string read FLinkGraphVizClasses write FLinkGraphVizClasses; property Abbreviations: TStringList read FAbbreviations write SetAbbreviations; property CheckSpelling: boolean read FCheckSpelling write FCheckSpelling default false; property AspellLanguage: string read FAspellLanguage write FAspellLanguage; property SpellCheckIgnoreWords: TStringList read FSpellCheckIgnoreWords write SetSpellCheckIgnoreWords; { The meaning of this is just like @--auto-abstract command-line option. It is used in @link(ExpandDescriptions). } property AutoAbstract: boolean read FAutoAbstract write FAutoAbstract default false; { This controls @link(SearchLink) behavior, as described in [https://pasdoc.github.io/LinkLookOption]. } property LinkLook: TLinkLook read FLinkLook write FLinkLook default llDefault; property WriteUsesClause: boolean read FWriteUsesClause write FWriteUsesClause default false; { This controls auto-linking, see [https://pasdoc.github.io/AutoLinkOption] } property AutoLink: boolean read FAutoLink write FAutoLink default false; property AutoLinkExclude: TStringList read FAutoLinkExclude; property ExternalClassHierarchy: TStrings read FExternalClassHierarchy write SetExternalClassHierarchy stored StoredExternalClassHierarchy; property Markdown: boolean read FMarkdown write FMarkdown default false; end; implementation uses SysUtils, StrUtils, PasDoc_Utils, PasDoc_Tokenizer; { TListItemData ------------------------------------------------------------- } constructor TListItemData.Create(AItemLabel, AText: string; AIndex: Integer); begin inherited Create; FItemLabel := AItemLabel; FText := AText; FIndex := AIndex; end; { TListData ----------------------------------------------------------------- } constructor TListData.Create(const AOwnsObject: boolean); begin inherited; FItemSpacing := lisParagraph; NextItemIndex := 1; end; { TRowData ------------------------------------------------------------------- } constructor TRowData.Create; begin inherited; Cells := TStringList.Create; end; destructor TRowData.Destroy; begin FreeAndNil(Cells); inherited; end; { TTableData ----------------------------------------------------------------- } procedure TTableData.CalculateCellCount; var i: Integer; CC: Cardinal; begin FMinCellCount := MaxInt; FMaxCellCount := 0; for i := 0 to Count - 1 do begin CC := TRowData(Items[i]).Cells.Count; if CC < FMinCellCount then FMinCellCount := CC; if CC > FMaxCellCount then FMaxCellCount := CC; end; end; { ---------------------------------------------------------------------------- } { TDocGenerator } { ---------------------------------------------------------------------------- } procedure TDocGenerator.BuildLinks; { Assign Cio.Ancestors.Objects[i] for every i } procedure AssignCioAncestorLinks(Cio: TPasCio); var i: Integer; begin for i := 0 to Cio.Ancestors.Count - 1 do Cio.Ancestors[i].Data := SearchItem(Cio.Ancestors[i].Name, Cio, true); end; { Assign MyXxx properties (MyUnit, MyObject, MyEnum) and FullLink for all items on the list. } procedure AssignLinks(MyUnit: TPasUnit; MyObject: TPasCio; MyEnum: TPasEnum; c: TPasItems); var i: Integer; p: TPasItem; begin if (not Assigned(c)) or (c.Count < 1) then Exit; for i := 0 to c.Count - 1 do begin p := c.PasItemAt[i]; p.MyObject := MyObject; p.MyUnit := MyUnit; p.MyEnum := MyEnum; p.FullLink := CreateLink(p); end; end; var U: TPasUnit; { Assign MyXxx properties (MyUnit, MyObject, MyEnum) and FullLink for all members of the enumerated types on this list. } procedure EnumsAssignLinks(const ATypes: TPasTypes; const ACio: TPasCio); var Enum: TPasEnum; I: Integer; begin for I := 0 to ATypes.Count - 1 do if ATypes.PasItemAt[I] is TPasEnum then begin Enum := TPasEnum(ATypes.PasItemAt[I]); AssignLinks(U, ACio, Enum, Enum.Members); end; end; { Assign MyXxx properties (MyUnit, MyObject, MyEnum), FullLink, OutputFileName and ansestor links for this Cio (classs / interface / object). } procedure CiosAssignLinks(ACios: TPasItems); var ACio : TPasCio; I : Integer; begin for I := 0 to ACios.Count -1 do begin ACio := TPasCio(ACios.PasItemAt[I]); ACio.MyUnit := U; ACio.FullLink := CreateLink(ACio); ACio.OutputFileName := ACio.FullLink; AssignCioAncestorLinks(ACio); AssignLinks(U, ACio, nil, ACio.Fields); AssignLinks(U, ACio, nil, ACio.Methods); AssignLinks(U, ACio, nil, ACio.Properties); AssignLinks(U, ACio, nil, ACio.Types); AssignLinks(U, ACio, nil, ACio.Cios); if not ObjectVectorIsNilOrEmpty(ACio.Types) then EnumsAssignLinks(ACio.Types, ACio); if ACio.Cios.Count > 0 then CiosAssignLinks(ACio.Cios); end; end; var i: Integer; j: Integer; begin DoMessage(2, pmtInformation, 'Creating links ...', []); if ObjectVectorIsNilOrEmpty(Units) then Exit; if Introduction <> nil then begin Introduction.FullLink := CreateLink(Introduction); Introduction.OutputFileName := Introduction.FullLink; end; if Conclusion <> nil then begin Conclusion.FullLink := CreateLink(Conclusion); Conclusion.OutputFileName := Conclusion.FullLink; end; if (AdditionalFiles <> nil) and (AdditionalFiles.Count > 0) then begin for i := 0 to AdditionalFiles.Count - 1 do begin AdditionalFiles.Get(i).FullLink := CreateLink(AdditionalFiles.Get(i)); AdditionalFiles.Get(i).OutputFileName := AdditionalFiles.Get(i).FullLink; end; end; for i := 0 to Units.Count - 1 do begin U := Units.UnitAt[i]; U.FullLink := CreateLink(U); U.OutputFileName := U.FullLink; for j := 0 to U.UsesUnits.Count - 1 do begin { Yes, this will also set U.UsesUnits.Objects[i] to nil if no such unit exists in Units table. } U.UsesUnits.Objects[j] := Units.FindListItem(U.UsesUnits[j]); end; AssignLinks(U, nil, nil, U.Constants); AssignLinks(U, nil, nil, U.Variables); AssignLinks(U, nil, nil, U.Types); AssignLinks(U, nil, nil, U.FuncsProcs); if not ObjectVectorIsNilOrEmpty(U.Types) then EnumsAssignLinks(U.Types, nil); if not ObjectVectorIsNilOrEmpty(U.CIOs) then CiosAssignLinks(U.CIOs); end; DoMessage(2, pmtInformation, '... ' + ' links created', []); end; { ---------------------------------------------------------------------------- } procedure TDocGenerator.CloseStream; begin if Assigned(FCurrentStream) then begin FCurrentStream.Free; FCurrentStream := nil; end; end; { ---------------------------------------------------------------------------- } function TDocGenerator.CreateLink(const Item: TBaseItem): string; begin Result := Item.Signature; end; { ---------------------------------------------------------------------------- } function TDocGenerator.CreateStream(const AName: string): Boolean; var S: string; begin CloseStream; DoMessage(4, pmtInformation, 'Creating output stream "' + AName + '".', []); Result := false; S := DestinationDirectory + AName; try FCurrentStream := {$IFDEF STRING_UNICODE} TStreamWriter.Create(S, false, false, FLanguage.CodePage); {$ELSE} {$IFDEF USE_BUFFERED_STREAM} TBufferedStream.Create(S, fmCreate); {$ELSE} TFileStream.Create(S, fmCreate); {$ENDIF} {$ENDIF} Result := true; except on E: Exception do DoMessage(1, pmtError, 'Could not create file "%s": %s', [S, E.Message]); end; end; { ---------------------------------------------------------------------------- } procedure TDocGenerator.HandleLongCodeTag( ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); begin // Always set ReplaceStr ReplaceStr := TagParameter; // Trim off "marker" characters at the beginning and end of TagParameter. // Do this only if they are the same character -- this way we are backward // compatible (in the past, matching characters were required), but were // not insisting on them being present in new code. if (Length(ReplaceStr) >= 2) and (ReplaceStr[1] = ReplaceStr[Length(ReplaceStr)]) then ReplaceStr := Copy(ReplaceStr, 2, Length(ReplaceStr) - 2); ReplaceStr := RemoveIndentation(ReplaceStr); // Then format pascal code. ReplaceStr := FormatPascalCode(ReplaceStr); end; procedure TDocGenerator.HandleHtmlTag( ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); begin ReplaceStr := HtmlString(TagParameter); end; procedure TDocGenerator.HandleLatexTag( ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); begin ReplaceStr := LatexString(TagParameter); end; procedure TDocGenerator.HandleNameTag( ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); begin ReplaceStr := CodeString(ConvertString(FCurrentItem.Name)); end; procedure TDocGenerator.HandleClassnameTag( ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); var ItemClassName: string; begin { TODO: this should be moved to TPasItem handler, so that @classname is registered only for TPasItem (or, even better, for TPasCio and TPasItem with MyObject <> nil). } ItemClassName := ''; if FCurrentItem is TPasItem then begin if Assigned(TPasItem(fCurrentItem).MyObject) then ItemClassName := TPasItem(fCurrentItem).MyObject.Name else if fCurrentItem is TPasCio then ItemClassName := fCurrentItem.Name; end; if ItemClassName <> '' then ReplaceStr := CodeString(ConvertString(ItemClassName)) else ThisTag.TagManager.DoMessage(1, pmtWarning, '@classname not available here', []); end; // handles @true, @false, @nil (Who uses these tags anyway?) procedure TDocGenerator.HandleLiteralTag( ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); begin if UseLowercaseKeywords then ReplaceStr := CodeString(ThisTag.Name) else ReplaceStr := CodeString(UpCase(ThisTag.Name[1]) + Copy(ThisTag.Name, 2, MaxInt)); end; procedure TDocGenerator.HandleInheritedClassTag( ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure InheritedClassCannotResolve(const Msg: string); begin ThisTag.TagManager.DoMessage(2, pmtWarning, 'Can''t resolve @inheritedClass: ' + Msg, []); ReplaceStr := CodeString(ConvertString(FCurrentItem.Name)); end; procedure HandleFromClass(TheObject: TPasCio); begin if TheObject.FirstAncestorName = '' then InheritedClassCannotResolve('No ancestor class') else if TheObject.FirstAncestor = nil then ReplaceStr := CodeString(ConvertString(TheObject.FirstAncestorName)) else ReplaceStr := MakeItemLink(TheObject.FirstAncestor, TheObject.FirstAncestorName, lcNormal); end; begin if FCurrentItem is TPasCio then HandleFromClass(TPasCio(FCurrentItem)) else if FCurrentItem is TPasItem then begin if Assigned(TPasItem(FCurrentItem).MyObject) then HandleFromClass(TPasItem(FCurrentItem).MyObject) else InheritedClassCannotResolve('This item is not a member of a class/interface/etc.'); end else InheritedClassCannotResolve('You can''t use @inheritedClass here'); end; procedure TDocGenerator.HandleInheritedTag( ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure InheritedCannotResolve(const Msg: string); begin ThisTag.TagManager.DoMessage(2, pmtWarning, 'Can''t resolve @inherited: ' + Msg, []); ReplaceStr := CodeString(ConvertString(FCurrentItem.Name)); end; var TheObject: TPasCio; InheritedItem: TPasItem; begin { TODO: this should be moved to TPasItem handler, so that @inherited is registered only for TPasItem (or, even better, for TPasCio and TPasItem with MyObject <> nil). } if FCurrentItem is TPasCio then begin TheObject := TPasCio(FCurrentItem); if TheObject.FirstAncestorName = '' then InheritedCannotResolve('No ancestor class') else if TheObject.FirstAncestor = nil then ReplaceStr := CodeString(ConvertString(TheObject.FirstAncestorName)) else ReplaceStr := MakeItemLink(TheObject.FirstAncestor, TheObject.FirstAncestorName, lcNormal); end else if FCurrentItem is TPasItem then begin InheritedItem := TPasItem(FCurrentItem).InheritedItem; if Assigned(InheritedItem) then begin Assert(Assigned(InheritedItem.MyObject), 'Only items on objects can be inherited'); ReplaceStr := MakeItemLink(InheritedItem, InheritedItem.MyObject.Name + '.' + InheritedItem.Name, lcNormal); end else InheritedCannotResolve(Format('Member "%s" not found in ancestors', [FCurrentItem.Name])) end else InheritedCannotResolve('You can''t use @inherited here'); end; procedure TDocGenerator.HandleLinkTag( ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); var LinkTarget, LinkDisplay: string; begin ExtractFirstWord(TagParameter, LinkTarget, LinkDisplay); ReplaceStr := SearchLink(LinkTarget, FCurrentItem, LinkDisplay, lnfWarn); end; procedure TDocGenerator.HandleUrlTag( ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); var LinkTarget, LinkDisplay: string; begin ExtractFirstWord(TagParameter, LinkTarget, LinkDisplay); ReplaceStr := URLLink(LinkTarget, LinkDisplay); end; procedure TDocGenerator.HandleCodeTag( ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); begin ReplaceStr := CodeString(TagParameter); end; procedure TDocGenerator.HandleWarningTag( ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); begin ReplaceStr := FormatWarning(TagParameter); end; procedure TDocGenerator.HandleNoteTag( ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); begin ReplaceStr := FormatNote(TagParameter); end; procedure TDocGenerator.HandleBrTag( ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); begin ReplaceStr := LineBreak; end; procedure TDocGenerator.HandleGroupTag( ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); begin ReplaceStr := ''; ThisTag.TagManager.DoMessage(1, pmtWarning, 'Tag "%s" is not implemented yet, ignoring', [ThisTag.Name]); end; procedure TDocGenerator.HandleBoldTag( ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); begin ReplaceStr := FormatBold(TagParameter); end; procedure TDocGenerator.HandleItalicTag( ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); begin ReplaceStr := FormatItalic(TagParameter); end; procedure TDocGenerator.HandlePreformattedTag( ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); begin ReplaceStr := FormatPreformatted(RemoveIndentation(TagParameter)); end; type { For @@orderedList, @@unorderedList and @@definitionList tags. } TListTag = class(TTag) function CreateOccurenceData: TObject; override; end; function TListTag.CreateOccurenceData: TObject; begin Result := TListData.Create(true); end; procedure TDocGenerator.HandleOrderedListTag( ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); var ListData: TListData; begin ListData := ThisTagData as TListData; ListData.FListType := ltOrdered; ReplaceStr := FormatList(ListData); end; procedure TDocGenerator.HandleUnorderedListTag( ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); var ListData: TListData; begin ListData := ThisTagData as TListData; ListData.FListType := ltUnordered; ReplaceStr := FormatList(ListData); end; procedure TDocGenerator.HandleDefinitionListTag( ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); var ListData: TListData; begin ListData := ThisTagData as TListData; ListData.FListType := ltDefinition; if ListData.LastItemLabel <> '' then begin ListData.Add(TListItemData.Create( ListData.LastItemLabel, '', ListData.NextItemIndex)); Inc(ListData.NextItemIndex); ListData.LastItemLabel := ''; end; ReplaceStr := FormatList(ListData); end; procedure TDocGenerator.HandleItemTag( ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); var ListData: TListData; begin ListData := EnclosingTagData as TListData; ListData.Add(TListItemData.Create( ListData.LastItemLabel, TagParameter, ListData.NextItemIndex)); Inc(ListData.NextItemIndex); ListData.LastItemLabel := ''; ReplaceStr := ''; end; procedure TDocGenerator.HandleItemLabelTag( ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); var ListData: TListData; begin ListData := EnclosingTagData as TListData; { If last tag was also @@itemLabel, not @@item, then make new item from ListData.LastItemLabel with empty Text. } if ListData.LastItemLabel <> '' then begin ListData.Add(TListItemData.Create( ListData.LastItemLabel, '', ListData.NextItemIndex)); Inc(ListData.NextItemIndex); end; { This @@itemLabel is stored inside ListData.LastItemLabel. Will be added later to ListData.Items wrapped inside some TListItemData. } ListData.LastItemLabel := TagParameter; ReplaceStr := ''; end; procedure TDocGenerator.HandleItemSpacingTag( ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); var ListData: TListData; LTagParameter: string; begin ListData := EnclosingTagData as TListData; LTagParameter := LowerCase(TagParameter); if LTagParameter = 'compact' then ListData.FItemSpacing := lisCompact else if LTagParameter = 'paragraph' then ListData.FItemSpacing := lisParagraph else ThisTag.TagManager.DoMessage(1, pmtWarning, 'Invalid parameter for @itemSpacing tag: "%s"', [TagParameter]); { @itemSpacing does not generate any output to ReplaceStr. It only sets ListData.ItemSpacing } ReplaceStr := ''; end; procedure TDocGenerator.HandleItemSetNumberTag( ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); var NewNextItemIndex: Integer; begin ReplaceStr := ''; try NewNextItemIndex := StrToInt(TagParameter); (EnclosingTagData as TListData).NextItemIndex := NewNextItemIndex; except on E: EConvertError do ThisTag.TagManager.DoMessage(1, pmtWarning, 'Cannot convert parameter of @itemSetNumber tag ("%s") to a number: %s', [TagParameter, E.Message]); end; end; type { For @@row and @@rowHead tags. } TRowTag = class(TTag) function CreateOccurenceData: TObject; override; end; TTableTag = class(TTag) function CreateOccurenceData: TObject; override; end; function TRowTag.CreateOccurenceData: TObject; begin Result := TRowData.Create; end; function TTableTag.CreateOccurenceData: TObject; begin Result := TTableData.Create(true); end; procedure TDocGenerator.HandleTableTag( ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); procedure Error(const S: string); begin ThisTag.TagManager.DoMessage(1, pmtWarning, S, []); ReplaceStr := ConvertString(S); end; var TableData: TTableData; begin TableData := ThisTagData as TTableData; if TableData.Count = 0 then begin Error('Invalid @table: no rows'); Exit; end; TableData.CalculateCellCount; if TableData.MinCellCount = 0 then begin Error('Invalid table @row: no cells'); Exit; end; ReplaceStr := FormatTable(TableData); end; procedure TDocGenerator.HandleSomeRowTag( ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); begin ReplaceStr := ''; (ThisTagData as TRowData).Head := ThisTag = RowHeadTag; (EnclosingTagData as TTableData).Add(ThisTagData); { Since we just added ThisTagData to EnclosingTagData, it should no longer be freed by DestroyOccurenceData. It will be freed when EnclosingTagData will be freed. } ThisTagData := nil; end; procedure TDocGenerator.HandleCellTag( ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); begin ReplaceStr := ''; (EnclosingTagData as TRowData).Cells.Append(TagParameter); end; procedure TDocGenerator.HandleNoAutoLinkTag( ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); begin ReplaceStr := ThisTag.TagManager.CoreExecute(TagParameter, false, ThisTag, ThisTagData); end; procedure TDocGenerator.HandleTableOfContentsTag( ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); var MaxLevel: Integer; AnchorIndex: Integer; ItemAnchors: TBaseItems; function CollectSections(MinLevel: Integer): TStringPairVector; var Anchor: TAnchorItem; SectionEntry: TStringPair; begin Result := TStringPairVector.Create(true); repeat if AnchorIndex = ItemAnchors.Count then Exit; Anchor := ItemAnchors[AnchorIndex] as TAnchorItem; if Anchor.SectionLevel = 0 then begin Inc(AnchorIndex); end else if Anchor.SectionLevel = MinLevel then begin Inc(AnchorIndex); SectionEntry := TStringPair.Create(Anchor.Name, Anchor.SectionCaption); SectionEntry.Data := CollectSections(MinLevel + 1); if MinLevel <= MaxLevel then Result.Add(SectionEntry) else begin TStringPairVector(SectionEntry.Data).Free; SectionEntry.Free; end; end else if Anchor.SectionLevel > MinLevel then begin { This is for the case of malformed sections, i.e. user suddenly specified section with level greater than the "last section level + 1". In the future we may just give a warning to the user and refuse to work in such case ? For now, we just try to go on and produce something sensible. } SectionEntry := TStringPair.Create('', ''); SectionEntry.Data := CollectSections(MinLevel + 1); if MinLevel <= MaxLevel then Result.Add(SectionEntry) else begin TStringPairVector(SectionEntry.Data).Free; SectionEntry.Free; end; end else { So Anchor.SectionLevel < MinLevel, so we have to return from recursive call. } Exit; until false; end; procedure FreeSectionsList(List: TStringPairVector); var i: Integer; begin for i := 0 to List.Count - 1 do FreeSectionsList(TStringPairVector(List[i].Data)); List.Free; end; var TopLevelSections: TStringPairVector; begin { calculate MaxLevel } if Trim(TagParameter) = '' then MaxLevel := MaxInt else try MaxLevel := StrToInt(TagParameter); except on E: EConvertError do begin ThisTag.TagManager.DoMessage(1, pmtWarning, 'Invalid parameter of @tableOfContents tag: "%s". %s', [TagParameter, E.Message]); Exit; end; end; { calculate ItemAnchors } ItemAnchors := (FCurrentItem as TExternalItem).Anchors; { calculate TopLevelSections } AnchorIndex := 0; TopLevelSections := CollectSections(1); { now make use of TopLevelSections -- call FormatTableOfContents } ReplaceStr := FormatTableOfContents(TopLevelSections); { free TopLevelSections } FreeSectionsList(TopLevelSections); end; procedure TDocGenerator.DoMessageFromExpandDescription( const MessageType: TPasDocMessageType; const AMessage: string; const AVerbosity: Cardinal); begin if Assigned(OnMessage) then OnMessage(MessageType, AMessage + ' (in description of "' + FCurrentItem.QualifiedName + '")', AVerbosity); end; procedure TDocGenerator.TryAutoLink(TagManager: TTagManager; const QualifiedIdentifier: TNameParts; out QualifiedIdentifierReplacement: string; var AutoLinked: boolean); var FoundItem: TBaseItem; QualifiedIdentifierGlued: string; begin QualifiedIdentifierGlued := GlueNameParts(QualifiedIdentifier); { first, check that we're not on AutoLinkExclude list } AutoLinked := AutoLinkExclude.IndexOf(QualifiedIdentifierGlued) = -1; if AutoLinked then begin FoundItem := FCurrentItem.FindName(QualifiedIdentifier); if FoundItem = nil then FoundItem := FindGlobal(QualifiedIdentifier); AutoLinked := (FoundItem <> nil) and FoundItem.AutoLinkHereAllowed; if AutoLinked then begin if FCurrentItem <> FoundItem then QualifiedIdentifierReplacement := MakeItemLink(FoundItem, QualifiedIdentifierGlued, lcNormal) else QualifiedIdentifierReplacement := CodeString(ConvertString(QualifiedIdentifierGlued)); end; end; end; { ---------------------------------------------------------------------------- } procedure TDocGenerator.ExpandDescriptions; (*Takes description D of the Item, expands links (using Item), converts output-specific characters. Note that you can't process with this function more than once the same Description (i.e. like @longcode(# { BAD EXAMPLE } Description := ExpandDescription(Item, Description); Description := ExpandDescription(Item, Description); #)) because output of this function is already something ready to be included in final doc output, it shouldn't be processed once more, moreover this function initializes some properties of Item to make them also in the "already-processed" form (ready to be included in final docs). Meaning of WantFirstSentenceEnd and FirstSentenceEnd: see @link(TTagManager.Execute). *) function ExpandDescription(PreExpand: boolean; Item: TBaseItem; const Description: string; WantFirstSentenceEnd: boolean; out FirstSentenceEnd: Integer): string; overload; var TagManager: TTagManager; ItemTag, ItemLabelTag, ItemSpacingTag, ItemSetNumberTag, CellTag: TTag; begin // make it available to the handlers FCurrentItem := Item; TagManager := TTagManager.Create; try TagManager.PreExecute := PreExpand; TagManager.Abbreviations := Abbreviations; TagManager.ConvertString := {$IFDEF FPC}@{$ENDIF} ConvertString; TagManager.URLLink := {$IFDEF FPC}@{$ENDIF} URLLink; TagManager.OnMessage := {$IFDEF FPC}@{$ENDIF} DoMessageFromExpandDescription; TagManager.OnTryAutoLink := {$IFDEF FPC}@{$ENDIF} TryAutoLink; TagManager.Paragraph := Paragraph; TagManager.ShortDash := ShortDash; TagManager.EnDash := EnDash; TagManager.EmDash := EmDash; TagManager.Markdown := Markdown; Item.RegisterTags(TagManager); { Tags without params } TTag.Create(TagManager, 'classname', nil, {$IFDEF FPC}@{$ENDIF} HandleClassnameTag, []); TTag.Create(TagManager, 'true', nil, {$IFDEF FPC}@{$ENDIF} HandleLiteralTag, []); TTag.Create(TagManager, 'false', nil, {$IFDEF FPC}@{$ENDIF} HandleLiteralTag, []); TTag.Create(TagManager, 'nil', nil, {$IFDEF FPC}@{$ENDIF} HandleLiteralTag, []); TTag.Create(TagManager, 'inheritedclass', nil, {$IFDEF FPC}@{$ENDIF} HandleInheritedClassTag, []); TTag.Create(TagManager, 'inherited', nil, {$IFDEF FPC}@{$ENDIF} HandleInheritedTag, []); TTag.Create(TagManager, 'name', nil, {$IFDEF FPC}@{$ENDIF} HandleNameTag, []); TTag.Create(TagManager, 'br', nil, {$IFDEF FPC}@{$ENDIF} HandleBrTag, []); TTag.Create(TagManager, 'groupbegin', nil, {$IFDEF FPC}@{$ENDIF} HandleGroupTag, []); TTag.Create(TagManager, 'groupend', nil, {$IFDEF FPC}@{$ENDIF} HandleGroupTag, []); { Tags with non-recursive params } TTag.Create(TagManager, 'longcode', nil, {$IFDEF FPC}@{$ENDIF} HandleLongCodeTag, [toParameterRequired]); TTag.Create(TagManager, 'html', nil, {$IFDEF FPC}@{$ENDIF} HandleHtmlTag, [toParameterRequired]); TTag.Create(TagManager, 'latex', nil, {$IFDEF FPC}@{$ENDIF} HandleLatexTag, [toParameterRequired]); TTag.Create(TagManager, 'link', nil, {$IFDEF FPC}@{$ENDIF} HandleLinkTag, [toParameterRequired]); TTag.Create(TagManager, 'url', nil, {$IFDEF FPC}@{$ENDIF} HandleUrlTag, [toParameterRequired]); TTag.Create(TagManager, 'preformatted', nil, {$IFDEF FPC}@{$ENDIF} HandlePreformattedTag, [toParameterRequired]); TTag.Create(TagManager, 'image', nil, {$IFDEF FPC}@{$ENDIF} HandleImageTag, [toParameterRequired]); TTag.Create(TagManager, 'include', { @include tag works the same way in both expanding passes. } {$IFDEF FPC}@{$ENDIF} HandleIncludeTag, {$IFDEF FPC}@{$ENDIF} HandleIncludeTag, [toParameterRequired]); TTag.Create(TagManager, 'includeCode', nil, {$IFDEF FPC}@{$ENDIF} HandleIncludeCodeTag, [toParameterRequired]); { Tags with recursive params } TTag.Create(TagManager, 'code', nil, {$IFDEF FPC}@{$ENDIF} HandleCodeTag, [toParameterRequired, toRecursiveTags, toAllowOtherTagsInsideByDefault, toAllowNormalTextInside]); TTag.Create(TagManager, 'bold', nil, {$IFDEF FPC}@{$ENDIF} HandleBoldTag, [toParameterRequired, toRecursiveTags, toAllowOtherTagsInsideByDefault, toAllowNormalTextInside]); TTag.Create(TagManager, 'italic', nil, {$IFDEF FPC}@{$ENDIF} HandleItalicTag, [toParameterRequired, toRecursiveTags, toAllowOtherTagsInsideByDefault, toAllowNormalTextInside]); TTag.Create(TagManager, 'warning', nil, {$IFDEF FPC}@{$ENDIF} HandleWarningTag, [toParameterRequired, toRecursiveTags, toAllowOtherTagsInsideByDefault, toAllowNormalTextInside]); TTag.Create(TagManager, 'note', nil, {$IFDEF FPC}@{$ENDIF} HandleNoteTag, [toParameterRequired, toRecursiveTags, toAllowOtherTagsInsideByDefault, toAllowNormalTextInside]); TTag.Create(TagManager, 'noautolink', nil, {$IFDEF FPC}@{$ENDIF} HandleNoAutoLinkTag, [toParameterRequired, toRecursiveTagsManually, toAllowOtherTagsInsideByDefault, toAllowNormalTextInside]); OrderedListTag := TListTag.Create(TagManager, 'orderedlist', nil, {$IFDEF FPC}@{$ENDIF} HandleOrderedListTag, [toParameterRequired, toRecursiveTags]); UnorderedListTag := TListTag.Create(TagManager, 'unorderedlist', nil, {$IFDEF FPC}@{$ENDIF} HandleUnorderedListTag, [toParameterRequired, toRecursiveTags]); DefinitionListTag := TListTag.Create(TagManager, 'definitionlist', nil, {$IFDEF FPC}@{$ENDIF} HandleDefinitionListTag, [toParameterRequired, toRecursiveTags]); ItemTag := TTag.Create(TagManager, 'item', nil, {$IFDEF FPC}@{$ENDIF} HandleItemTag, [toParameterRequired, toRecursiveTags, toAllowOtherTagsInsideByDefault, toAllowNormalTextInside]); ItemTag.OnAllowedInside := {$IFDEF FPC}@{$ENDIF} TagAllowedInsideLists; ItemLabelTag := TTag.Create(TagManager, 'itemlabel', nil, {$IFDEF FPC}@{$ENDIF} HandleItemLabelTag, [toParameterRequired, toRecursiveTags, toAllowOtherTagsInsideByDefault, toAllowNormalTextInside]); ItemLabelTag.OnAllowedInside := {$IFDEF FPC}@{$ENDIF} ItemLabelTagAllowedInside; ItemSpacingTag := TTag.Create(TagManager, 'itemspacing', nil, {$IFDEF FPC}@{$ENDIF} HandleItemSpacingTag, [toParameterRequired]); ItemSpacingTag.OnAllowedInside := {$IFDEF FPC}@{$ENDIF} TagAllowedInsideLists; ItemSetNumberTag := TTag.Create(TagManager, 'itemsetnumber', nil, {$IFDEF FPC}@{$ENDIF} HandleItemSetNumberTag, [toParameterRequired, toAllowNormalTextInside]); ItemSetNumberTag.OnAllowedInside := {$IFDEF FPC}@{$ENDIF} TagAllowedInsideLists; TableTag := TTableTag.Create(TagManager, 'table', nil, {$IFDEF FPC}@{$ENDIF} HandleTableTag, [toParameterRequired, toRecursiveTags]); RowTag := TRowTag.Create(TagManager, 'row', nil, {$IFDEF FPC}@{$ENDIF} HandleSomeRowTag, [toParameterRequired, toRecursiveTags]); RowTag.OnAllowedInside := {$IFDEF FPC}@{$ENDIF} TagAllowedInsideTable; RowHeadTag := TRowTag.Create(TagManager, 'rowhead', nil, {$IFDEF FPC}@{$ENDIF} HandleSomeRowTag, [toParameterRequired, toRecursiveTags]); RowHeadTag.OnAllowedInside := {$IFDEF FPC}@{$ENDIF} TagAllowedInsideTable; CellTag := TTag.Create(TagManager, 'cell', nil, {$IFDEF FPC}@{$ENDIF} HandleCellTag, [toParameterRequired, toRecursiveTags, toAllowOtherTagsInsideByDefault, toAllowNormalTextInside]); CellTag.OnAllowedInside := {$IFDEF FPC}@{$ENDIF} TagAllowedInsideRows; if FCurrentItem is TExternalItem then begin TTopLevelTag.Create(TagManager, 'section', {$IFDEF FPC}@{$ENDIF} PreHandleSectionTag, {$IFDEF FPC}@{$ENDIF} HandleSectionTag, [toParameterRequired]); TTopLevelTag.Create(TagManager, 'anchor', {$IFDEF FPC}@{$ENDIF} PreHandleAnchorTag, {$IFDEF FPC}@{$ENDIF} HandleAnchorTag, [toParameterRequired]); TTopLevelTag.Create(TagManager, 'tableofcontents', nil, {$IFDEF FPC}@{$ENDIF} HandleTableOfContentsTag, [toParameterRequired]); end; Result := TagManager.Execute(Description, AutoLink, WantFirstSentenceEnd, FirstSentenceEnd); finally TagManager.Free; end; end; { Same thing as ExpandDescription(PreExpand, Item, Description, false, Dummy) } function ExpandDescription(PreExpand: boolean; Item: TBaseItem; const Description: string): string; overload; var Dummy: Integer; begin Result := ExpandDescription(PreExpand, Item, Description, false, Dummy); end; procedure ExpandCollection(PreExpand: boolean; c: TPasItems); forward; { expands RawDescription of Item } procedure ExpandPasItem(PreExpand: boolean; Item: TPasItem); var FirstSentenceEnd: Integer; Expanded: string; begin if Item = nil then Exit; { Note: don't just Trim or TrimCompress here resulting Item.DetailedDescription (because whitespaces, including leading and trailing, may be important for final doc format; moreover, you would break the value of FirstSentenceEnd by such thing). } Expanded := ExpandDescription(PreExpand, Item, Trim(Item.RawDescription), true, FirstSentenceEnd); if not PreExpand then begin Item.DetailedDescription := Expanded; Item.AbstractDescriptionWasAutomatic := AutoAbstract and (Trim(Item.AbstractDescription) = ''); if Item.AbstractDescriptionWasAutomatic then begin Item.AbstractDescription := Copy(Item.DetailedDescription, 1, FirstSentenceEnd); Item.DetailedDescription := Copy(Item.DetailedDescription, FirstSentenceEnd + 1, MaxInt); end; end; if Item is TPasEnum then ExpandCollection(PreExpand, TPasEnum(Item).Members); end; procedure ExpandExternalItem(PreExpand: boolean; Item: TExternalItem); var Expanded: string; begin Expanded := ExpandDescription(PreExpand, Item, Trim(Item.RawDescription)); if not PreExpand then Item.DetailedDescription := Expanded; end; { for all items in collection C, expands descriptions } procedure ExpandCollection(PreExpand: boolean; c: TPasItems); var i: Integer; begin if c = nil then Exit; for i := 0 to c.Count - 1 do ExpandPasItem(PreExpand, c.PasItemAt[i]); end; procedure ExpandEverything(PreExpand: boolean); procedure CiosExpand(const ACios: TPasItems); procedure CioExpand(const ACio: TPasCio); begin ExpandPasItem(PreExpand, ACio); ExpandCollection(PreExpand, ACio.Fields); ExpandCollection(PreExpand, ACio.Methods); ExpandCollection(PreExpand, ACio.Properties); ExpandCollection(PreExpand, ACio.Types); if ACio.Cios.Count > 0 then CiosExpand(ACio.Cios); end; var I: Integer; LCio: TPasCio; begin for I := 0 to ACios.Count - 1 do begin LCio := TPasCio(ACios.PasItemAt[I]); CioExpand(LCio); end; end; var i: Integer; U: TPasUnit; begin if Introduction <> nil then begin ExpandExternalItem(PreExpand, Introduction); end; if Conclusion <> nil then begin ExpandExternalItem(PreExpand, Conclusion); end; if (AdditionalFiles <> nil) and (AdditionalFiles.Count > 0) then begin for i := 0 to AdditionalFiles.Count - 1 do begin ExpandExternalItem(PreExpand, AdditionalFiles.Get(i)); end; end; for i := 0 to Units.Count - 1 do begin U := Units.UnitAt[i]; ExpandPasItem(PreExpand, U); ExpandCollection(PreExpand, U.Constants); ExpandCollection(PreExpand, U.Variables); ExpandCollection(PreExpand, U.Types); ExpandCollection(PreExpand, U.FuncsProcs); if not ObjectVectorIsNilOrEmpty(U.CIOs) then CiosExpand(U.CIOs); end; end; begin DoMessage(2, pmtInformation, 'Expanding descriptions (pass 1) ...', []); ExpandEverything(true); DoMessage(2, pmtInformation, 'Expanding descriptions (pass 2) ...', []); ExpandEverything(false); DoMessage(2, pmtInformation, '... Descriptions expanded', []); end; { ---------------------------------------------------------------------------- } function TDocGenerator.ExtractEmailAddress(s: string; out S1, S2, EmailAddress: string): Boolean; const ALLOWED_CHARS = ['a'..'z', 'A'..'Z', '-', '.', '_', '0'..'9']; Letters = ['a'..'z', 'A'..'Z']; var atPos: Integer; i: Integer; begin Result := False; if (Length(s) < 6) { minimum length of email address: a@b.cd } then Exit; atPos := Pos('@', s); if (atPos < 2) or (atPos > Length(s) - 3) then Exit; { assemble address left of @ } i := atPos - 1; while (i >= 1) and IsCharInSet(s[i], ALLOWED_CHARS) do Dec(i); EmailAddress := System.Copy(s, i + 1, atPos - i - 1) + '@'; S1 := ''; if (i > 1) then S1 := System.Copy(s, 1, i); { assemble address right of @ } i := atPos + 1; while (i <= Length(s)) and IsCharInSet(s[i], ALLOWED_CHARS) do Inc(i); EmailAddress := EmailAddress + System.Copy(s, atPos + 1, i - atPos - 1); if (Length(EmailAddress) < 6) or (not IsCharInSet(EmailAddress[Length(EmailAddress)], Letters)) or (not IsCharInSet(EmailAddress[Length(EmailAddress) - 1], Letters)) then Exit; S2 := ''; if (i <= Length(s)) then S2 := System.Copy(s, i, Length(s) - i + 1); Result := True; end; function TDocGenerator.FixEmailaddressWithoutMailTo(const PossibleEmailAddress: String): String; const EMailAddressPrefix = 'mailto:'; var a, b, Email: String; begin if ExtractEmailAddress(PossibleEmailAddress, a, b, Email) then begin if not AnsiStartsText(EMailAddressPrefix, Email) then Result := EMailAddressPrefix + Email; end else Result := PossibleEmailAddress; end; function TDocGenerator.ExtractWebAddress(s: string; out S1, S2, WebAddress: string): Boolean; const ALLOWED_CHARS = ['a'..'z', 'A'..'Z', '-', '.', '_', '0'..'9']; var p: integer; begin Result := false; p := Pos('http://', s); if p > 0 then begin { if it starts with "http://" it is at least meant to be a web address } S1 := Copy(s, 1, p - 1); WebAddress := Copy(s, p + 7, 255); p := 1; while (p < Length(WebAddress)) and IsCharInSet(WebAddress[p], ALLOWED_CHARS) do Inc(p); S2 := Copy(WebAddress, p, 255); WebAddress := Copy(WebAddress, 1, p - 1); Result := true; end else begin p := Pos('www.', s); if p = 0 then exit; { if it starts with "www.", its most likely a web address, we could probably add more checks here (like: does it contain an additional dot for the TLD?) } S1 := Copy(s, 1, p - 1); WebAddress := Copy(s, p, 255); p := 1; while (p < Length(WebAddress)) and IsCharInSet(WebAddress[p], ALLOWED_CHARS) do Inc(p); S2 := Copy(WebAddress, p, 255); WebAddress := Copy(WebAddress, 1, p - 1); Result := true; end; end; { ---------------------------------------------------------------------------- } function TDocGenerator.FindGlobal( const NameParts: TNameParts): TBaseItem; var i, UnitNamePartIdx: Integer; Item: TBaseItem; U: TPasUnit; NewNameParts: TNameParts; FullUnitName: string; begin Result := nil; if ObjectVectorIsNilOrEmpty(Units) then Exit; { Units could have multipart names with dots (a-la namespace). So we first must check whether NameParts contains multiple parts of a unit name and correct the array accordingly. There's no sense in check if NameParts hold a single item. Similarily we start check starting from two-part name as one-part name is default. } if Length(NameParts) > 1 then begin FullUnitName := NameParts[0]; UnitNamePartIdx := 1; // start check from two-part name repeat FullUnitName := FullUnitName + '.' + NameParts[UnitNamePartIdx]; U := TPasUnit(Units.FindListItem(FullUnitName)); if U = nil then Inc(UnitNamePartIdx) else Break; until UnitNamePartIdx >= High(NameParts); { So now we have full unit name and index until which unit name is spread. Construct new nameparts array with unit name glued together. } if (U <> nil) and (UnitNamePartIdx >= 1) then // Skip simple case of single-part unit name begin SetLength(NewNameParts, Length(NameParts) - UnitNamePartIdx); NewNameParts[0] := FullUnitName; for i := 1 to High(NewNameParts) do NewNameParts[i] := NameParts[UnitNamePartIdx + i]; end else NewNameParts := NameParts; end else NewNameParts := NameParts; case Length(NewNameParts) of 1: begin if (Introduction <> nil) then begin if SameText(Introduction.Name, NewNameParts[0]) then begin Result := Introduction; Exit; end; Result := Introduction.FindItem(NewNameParts[0]); if Result <> nil then Exit; end; if (Conclusion <> nil) then begin if SameText(Conclusion.Name, NewNameParts[0]) then begin Result := Conclusion; Exit; end; Result := Conclusion.FindItem(NewNameParts[0]); if Result <> nil then Exit; end; if (AdditionalFiles <> nil) and (AdditionalFiles.Count > 0) then begin for i := 0 to AdditionalFiles.Count - 1 do begin if SameText(AdditionalFiles.Get(i).Name, NewNameParts[0]) then begin Result := AdditionalFiles.Get(i); Exit; end; Result := AdditionalFiles.Get(i).FindItem(NewNameParts[0]); if Result <> nil then Exit; end; end; for i := 0 to Units.Count - 1 do begin U := Units.UnitAt[i]; if SameText(U.Name, NewNameParts[0]) then begin Result := U; Exit; end; Result := U.FindItem(NewNameParts[0]); if Result <> nil then Exit; end; end; 2: begin { object.field_method_property } for i := 0 to Units.Count - 1 do begin Result := Units.UnitAt[i].FindInsideSomeClass(NewNameParts[0], NewNameParts[1]); if Assigned(Result) then Exit; end; { unit.cio_var_const_type } U := TPasUnit(Units.FindListItem(NewNameParts[0])); if Assigned(U) then Result := U.FindItem(NewNameParts[1]); end; 3: begin { unit.objectorclassorinterface.fieldormethodorproperty } U := TPasUnit(Units.FindListItem(NewNameParts[0])); if (not Assigned(U)) then Exit; Item := U.FindItem(NewNameParts[1]); if (not Assigned(Item)) then Exit; Item := Item.FindItem(NewNameParts[2]); if (not Assigned(Item)) then Exit; Result := Item; Exit; end; end; end; function TDocGenerator.FindGlobalPasItem(const NameParts: TNameParts): TPasItem; var BaseResult: TBaseItem; begin BaseResult := FindGlobal(NameParts); if BaseResult is TPasItem then Result := TPasItem(BaseResult) else Result := nil; end; function TDocGenerator.FindGlobalPasItem(const ItemName: String): TPasItem; begin Result := FindGlobalPasItem(OneNamePart(ItemName)); end; { ---------------------------------------------------------------------------- } function TDocGenerator.GetClassDirectiveName(Directive: TClassDirective): string; begin case Directive of CT_NONE: begin result := ''; end; CT_ABSTRACT: begin result := ' abstract'; end; CT_SEALED: begin result := ' sealed'; end; CT_HELPER: begin result := ' helper'; end; else Assert(False); end; end; function TDocGenerator.GetCIOTypeName(MyType: TCIOType): string; begin case MyType of CIO_CLASS: Result := FLanguage.Translation[trClass]; CIO_PACKEDCLASS: Result := FLanguage.Translation[trPacked] + ' ' + FLanguage.Translation[trClass]; CIO_DISPINTERFACE: Result := FLanguage.Translation[trDispInterface]; CIO_INTERFACE: Result := FLanguage.Translation[trInterface]; CIO_OBJECT: Result := FLanguage.Translation[trObject]; CIO_PACKEDOBJECT: Result := FLanguage.Translation[trPacked] + ' ' + FLanguage.Translation[trObject]; CIO_RECORD: Result := FLanguage.Translation[trRecord]; CIO_PACKEDRECORD: Result := FLanguage.Translation[trPacked] + ' ' + FLanguage.Translation[trRecord]; CIO_TYPE: Result := FLanguage.Translation[trType]; else Result := ''; end; end; { ---------------------------------------------------------------------------- } procedure TDocGenerator.LoadDescriptionFile(n: string); var {$IFDEF STRING_UNICODE} f : TStreamReader; {$ELSE} f : TStream; {$ENDIF} ItemName : string; Description : string; i : Integer; s : string; const IdentChars = ['A'..'Z', 'a'..'z', '_', '.', '0'..'9']; begin ItemName := ''; if n = '' then Exit; try {$IFDEF STRING_UNICODE} f := TStreamReader.Create(n); {$ELSE} {$IFDEF USE_BUFFERED_STREAM} f := TBufferedStream.Create(n, fmOpenRead or fmShareDenyWrite); {$ELSE} f := TFileStream.Create(n, fmOpenRead or fmShareDenyWrite); {$ENDIF} {$ENDIF} // Assert(Assigned(f)); useless here try {$IFDEF STRING_UNICODE} while f.ReadLine(S) do begin {$ELSE} while f.Position < f.Size do begin s := StreamReadLine(f); {$ENDIF} if s[1] = '#' then begin i := 2; while IsCharInSet(s[i], [' ', #9]) do Inc(i); { Make sure we read a valid name - the user might have used # in his description. } if IsCharInSet(s[i], IdentChars) then begin if ItemName <> '' then StoreDescription(ItemName, Description); { Read item name and beginning of the description } ItemName := ''; repeat ItemName := ItemName + s[i]; Inc(i); until not IsCharInSet(s[i], IdentChars); while IsCharInSet(s[i], [' ', #9]) do Inc(i); Description := Copy(s, i, MaxInt); Continue; end; end; Description := Description + s; end; if ItemName = '' then DoMessage(2, pmtWarning, 'No descriptions read from "%s" -- invalid or empty file', [n]) else StoreDescription(ItemName, Description); finally f.Free; end; except on E: Exception do DoError('Could not open description file "%s". Reason: "%s"', [n, E.Message], 0); end; end; {TDocGenerator.LoadDescriptionFile} { ---------------------------------------------------------------------------- } procedure TDocGenerator.LoadDescriptionFiles(const c: TStringVector); var i: Integer; begin if c <> nil then begin DoMessage(3, pmtInformation, 'Loading description files ...', []); for i := 0 to c.Count - 1 do LoadDescriptionFile(c[i]); end; end; { ---------------------------------------------------------------------------- } function TDocGenerator.SearchItem(s: string; const Item: TBaseItem; WarningIfNotSplittable: boolean): TBaseItem; var NameParts: TNameParts; begin if not SplitNameParts(s, NameParts) then begin if WarningIfNotSplittable then DoMessage(2, pmtWarning, 'The link "' + s + '" is invalid', []); Result := nil; Exit; end; { first try to find link starting at Item } if Assigned(Item) then Result := Item.FindName(NameParts) else Result := nil; if not Assigned(Result) then Result := FindGlobal(NameParts); end; { ---------------------------------------------------------------------------- } function TDocGenerator.SearchLink(s: string; const Item: TBaseItem; const LinkDisplay: string; const WarningIfLinkNotFound: TLinkNotFoundAction; out FoundItem: TBaseItem): string; var NameParts: TNameParts; OriginalNamePartsStr: String; begin FoundItem := nil; if (not SplitNameParts(s, NameParts)) then begin DoMessage(2, pmtWarning, 'Invalid Link "' + s + '" (' + Item.QualifiedName + ')', []); Result := 'UNKNOWN'; Exit; end; { first try to find link starting at Item } if Assigned(Item) then begin FoundItem := Item.FindName(NameParts); end; { Find Global } if FoundItem = nil then FoundItem := FindGlobal(NameParts); if Assigned(FoundItem) then begin if LinkDisplay <> '' then Result := MakeItemLink(FoundItem, LinkDisplay, lcNormal) else case LinkLook of llDefault: Result := MakeItemLink(FoundItem, S, lcNormal); llStripped: Result := MakeItemLink(FoundItem, FoundItem.Name, lcNormal); llFull: begin Result := MakeItemLink(FoundItem, FoundItem.Name, lcNormal); OriginalNamePartsStr := GlueNameParts(NameParts); if Length(NameParts) = 3 then begin SetLength(NameParts, 2); FoundItem := FindGlobal(NameParts); if FoundItem <> nil then begin Result := MakeItemLink(FoundItem, FoundItem.Name, lcNormal) + '.' + Result; end else begin DoMessage(1, pmtWarning, 'Could not resolve link "%s" (from description of "%s"), necessary because LinkLook=llFull and original link was to "%s"', [ GlueNameParts(NameParts), Item.QualifiedName, OriginalNamePartsStr ]); end; end; if Length(NameParts) = 2 then begin SetLength(NameParts, 1); FoundItem := FindGlobal(NameParts); if FoundItem <> nil then begin Result := MakeItemLink(FoundItem, FoundItem.Name, lcNormal) + '.' + Result; end else begin DoMessage(1, pmtWarning, 'Could not resolve link "%s" (from description of "%s"), necessary because LinkLook=llFull and original link was to "%s"', [ GlueNameParts(NameParts), Item.QualifiedName, OriginalNamePartsStr ]); end; end; end; else Assert(false, 'LinkLook = ??'); end; end else begin if (WarningIfLinkNotFound = lnfWarnIfNotInternal) and (FExternalClassHierarchy.IndexOfName(S) <> -1) then begin DoMessage( 6, pmtInformation, 'Link "%s" resolved as reference to external class hierarchy (from description of "%s")', [S, Item.QualifiedName]); end else if (WarningIfLinkNotFound <> lnfIgnore) then begin DoMessage(1, pmtWarning, 'Could not resolve link "%s" (from description of "%s")', [S, Item.QualifiedName]); end; Result := CodeString(ConvertString(S)); end; end; function TDocGenerator.SearchLink(s: string; const Item: TBaseItem; const LinkDisplay: string; const WarningIfLinkNotFound: TLinkNotFoundAction): string; var Dummy: TBaseItem; begin Result := SearchLink(S, Item, LinkDisplay, WarningIfLinkNotFound, Dummy); end; { ---------------------------------------------------------------------------- } procedure TDocGenerator.StoreDescription(ItemName: string; var t: string); var Item: TBaseItem; NameParts: TNameParts; begin if t = '' then Exit; DoMessage(5, pmtInformation, 'Storing description for ' + ItemName, []); if SplitNameParts(ItemName, NameParts) then begin Item := FindGlobal(NameParts); if Assigned(Item) then begin if Item.RawDescription <> '' then { Delimit previous contents of Item.RawDescription with a paragraph } Item.RawDescription := Item.RawDescription + LineEnding + LineEnding; Item.RawDescription := Item.RawDescription + t; end else DoMessage(2, pmtWarning, 'Could not find item ' + ItemName, []); end else DoMessage(2, pmtWarning, 'Could not split item "' + ItemName + '"', []); t := ''; end; { ---------------------------------------------------------------------------- } procedure TDocGenerator.WriteConverted(const s: string; Newline: boolean); begin WriteDirect(ConvertString(s), Newline); end; procedure TDocGenerator.WriteConverted(const s: string); begin WriteDirect(ConvertString(s)); end; procedure TDocGenerator.WriteConvertedLine(const s: string); begin WriteDirectLine(ConvertString(s)); end; { ---------------------------------------------------------------------------- } procedure TDocGenerator.WriteDirect(const t: string; Newline: boolean); begin if NewLine then WriteDirectLine(T) else WriteDirect(T); end; procedure TDocGenerator.WriteDirect(const t: string); begin {$IFDEF STRING_UNICODE} CurrentStream.Write(t); {$ELSE} StreamWriteString(CurrentStream, AnsiString(t)); {$ENDIF} end; procedure TDocGenerator.WriteDirectLine(const t: string); begin {$IFDEF STRING_UNICODE} CurrentStream.WriteLine(t); {$ELSE} StreamWriteLine(CurrentStream, AnsiString(t)); {$ENDIF} end; { ---------------------------------------------------------------------------- } procedure TDocGenerator.WriteUnits(const HL: integer); var i: Integer; begin if ObjectVectorIsNilOrEmpty(Units) then Exit; for i := 0 to Units.Count - 1 do begin WriteUnit(HL, Units.UnitAt[i]); end; end; { ---------------------------------------------------------------------------- } procedure TDocGenerator.DoError(const AMessage: string; const AArguments: array of const; const AExitCode: Word); begin raise EPasDoc.Create(AMessage, AArguments, AExitCode); end; { ---------------------------------------------------------------------------- } procedure TDocGenerator.DoMessage(const AVerbosity: Cardinal; const MessageType: TPasDocMessageType; const AMessage: string; const AArguments: array of const); begin if Assigned(FOnMessage) then begin FOnMessage(MessageType, Format(AMessage, AArguments), AVerbosity); end; end; const DefaultExternalClassHierarchy = {$I external_class_hierarchy.txt.inc}; constructor TDocGenerator.Create(AOwner: TComponent); begin inherited; FClassHierarchy := nil; FExcludeGenerator := false; FIncludeCreationTime := false; FUseLowercaseKeywords := false; FLanguage := TPasDocLanguages.Create; FAbbreviations := TStringList.Create; FAbbreviations.Duplicates := dupIgnore; FSpellCheckIgnoreWords := TStringList.Create; FAutoLinkExclude := TStringList.Create; FAutoLinkExclude.CaseSensitive := false; FExternalClassHierarchy := TStringList.Create; FExternalClassHierarchy.Text := DefaultExternalClassHierarchy; TStringList(FExternalClassHierarchy).CaseSensitive := false; end; destructor TDocGenerator.Destroy; begin FreeAndNil(FExternalClassHierarchy); FreeAndNil(FAutoLinkExclude); FSpellCheckIgnoreWords.Free; FLanguage.Free; FClassHierarchy.Free; FAbbreviations.Free; FCurrentStream.Free; inherited; end; procedure TDocGenerator.CreateClassHierarchy; { Insert a parent->child relation into the tree. ParentItem may be nil, then only ParentName (not linked) will be used. When ParentName is '', this child has no parent (for example, maybe it's TObject class). ChildItem also may be nil, then only ChildName (not linked) will be used. ChildName must not be empty. } procedure Insert(const ParentName: string; ParentItem: TPasItem; const ChildName: string; ChildItem: TPasItem); var Parent, Child: TPasItemNode; GrandParentName: string; GrandParentItem: TPasItem; begin if Assigned(ParentItem) then begin Parent := FClassHierarchy.ItemOfName(ParentItem.Name); // Add parent if not already there. if Parent = nil then Parent := FClassHierarchy.InsertItem(ParentItem); end else if Length(ParentName) <> 0 then begin Parent := FClassHierarchy.ItemOfName(ParentName); if Parent = nil then begin Parent := FClassHierarchy.InsertName(ParentName); { We add a new item to the tree that is not a TPasItem. So look for it's parents using ExternalClassHierarchy. } GrandParentName := ExternalClassHierarchy.Values[ParentName]; if GrandParentName <> '' then begin { Although we found GrandParentName using ExternalClassHierarchy, it's possible that it's actually present in parsed files. This may happen when you have classes A -> B -> C (descending like this), and your source code includes classes A and C, but not B. So we have to use here FindGlobalPasItem. } GrandParentItem := FindGlobalPasItem(GrandParentName); Insert(GrandParentName, GrandParentItem, ParentName, nil); end; end; end else Parent := nil; Child := FClassHierarchy.ItemOfName(ChildName); if Child = nil then begin if ChildItem <> nil then FClassHierarchy.InsertParented(Parent, ChildItem) else FClassHierarchy.InsertParented(Parent, ChildName); end else begin if Parent <> nil then FClassHierarchy.MoveChildLast(Child, Parent); end; end; var ParentItem: TPasItem; ParentName: string; procedure CioClassHierarchy(const ACio: TPasCio); begin if ACio.MyType in CIONonHierarchy then Exit; { calculate ParentName and ParentItem for current ACIO. } if Assigned(ACio.Ancestors) and (ACio.Ancestors.Count > 0) then begin ParentName := ACio.Ancestors.FirstName; ParentItem := FindGlobalPasItem(ParentName); end else begin ParentName := ''; ParentItem := nil; end; Insert(ParentName, ParentItem, ACio.Name, ACio); end; procedure CiosClassHierarchy(const ACios: TPasNestedCios); var I: Integer; LCio: TPasCio; begin for I := 0 to ACios.Count - 1 do begin LCio := TPasCio(ACios.PasItemAt[I]); CioClassHierarchy(LCio); end; end; var unitLoop: Integer; classLoop: Integer; PU: TPasUnit; LCio: TPasCio; begin FClassHierarchy.Free; FClassHierarchy := TStringCardinalTree.Create; for unitLoop := 0 to Units.Count - 1 do begin PU := Units.UnitAt[unitLoop]; if PU.CIOs = nil then Continue; for classLoop := 0 to PU.CIOs.Count - 1 do begin LCio := TPasCio(PU.CIOs.PasItemAt[classLoop]); CioClassHierarchy(LCio); if LCio.Cios.Count > 0 then CiosClassHierarchy(LCio.Cios); end; end; FClassHierarchy.Sort; end; procedure TDocGenerator.WriteEndOfCode; begin // nothing - for some output this is irrelevant end; procedure TDocGenerator.WriteStartOfCode; begin // nothing - for some output this is irrelevant end; procedure TDocGenerator.WriteDocumentation; begin if OutputGraphVizUses then WriteGVUses; if OutputGraphVizClassHierarchy then WriteGVClasses; end; procedure TDocGenerator.SetLanguage(const Value: TLanguageID); begin FLanguage.Language := Value; end; procedure TDocGenerator.SetDestDir(const Value: string); begin if Value <> '' then begin FDestDir := IncludeTrailingPathDelimiter(Value); end else begin FDestDir := ''; end; end; function TDocGenerator.GetLanguage: TLanguageID; begin Result := FLanguage.Language; end; procedure TDocGenerator.WriteGVClasses; var LNode: TPasItemNode; OverviewFileName: string; begin CreateClassHierarchy; LNode := FClassHierarchy.FirstItem; if Assigned(LNode) then begin OverviewFileName := OverviewFilesInfo[ofGraphVizClasses].BaseFileName + '.dot'; if not CreateStream(OverviewFileName) then Exit; // Note: "digraph" is case sensitive since later versions of GraphViz (at least in >= 2.47.2; ignored case in <= 2.40.1) WriteDirect('digraph Classes {', true); while Assigned(LNode) do begin if Assigned(LNode.Parent) and (LNode.Parent.Name <> '') then begin { Note that LNode.Parent.Name may be a qualified name, like "Classes.TThread", so it really requires quoting with "" for GraphViz. } WriteDirectLine(' "' + LNode.Name + '" -> "' + LNode.Parent.Name + '"'); end; if Assigned(LNode.Item) and (LNode.Item is TPasCio) then begin WriteDirectLine(' "' + LNode.Name + '" [href="' + TPasCio(LNode.Item).OutputFileName + '"]'); end; LNode := FClassHierarchy.NextItem(LNode); end; WriteDirect('}', true); CloseStream; end; end; procedure TDocGenerator.WriteGVUses; var i, j: Integer; U: TPasUnit; OverviewFileName: string; begin if not ObjectVectorIsNilOrEmpty(FUnits) then begin OverviewFileName := OverviewFilesInfo[ofGraphVizUses].BaseFileName + '.dot'; if not CreateStream(OverviewFileName) then Exit; // Note: "digraph" is case sensitive since later versions of GraphViz (at least in >= 2.47.2; ignored case in <= 2.40.1) WriteDirect('digraph Uses {', true); for i := 0 to FUnits.Count-1 do begin if FUnits.PasItemAt[i] is TPasUnit then begin U := TPasUnit(FUnits.PasItemAt[i]); if not IsEmpty(U.UsesUnits) then begin for j := 0 to U.UsesUnits.Count-1 do begin WriteDirectLine(' "' + U.Name + '" -> "' + U.UsesUnits[j] + '"'); end; end; WriteDirectLine(' "' + U.Name + '" [href="' + U.OutputFileName + '"]'); end; end; WriteDirect('}', true); CloseStream; end; end; procedure TDocGenerator.SetAbbreviations(const Value: TStringList); begin FAbbreviations.Assign(Value); end; procedure TDocGenerator.ParseAbbreviationsFile(const AFileName: string); var L: TStringList; i, p: Integer; s, lname, value: string; begin if FileExists(AFileName) then begin L := TStringList.Create; try L.LoadFromFile(AFileName); for i := 0 to L.Count-1 do begin s := Trim(L[i]); if length(s)>0 then begin if s[1] = '[' then begin p := pos(']', s); if p>=0 then begin lname := Trim(copy(s, 2, p-2)); value := Trim(copy(s,p+1,MaxInt)); FAbbreviations.Values[lname] := value; end; end; end; end; finally L.Free; end; end; end; procedure TDocGenerator.CheckString(const AString: string; const AErrors: TObjectVector); var i: Integer; begin if FCheckSpelling and (FAspellProcess <> nil) then begin FAspellProcess.CheckString(AString, AErrors); for i := 0 to AErrors.Count - 1 do DoMessage(2, pmtWarning, 'Word misspelled "%s"', [TSpellingError(AErrors[i]).Word]); end else AErrors.Clear; end; procedure TDocGenerator.EndSpellChecking; begin { If CheckSpelling was false or StartSpellChecking failed then FAspellProcess will be nil, so it's safe to just always call FreeAndNil here. } FreeAndNil(FAspellProcess); end; procedure TDocGenerator.StartSpellChecking(const AMode: string); var WordsToIgnore: TStringList; i: Integer; procedure AddSubItems(Items: TBaseItems); var SubItem: TBaseItem; Index: integer; AName: string; NewName: string; begin for Index := 0 to Items.Count -1 do begin SubItem := Items[Index] as TBaseItem; AName := Trim(SCharsReplace(SubItem.Name, ['0'..'9', '_'], ' ')); if AName = SubItem.Name then begin if (SubItem.Name <> '') then begin WordsToIgnore.Add(SubItem.Name); end; end else begin While AName <> '' do begin NewName := ExtractFirstWord(AName); WordsToIgnore.Add(NewName); end; end; if SubItem is TExternalItem then begin AddSubItems(TExternalItem(SubItem).Anchors); end else if SubItem is TPasEnum then begin AddSubItems(TPasEnum(SubItem).Members); end else if SubItem is TPasCio then begin AddSubItems(TPasCio(SubItem).Fields); AddSubItems(TPasCio(SubItem).Methods); AddSubItems(TPasCio(SubItem).Properties); AddSubItems(TPasCio(SubItem).Fields); end else if SubItem is TPasUnit then begin AddSubItems(TPasUnit(SubItem).CIOs); AddSubItems(TPasUnit(SubItem).Constants); AddSubItems(TPasUnit(SubItem).FuncsProcs); AddSubItems(TPasUnit(SubItem).Types); AddSubItems(TPasUnit(SubItem).Variables); AddSubItems(TPasUnit(SubItem).Types); AddSubItems(TPasUnit(SubItem).Types); end; end; end; begin { Make sure that previous aspell process is closed } FreeAndNil(FAspellProcess); if CheckSpelling then begin try FAspellProcess := TAspellProcess.Create(AMode, FAspellLanguage, OnMessage); except on E: Exception do begin DoMessage(1, pmtWarning, 'Executing aspell failed, ' + 'disabling spell checking: "%s"', [E.Message]); Exit; end; end; WordsToIgnore := TStringList.Create; try WordsToIgnore.Sorted := True; WordsToIgnore.Duplicates := dupIgnore; WordsToIgnore.AddStrings(SpellCheckIgnoreWords); if Introduction <> nil then begin WordsToIgnore.Add(Introduction.Name); AddSubItems(Introduction.Anchors); end; if Conclusion <> nil then begin WordsToIgnore.Add(Conclusion.Name); AddSubItems(Conclusion.Anchors); end; if (AdditionalFiles <> nil) and (AdditionalFiles.Count > 0) then begin for i := 0 to AdditionalFiles.Count - 1 do begin WordsToIgnore.Add(AdditionalFiles.Get(i).Name); AddSubItems(AdditionalFiles.Get(i).Anchors); end; end; AddSubItems(Units); FAspellProcess.SetIgnoreWords(WordsToIgnore); finally WordsToIgnore.Free; end; end; end; procedure TDocGenerator.SetSpellCheckIgnoreWords(Value: TStringList); begin SpellCheckIgnoreWords.Assign(Value); end; function TDocGenerator.FormatPascalCode(const Line: string): string; { Calls FormatKeyWord or FormatNormalCode, depending on whether AString is keyword. } function FormatCode(const AString: string): string; begin if (KeyWordByName(AString) <> KEY_INVALIDKEYWORD) or (StandardDirectiveByName(AString) <> SD_INVALIDSTANDARDDIRECTIVE) then Result := FormatKeyWord(AString) else Result := FormatNormalCode(AString); end; type TCodeType = (ctWhiteSpace, ctString, ctCode, ctEndString, ctChar, ctParenComment, ctBracketComment, ctSlashComment, ctCompilerComment, ctHex, ctEndHex, ctNumeric, ctEndNumeric); var CharIndex: integer; CodeType: TCodeType; CommentBegining: integer; StringBeginning: integer; CodeBeginning: integer; HexBeginning: Integer; NumBeginning: Integer; EndOfCode: boolean; WhiteSpaceBeginning: integer; NumberSubBlock: String; NumberRange: Integer; const Separators = [' ', ',', '(', ')', #9, #10, #13, ';', '[', ']', '{', '}', '''', ':', '<', '>', '=', '+', '-', '*', '/', '@', '.']; LineEnd = [#10, #13]; AlphaNumeric = ['0'..'9', 'a'..'z', 'A'..'Z', '_']; NumericStart = ['0'..'9']; { We add characters eE-+ to allow floating-point values in scientific notation, like 1e10, 9e-4 . This is a simple way to accept numbers. Our PasDoc_Tokenizer has a similar simple approach (scanning number until character outside of "NumberOther = HexadecimalDigits + ['.', '+', '-']") so maybe it is "good enough" solution. } NumericOther = NumericStart + ['.','e','E','-','+']; Hexadec = ['0'..'9', 'a'..'f', 'A'..'F', '$']; function TestCommentStart: boolean; begin result := False; if Line[CharIndex] = '(' then begin if (CharIndex < Length(Line)) and (Line[CharIndex + 1] = '*') then begin CodeType := ctParenComment; result := True; end end else if Line[CharIndex] = '{' then begin if (CharIndex < Length(Line)) and (Line[CharIndex + 1] = '$') then begin CodeType := ctCompilerComment; end else begin CodeType := ctBracketComment; end; result := True; end else if Line[CharIndex] = '/' then begin if (CharIndex < Length(Line)) and (Line[CharIndex + 1] = '/') then begin CodeType := ctSlashComment; result := True; end end; if result then begin CommentBegining := CharIndex; end; end; function TestStringBeginning: boolean; begin result := False; if Line[CharIndex] = '''' then begin if CodeType <> ctChar then begin StringBeginning := CharIndex; end; CodeType := ctString; result := True; end end; begin CommentBegining := 1; StringBeginning := 1; HexBeginning := 1; NumBeginning := 1; result := ''; CodeType := ctWhiteSpace; WhiteSpaceBeginning := 1; CodeBeginning := 1; for CharIndex := 1 to Length(Line) do begin case CodeType of ctWhiteSpace: begin EndOfCode := False; if TestStringBeginning then begin EndOfCode := True; end else if Line[CharIndex] = '#' then begin StringBeginning := CharIndex; CodeType := ctChar; EndOfCode := True; end else if TestCommentStart then begin EndOfCode := True; end else if Line[CharIndex] = '$' Then begin CodeType := ctHex; HexBeginning := CharIndex; EndOfCode := True; end else if IsCharInSet(Line[CharIndex], NumericStart) then begin CodeType := ctNumeric; NumBeginning := CharIndex; EndOfCode := True; end else if IsCharInSet(Line[CharIndex], AlphaNumeric) then begin CodeType := ctCode; CodeBeginning := CharIndex; EndOfCode := True; end; if EndOfCode then begin result := result + ConvertString(Copy(Line, WhiteSpaceBeginning, CharIndex - WhiteSpaceBeginning)); end; end; ctString: begin if Line[CharIndex] = '''' then begin if (CharIndex = Length(Line)) or (Line[CharIndex + 1] <> '''') then begin CodeType := ctEndString; result := result + FormatString(Copy(Line, StringBeginning, CharIndex - StringBeginning + 1)); end; end; end; ctCode: begin EndOfCode := False; if TestStringBeginning then begin EndOfCode := True; end else if Line[CharIndex] = '#' then begin EndOfCode := True; CodeType := ctChar; StringBeginning := CharIndex; end else if TestCommentStart then begin EndOfCode := True; end else if not IsCharInSet(Line[CharIndex], AlphaNumeric) then begin EndOfCode := True; CodeType := ctWhiteSpace; WhiteSpaceBeginning := CharIndex; end; if EndOfCode then begin result := result + FormatCode(Copy(Line, CodeBeginning, CharIndex - CodeBeginning)); end; end; ctEndString: begin if Line[CharIndex] = '#' then begin CodeType := ctChar; end else if TestCommentStart then begin // do nothing end else if Line[CharIndex] = '$' Then Begin CodeType := ctHex; HexBeginning := CharIndex; End else if IsCharInSet(Line[CharIndex], NumericStart) then begin CodeType := ctNumeric; NumBeginning := CharIndex; end else if IsCharInSet(Line[CharIndex], AlphaNumeric) then begin CodeType := ctCode; CodeBeginning := CharIndex; end else begin CodeType := ctWhiteSpace; WhiteSpaceBeginning := CharIndex; end; end; ctChar: begin if Line[CharIndex] = '''' then begin CodeType := ctString; end else if TestCommentStart then begin // do nothing end else if IsCharInSet(Line[CharIndex], Separators) then begin result := result + FormatString(Copy(Line, StringBeginning, CharIndex - StringBeginning)); CodeType := ctWhiteSpace; WhiteSpaceBeginning := CharIndex; end; end; ctParenComment: begin if (Line[CharIndex] = ')') and (CharIndex > 1) and (Line[CharIndex - 1] = '*') then begin result := result + FormatComment(Copy(Line, CommentBegining, CharIndex - CommentBegining + 1)); // behave like whitespace starts right after the comment CodeType := ctWhiteSpace; WhiteSpaceBeginning := CharIndex + 1; end; end; ctBracketComment: begin if Line[CharIndex] = '}' then begin result := result + FormatComment(Copy(Line, CommentBegining, CharIndex - CommentBegining + 1)); // behave like whitespace starts right after the comment CodeType := ctWhiteSpace; WhiteSpaceBeginning := CharIndex + 1; end; end; ctCompilerComment: begin if Line[CharIndex] = '}' then begin result := result + FormatCompilerComment(Copy(Line, CommentBegining, CharIndex - CommentBegining + 1)); // behave like whitespace starts right after the comment CodeType := ctWhiteSpace; WhiteSpaceBeginning := CharIndex + 1; end; end; ctSlashComment: begin if IsCharInSet(Line[CharIndex], LineEnd) then begin CodeType := ctWhiteSpace; result := result + FormatComment(Copy(Line, CommentBegining, CharIndex - CommentBegining)); WhiteSpaceBeginning := CharIndex; end; end; ctHex: Begin If IsCharInSet(Line[CharIndex], Separators) Or Not IsCharInSet(Line[CharIndex], Hexadec) then begin CodeType := ctEndHex; result := result + FormatHex(Copy(Line, HexBeginning, CharIndex - HexBeginning)); result := result + FormatCode(Copy(Line, CharIndex, 1)); end; End; ctNumeric: begin if IsCharInSet(Line[CharIndex], (Separators - NumericOther)) or not IsCharInSet(Line[CharIndex], NumericOther) then begin CodeType := ctEndNumeric; NumberSubBlock := Copy(Line, NumBeginning, CharIndex - NumBeginning); if CharsPos(['.', 'e', 'E'], NumberSubBlock) > 0 then begin // floating-point value in decimal 123.456 notation NumberRange := Pos('..', NumberSubBlock); if NumberRange > 0 Then begin result := result + FormatNumeric( Copy(NumberSubBlock, 1, NumberRange - 1)); result := result + FormatCode( Copy(NumberSubBlock, NumberRange, Length(NumberSubBlock))); end else result := result + FormatFloat(NumberSubBlock); end else begin result := result + FormatNumeric(NumberSubBlock); end; result := result + FormatCode(Copy(Line, CharIndex, 1)); end; end; ctEndHex: begin if Line[CharIndex] = '#' then begin CodeType := ctChar; end else if TestCommentStart then begin // do nothing end else if IsCharInSet(Line[CharIndex], NumericOther) then begin CodeType := ctNumeric; NumBeginning := CharIndex; end else if IsCharInSet(Line[CharIndex], AlphaNumeric) then begin CodeType := ctCode; CodeBeginning := CharIndex; end else begin CodeType := ctWhiteSpace; WhiteSpaceBeginning := CharIndex; end; end; ctEndNumeric: begin if Line[CharIndex] = '#' then begin CodeType := ctChar; end else if TestCommentStart then begin // do nothing end else if Line[CharIndex] = '$' Then Begin CodeType := ctHex; HexBeginning := CharIndex; End else if IsCharInSet(Line[CharIndex], AlphaNumeric) then begin CodeType := ctCode; CodeBeginning := CharIndex; end else begin CodeType := ctWhiteSpace; WhiteSpaceBeginning := CharIndex; end; end; else Assert(False); end; end; CharIndex := Length(Line) + 1; case CodeType of ctWhiteSpace: begin result := result + (Copy(Line, WhiteSpaceBeginning, CharIndex - WhiteSpaceBeginning)); end; ctString: begin end; ctCode: begin result := result + FormatCode(Copy(Line, CodeBeginning, CharIndex - CodeBeginning)); end; ctEndString: begin end; ctChar: begin result := result + FormatString(Copy(Line, StringBeginning, CharIndex - StringBeginning)); end; ctParenComment, ctSlashComment, ctBracketComment: begin { add an unterminated comment at the end } result := result + FormatComment(Copy(Line, CommentBegining, CharIndex - CommentBegining)); end; ctCompilerComment: begin result := result + FormatCompilerComment(Copy(Line, CommentBegining, CharIndex - CommentBegining)); end; ctHex: begin end; ctEndHex: begin end; ctNumeric: begin end; ctEndNumeric: begin end; else Assert(False); end; end; function TDocGenerator.FormatNormalCode(AString: string): string; begin Result := ConvertString(AString); end; function TDocGenerator.FormatComment(AString: string): string; begin result := ConvertString(AString); end; function TDocGenerator.FormatHex(AString: string): string; begin result := ConvertString(AString); end; function TDocGenerator.FormatNumeric(AString: string): string; begin result := ConvertString(AString); end; function TDocGenerator.FormatFloat(AString: string): string; begin result := ConvertString(AString); end; function TDocGenerator.FormatCompilerComment(AString: string): string; begin result := ConvertString(AString); end; function TDocGenerator.FormatKeyWord(AString: string): string; begin result := ConvertString(AString); end; function TDocGenerator.FormatString(AString: string): string; begin result := ConvertString(AString); end; function TDocGenerator.Paragraph: string; begin Result := ' '; end; function TDocGenerator.ShortDash: string; begin Result := '-'; end; function TDocGenerator.EnDash: string; begin Result := '--'; end; function TDocGenerator.EmDash: string; begin Result := '---'; end; function TDocGenerator.HtmlString(const S: string): string; begin Result := ''; end; function TDocGenerator.LatexString(const S: string): string; begin Result := ''; end; function TDocGenerator.LineBreak: string; begin Result := ''; end; function TDocGenerator.URLLink(const URL: string): string; begin Result := ConvertString(URL); end; function TDocGenerator.URLLink(const URL, LinkDisplay: string): string; var Link: String; begin Link := FixEmailaddressWithoutMailTo(URL); if LinkDisplay <> '' then Result := Format('%s (%s)', [ConvertString(Link), ConvertString(LinkDisplay)]) else Result := URLLink(Link); end; function TDocGenerator.FormatBold(const Text: string): string; begin Result := Text; end; function TDocGenerator.FormatItalic(const Text: string): string; begin Result := Text; end; function TDocGenerator.FormatWarning(const Text: string): string; begin Result := FormatBold(Text); end; function TDocGenerator.FormatNote(const Text: string): string; begin Result := FormatItalic(Text); end; function TDocGenerator.FormatPreformatted(const Text: string): string; begin Result := ConvertString(Text); end; function TDocGenerator.FormatTableOfContents(Sections: TStringPairVector): string; begin Result := ''; end; function TDocGenerator.MakeItemLink(const Item: TBaseItem; const LinkCaption: string; const LinkContext: TLinkContext): string; begin Result := ConvertString(LinkCaption); end; procedure TDocGenerator.WriteCodeWithLinksCommon(const Item: TPasItem; const Code: string; WriteItemLink: boolean; const NameLinkBegin, NameLinkEnd: string); var NameFound, SearchForLink: Boolean; FoundItem: TBaseItem; i, j, l: Integer; s: string; pl: TStandardDirective; { ncstart marks what part of Code was already written: Code[1..ncstart - 1] is already written to output stream. } ncstart: Integer; begin WriteStartOfCode; i := 1; NameFound := false; SearchForLink := False; l := Length(Code); ncstart := i; while i <= l do begin case Code[i] of '_', 'A'..'Z', 'a'..'z': begin WriteConverted(Copy(Code, ncstart, i - ncstart)); { assemble item } j := i; repeat Inc(i); until (i > l) or (not IsCharInSet(Code[i], ['.', '_', '0'..'9', 'A'..'Z', 'a'..'z'])); s := Copy(Code, j, i - j); if not NameFound and (s = Item.Name) then begin WriteDirect(NameLinkBegin); if WriteItemLink then WriteDirect(MakeItemLink(Item, s, lcCode)) else WriteConverted(s); WriteDirect(NameLinkEnd); NameFound := True; end else begin { Special processing for standard directives. Note that we check whether S is standard directive *after* we checked whether S matches P.Name, otherwise we would mistakenly think that 'register' is a standard directive in Code 'procedure Register;' This shouldn't cause another problem (accidentally making standard directive a link, e.g. in code like 'procedure Foo; register' or even 'procedure Register; register;' ) because we safeguard against it using NameFound and SearchForLink state variables. That said, WriteCodeWithLinksCommon still remains a hackish excuse to not cooperate better with PasDoc_Parser when generating FullDeclaration of every item. } pl := StandardDirectiveByName(s); case pl of SD_ABSTRACT, SD_ASSEMBLER, SD_CDECL, SD_DYNAMIC, SD_EXPORT, SD_FAR, SD_FORWARD, SD_NAME, SD_NEAR, SD_OVERLOAD, SD_OVERRIDE, SD_PASCAL, SD_REGISTER, SD_SAFECALL, SD_STATIC, SD_STDCALL, SD_REINTRODUCE, SD_VIRTUAL: begin WriteConverted(s); SearchForLink := False; end; SD_EXTERNAL: begin WriteConverted(s); SearchForLink := true; end; else begin if SearchForLink then FoundItem := SearchItem(S, Item, false) else FoundItem := nil; if Assigned(FoundItem) then WriteDirect(MakeItemLink(FoundItem, s, lcCode)) else WriteConverted(s); end; end; end; ncstart := i; end; ':', '=': begin SearchForLink := True; Inc(i); end; ';': begin SearchForLink := False; Inc(i); end; '''': begin { No need to worry here about the fact that 'foo''bar' is actually one string, "foo'bar". We will parse it in this procedure as two strings, 'foo', then 'bar' (missing the fact that ' is a part of string), but this doesn't harm us (as we don't need here the value of parsed string). } repeat Inc(i); until (i > l) or (Code[i] = ''''); Inc(i); end; else Inc(i); end; end; WriteConverted(Copy(Code, ncstart, i - ncstart)); WriteEndOfCode; end; procedure TDocGenerator.WriteExternal( const ExternalItem: TExternalItem; const Id: TTranslationID); begin if not Assigned(ExternalItem) then begin Exit; end; DoMessage(2, pmtInformation, 'Writing Docs for %s, "%s"', [FLanguage.Translation[Id], ExternalItem.Name]); If ExternalItem.Title = '' then begin ExternalItem.Title := FLanguage.Translation[Id]; end; If ExternalItem.ShortTitle = '' then begin ExternalItem.ShortTitle := ExternalItem.Title; end; WriteExternalCore(ExternalItem, Id); end; procedure TDocGenerator.WriteIntroduction; begin WriteExternal(Introduction, trIntroduction); end; procedure TDocGenerator.WriteConclusion; begin WriteExternal(Conclusion, trConclusion); end; procedure TDocGenerator.WriteAdditionalFiles; var i: Integer; begin for i := 0 to AdditionalFiles.Count - 1 do begin WriteExternal(AdditionalFiles.Get(i), trAdditionalFile); end; end; procedure TDocGenerator.PreHandleAnchorTag( ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); var AnchorName: string; AnchorItem: TAnchorItem; begin { We add AnchorName to FCurrentItem.Anchors in the 1st pass of expanding descriptions (i.e. in PreHandleAnchorTag instead of HandleAnchorTag), this way creating @links in the 2nd pass of expanding descriptions works good. } ReplaceStr := ''; AnchorName := Trim(TagParameter); if not IsValidIdent(AnchorName) then begin ThisTag.TagManager.DoMessage(1, pmtWarning, 'Invalid anchor name: "%s"', [AnchorName]); Exit; end; AnchorItem := (FCurrentItem as TExternalItem).AddAnchor(AnchorName); AnchorItem.FullLink := CreateLink(AnchorItem); end; procedure TDocGenerator.HandleAnchorTag( ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); var AnchorName: string; begin { AnchorName is already added to FCurrentItem.Anchors, thanks to PreHandleAnchorTag. All we do here is to generate correct ReplaceStr. } AnchorName := Trim(TagParameter); if not IsValidIdent(AnchorName) then { Warning for this case was already printed by PreHandleAnchorTag. That's why here we do only Exit. } Exit; ReplaceStr := FormatAnchor(AnchorName); end; function TDocGenerator.SplitSectionTagParameters( ThisTag: TTag; const TagParameter: string; DoMessages: boolean; out HeadingLevel: integer; out AnchorName: string; out Caption: string): boolean; var HeadingLevelString: string; Remainder: string; begin Result := false; ExtractFirstWord(TagParameter, HeadingLevelString, Remainder); ExtractFirstWord(Remainder, AnchorName, Caption); try HeadingLevel := StrToInt(HeadingLevelString); except on E: EConvertError do begin if DoMessages then ThisTag.TagManager.DoMessage(1, pmtWarning, 'Invalid heading level in @section tag: "%s". %s', [HeadingLevelString, E.Message]); Exit; end; end; if HeadingLevel < 1 then begin if DoMessages then ThisTag.TagManager.DoMessage(1, pmtWarning, 'Invalid heading level in @section tag: %d. Heading level must be >= 1', [HeadingLevel]); Exit; end; Result := true; end; procedure TDocGenerator.PreHandleSectionTag( ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); var AnchorName: string; Caption: string; AnchorItem: TAnchorItem; HeadingLevel: integer; begin { We add AnchorName to FCurrentItem.Anchors in the 1st pass of expanding descriptions (i.e. in PreHandleSectionTag instead of HandleSectionTag), this way creating @links in the 2nd pass of expanding descriptions works good. Also, we can handle @tableOfContents in the 2nd pass. } ReplaceStr := ''; if SplitSectionTagParameters(ThisTag, TagParameter, false, HeadingLevel, AnchorName, Caption) then begin AnchorItem := (FCurrentItem as TExternalItem).AddAnchor(AnchorName); AnchorItem.FullLink := CreateLink(AnchorItem); AnchorItem.SectionLevel := HeadingLevel; AnchorItem.SectionCaption := Caption; end; end; procedure TDocGenerator.HandleSectionTag( ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); var AnchorName: string; Caption: string; HeadingLevel: integer; begin if SplitSectionTagParameters(ThisTag, TagParameter, true, HeadingLevel, AnchorName, Caption) then begin ReplaceStr := FormatSection(HeadingLevel, AnchorName, Caption); end; { Section is already added to FCurrentItem.Anchors, thanks to PreHandleSectionTag. } end; procedure TDocGenerator.TagAllowedInsideLists( ThisTag: TTag; EnclosingTag: TTag; var Allowed: boolean); begin Allowed := (EnclosingTag = OrderedListTag) or (EnclosingTag = UnorderedListTag) or (EnclosingTag = DefinitionListTag); end; procedure TDocGenerator.ItemLabelTagAllowedInside( ThisTag: TTag; EnclosingTag: TTag; var Allowed: boolean); begin Allowed := EnclosingTag = DefinitionListTag; end; procedure TDocGenerator.TagAllowedInsideTable( ThisTag: TTag; EnclosingTag: TTag; var Allowed: boolean); begin Allowed := EnclosingTag = TableTag; end; procedure TDocGenerator.TagAllowedInsideRows( ThisTag: TTag; EnclosingTag: TTag; var Allowed: boolean); begin Allowed := (EnclosingTag = RowTag) or (EnclosingTag = RowHeadTag); end; procedure TDocGenerator.HandleImageTag( ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); var I: Integer; FileNames: TStringList; begin FileNames := TStringList.Create; try FileNames.Text := TagParameter; { Trim, remove empty lines, and expand paths on FileNames } I := 0; while I < FileNames.Count do begin FileNames[I] := Trim(FileNames[I]); if FileNames[I] = '' then FileNames.Delete(I) else begin FileNames[I] := CombinePaths(FCurrentItem.BasePath, FileNames[I]); Inc(I); end; end; if FileNames.Count = 0 then begin ThisTag.TagManager.DoMessage(1, pmtWarning, 'No parameters for @image tag', []); end else ReplaceStr := FormatImage(FileNames); finally FileNames.Free end; end; function TDocGenerator.FormatImage(FileNames: TStringList): string; begin // Result := FileNames[0]; { Show relative path, since absolute path is - unportable (doesn't make sense on other systems than current) - makes our tests output not reproducible. } Result := ExtractRelativePath(FCurrentItem.BasePath, FileNames[0]); end; procedure TDocGenerator.HandleIncludeTag( ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); var IncludedText: string; begin IncludedText := FileToString(CombinePaths(FCurrentItem.BasePath, Trim(TagParameter))); ReplaceStr := ThisTag.TagManager.Execute(IncludedText, { Note that this means that we reset auto-linking state inside the include file to what was chosen by --auto-link command-line option. I.e., @noAutoLink(@include(file.txt)) does NOT turn auto-linking off inside file.txt. } AutoLink); end; procedure TDocGenerator.HandleIncludeCodeTag( ThisTag: TTag; var ThisTagData: TObject; EnclosingTag: TTag; var EnclosingTagData: TObject; const TagParameter: string; var ReplaceStr: string); var I: Integer; FileName: string; FileNames: TStringList; begin FileNames := TStringList.Create; try FileNames.Text := TagParameter; ReplaceStr := ''; for I := 0 to Pred(FileNames.Count) do begin FileName := Trim(FileNames[I]); if Length(FileName) > 0 then begin FileName := CombinePaths(FCurrentItem.BasePath, FileName); ReplaceStr := ReplaceStr + FormatPascalCode(FileToString(FileName)); end; end; if ReplaceStr = '' then ThisTag.TagManager.DoMessage(1, pmtWarning, 'No parameters for @includeCode tag', []); finally FileNames.Free end; end; procedure TDocGenerator.SetExternalClassHierarchy(const Value: TStrings); begin FExternalClassHierarchy.Assign(Value); end; function TDocGenerator.StoredExternalClassHierarchy: boolean; begin Result := Trim(FExternalClassHierarchy.Text) <> Trim(DefaultExternalClassHierarchy); end; end.