{
  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);

  { 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;

    { @abstract(Makes a String look like a coded String,
      i.e. <CODE>TheString</CODE> in Html.)
      @param(s is the string to format)
      @returns(the formatted string) }
    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: boolean;
      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: boolean): 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://github.com/pasdoc/pasdoc/wiki/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://github.com/pasdoc/pasdoc/wiki/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://github.com/pasdoc/pasdoc/wiki/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), 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 ACio.Cios.Count > 0 then
        CiosAssignLinks(ACio.Cios);
    end;
  end;

  { Assign MyXxx properties (MyUnit, MyObject, MyEnum) and FullLink
    for all members of the enumerated types on this list. }
  procedure EnumsAssignLinks(ATypes: TPasTypes);
  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, nil, Enum, Enum.Members);
      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);

    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.Name;
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
    if Assigned(TPasItem(FCurrentItem).MyObject) then
    begin
      InheritedItem := TPasItem(FCurrentItem).MyObject.FindItemInAncestors(
        FCurrentItem.Name);
      if InheritedItem = nil then
        InheritedCannotResolve(Format('Member "%s" not found in ancestors',
          [FCurrentItem.Name])) else
        ReplaceStr := MakeItemLink(InheritedItem,
          InheritedItem.MyObject.Name + '.' + InheritedItem.Name, lcNormal);
    end else
      InheritedCannotResolve('This item is not a member of a class/interface/etc.');
  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, true);
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];
  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: boolean;
  out FoundItem: TBaseItem): string;
var
  NameParts: TNameParts;
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);

          if Length(NameParts) = 3 then
          begin
            SetLength(NameParts, 2);
            FoundItem := FindGlobal(NameParts);
            Result := MakeItemLink(FoundItem, FoundItem.Name, lcNormal) +
              '.' + Result;
          end;

          if Length(NameParts) = 2 then
          begin
            SetLength(NameParts, 1);
            FoundItem := FindGlobal(NameParts);
            Result := MakeItemLink(FoundItem, FoundItem.Name, lcNormal) +
              '.' + Result;
          end;
        end;
      else Assert(false, 'LinkLook = ??');
    end;
  end else
  if WarningIfLinkNotFound then
  begin
    DoMessage(1, pmtWarning, 'Could not resolve link "%s" (from description of "%s")',
      [S, Item.QualifiedName]);
    Result := CodeString(ConvertString(S));
  end else
    Result := '';
end;

function TDocGenerator.SearchLink(s: string; const Item: TBaseItem;
  const LinkDisplay: string;
  const WarningIfLinkNotFound: boolean): 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;

    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;

    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', '_'];
  Numeric = ['0'..'9','.'];
  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], Numeric) 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], Numeric) 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 - ['.'])) Or
              Not IsCharInSet(Line[CharIndex], Numeric) then
          begin
            CodeType := ctEndNumeric;
            If Pos('.', Copy(Line, NumBeginning, CharIndex - NumBeginning)) > 0 Then
            Begin
              NumberSubBlock := Copy(Line, NumBeginning, CharIndex - NumBeginning);
              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
              result := result + FormatNumeric(Copy(Line, NumBeginning,
                        CharIndex - NumBeginning));
            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], Numeric) 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.
