From 79d78bd4d8678816e2f5de815a99ff9a935299d3 Mon Sep 17 00:00:00 2001 From: Iwan Kelaiah Date: Sat, 19 Sep 2026 09:09:34 +1000 Subject: [PATCH 1/6] refactor(fs): add a shared filesystem helper unit Move path normalization, whole-file reads, Pascal source detection, readability probing, and symlink checks into the leaf unit PasWeave.FS. Parser, discovery, Lazarus, output, and symbol conversion now share one definition of each helper instead of carrying local copies. Directory walkers stay specialized because their filtering and pruning semantics differ. No behavior changes. Verified: make test (CLI and functional suites) and byte-identical checked-in example output. --- CHANGELOG.md | 5 ++ Makefile | 2 +- README.md | 1 + scripts/build-portable-windows.ps1 | 2 +- src/incremental/PasWeave.Incremental.pas | 22 +----- src/incremental/PasWeave.Output.pas | 22 +----- src/parser/PasWeave.Compiler.pas | 12 +-- src/parser/PasWeave.FPCAdapter.Symbols.pas | 7 +- src/parser/PasWeave.FPCAdapter.pas | 2 +- src/parser/PasWeave.Lazarus.Support.pas | 29 +------- src/parser/PasWeave.Lazarus.pas | 3 +- src/parser/PasWeave.Parser.pas | 23 +----- src/support/PasWeave.FS.pas | 86 ++++++++++++++++++++++ 13 files changed, 111 insertions(+), 105 deletions(-) create mode 100644 src/support/PasWeave.FS.pas diff --git a/CHANGELOG.md b/CHANGELOG.md index f6b74af..2e184ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,11 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht - Split the CLI build pipeline into named `ParseCommandLine`, `ExecuteBuild`, and `RenderAndReport` phases, leaving `RunBuild` as a short orchestrator with one owner for option objects and build state. +- Added a leaf `PasWeave.FS` unit for shared path and file helpers; discovery, + Lazarus, and output code now share one definition of path normalization, + source-file detection, readability, symlink checks, and whole-file reads. + Directory walkers remain specialized where their pruning and filtering + semantics differ. ## [0.7.0] - 2026-09-17 diff --git a/Makefile b/Makefile index 6790904..1ad2b67 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ FPC ?= fpc -UNIT_PATHS = -Fusrc/cli -Fusrc/diagnostics -Fusrc/incremental -Fusrc/model -Fusrc/parser -Fusrc/render -Fusrc/validation +UNIT_PATHS = -Fusrc/cli -Fusrc/diagnostics -Fusrc/incremental -Fusrc/model -Fusrc/parser -Fusrc/render -Fusrc/support -Fusrc/validation FPC_FLAGS = -Mobjfpc -Sh $(UNIT_PATHS) -FUbuild/units ifeq ($(OS),Windows_NT) diff --git a/README.md b/README.md index 91474bb..54c127b 100644 --- a/README.md +++ b/README.md @@ -278,6 +278,7 @@ src/validation/ Authoring diagnostics and coverage gates src/render/ HTML, Markdown, links, shared helpers, offline assets src/diagnostics/ Stable diagnostic codes and severities src/incremental/ Fingerprints, manifest.json, atomic writes +src/support/ Shared filesystem helpers (PasWeave.FS) tests/ Fixtures, shared assertions, CLI + focused regression suites examples/ Minimal documented-api first, rich scientific-api second ``` diff --git a/scripts/build-portable-windows.ps1 b/scripts/build-portable-windows.ps1 index 1498bc6..c4a7d9e 100644 --- a/scripts/build-portable-windows.ps1 +++ b/scripts/build-portable-windows.ps1 @@ -67,7 +67,7 @@ try { $unitPaths = @( '-Fusrc/cli', '-Fusrc/diagnostics', '-Fusrc/incremental', '-Fusrc/model', - '-Fusrc/parser', '-Fusrc/render', '-Fusrc/validation' + '-Fusrc/parser', '-Fusrc/render', '-Fusrc/support', '-Fusrc/validation' ) $testCompilerArguments = @( '-Twin64', '-Px86_64', '-B', '-O2', '-Mobjfpc', '-Sh' diff --git a/src/incremental/PasWeave.Incremental.pas b/src/incremental/PasWeave.Incremental.pas index 6e7ef1d..1abafb2 100644 --- a/src/incremental/PasWeave.Incremental.pas +++ b/src/incremental/PasWeave.Incremental.pas @@ -46,28 +46,12 @@ function PeakHeapBytes: QWord; implementation uses - PasWeave.Hashing, PasWeave.Version, PasWeave.Output; + PasWeave.FS, PasWeave.Hashing, PasWeave.Version; var GPeakHeap: QWord; -function IsPascalLikeFilename(const AFilename: string): Boolean; -var - Extension: string; -begin - Extension := LowerCase(ExtractFileExt(AFilename)); - Result := (Extension = '.pas') or (Extension = '.pp') or - (Extension = '.inc') or (Extension = '.lpi') or (Extension = '.lpk'); -end; -function IsSymbolicLink(AAttributes: LongInt): Boolean; -begin - {$IFDEF UNIX} - Result := (AAttributes and faSymLink) <> 0; - {$ELSE} - Result := False; - {$ENDIF} -end; procedure CollectDirectoryPascalFiles(const ARootDirectory, ARelativeDirectory: string; ARecursive: Boolean; @@ -105,7 +89,7 @@ procedure CollectDirectoryPascalFiles(const ARootDirectory, CollectDirectoryPascalFiles(ARootDirectory, RelativePath, ARecursive, ADiscovery, AFiles); end - else if IsPascalLikeFilename(Search.Name) then + else if HasBuildInputExtension(Search.Name) then begin if Assigned(ADiscovery) and (ADiscovery.IsExcluded(RelativePath) or @@ -137,7 +121,7 @@ procedure CollectDirectoryListingFiles(const ADirectory: string; FullPath := IncludeTrailingPathDelimiter(ADirectory) + Search.Name; if (Search.Attr and faDirectory) = 0 then begin - if IsPascalLikeFilename(Search.Name) then + if HasBuildInputExtension(Search.Name) then AFiles.Add(NormalisePath(ExpandFileName(FullPath))); end; until FindNext(Search) <> 0; diff --git a/src/incremental/PasWeave.Output.pas b/src/incremental/PasWeave.Output.pas index 9888928..e196f4b 100644 --- a/src/incremental/PasWeave.Output.pas +++ b/src/incremental/PasWeave.Output.pas @@ -11,8 +11,7 @@ interface uses Classes; -/// Normalises path separators to `/` for deterministic comparisons. -function NormalisePath(const APath: string): string; + const /// Expected `schemaVersion` inside `manifest.json`. @@ -130,7 +129,7 @@ procedure RemoveStaleOutputs(const AOutputDirectory: string; implementation uses - SysUtils, FPJSON, JSONParser, PasWeave.Hashing; + SysUtils, FPJSON, JSONParser, PasWeave.FS, PasWeave.Hashing; var GLedger: TStringList; @@ -148,10 +147,6 @@ function MoveFileExA(lpExistingFileName, lpNewFileName: PAnsiChar; dwFlags: DWORD): LongBool; stdcall; external 'kernel32' name 'MoveFileExA'; {$ENDIF} -function NormalisePath(const APath: string): string; -begin - Result := StringReplace(APath, '\', '/', [rfReplaceAll]); -end; /// Returns True when a manifest-relative path is safe to resolve inside the /// output directory (no absolute paths, drive letters, or parent traversal). @@ -232,19 +227,6 @@ function ManifestEntryFromJSON(AObject: TJSONObject): TManifestEntry; Result.Size := AObject.Get('size', Int64(0)); end; -function ReadFileToString(const AFilename: string): string; -var - Stream: TFileStream; -begin - Stream := TFileStream.Create(AFilename, fmOpenRead or fmShareDenyWrite); - try - SetLength(Result, Stream.Size); - if Stream.Size > 0 then - Stream.ReadBuffer(Result[1], Stream.Size); - finally - Stream.Free; - end; -end; function ReadManifest(const AOutputDirectory: string): TManifest; var diff --git a/src/parser/PasWeave.Compiler.pas b/src/parser/PasWeave.Compiler.pas index c7f7db6..40373a8 100644 --- a/src/parser/PasWeave.Compiler.pas +++ b/src/parser/PasWeave.Compiler.pas @@ -5,7 +5,7 @@ interface uses - Classes, SysUtils; + Classes, SysUtils, PasWeave.FS; type ECompilerConfigurationError = class(Exception); @@ -128,15 +128,7 @@ function TryNormaliseTargetCPU(const AValue: string; ANormalised := ''; end; -function DirectoryIsReadable(const APath: string): Boolean; -var - Search: TSearchRec; -begin - Result := FindFirst(IncludeTrailingPathDelimiter(APath) + '*', - faAnyFile, Search) = 0; - if Result then - FindClose(Search); -end; + function NormaliseDirectoryPath(const AValue, AOptionName: string): string; begin diff --git a/src/parser/PasWeave.FPCAdapter.Symbols.pas b/src/parser/PasWeave.FPCAdapter.Symbols.pas index 5212ce4..ddd8dbb 100644 --- a/src/parser/PasWeave.FPCAdapter.Symbols.pas +++ b/src/parser/PasWeave.FPCAdapter.Symbols.pas @@ -30,7 +30,6 @@ TSourceTextCache = class function TextFor(const AFileName: string): string; end; -function NormalisePath(const APath: string): string; function RelativeSourceFilename(const AFileName, ASourceRoot: string): string; function ReadSourceText(const AFileName: string): string; function ConvertModule(AModule: TPasModule; @@ -41,7 +40,7 @@ function ConvertModule(AModule: TPasModule; implementation uses - SysUtils; + SysUtils, PasWeave.FS; constructor TElementSourceInfo.Create(AColumn: Integer); begin @@ -50,10 +49,6 @@ constructor TElementSourceInfo.Create(AColumn: Integer); end; -function NormalisePath(const APath: string): string; -begin - Result := StringReplace(APath, '\', '/', [rfReplaceAll]); -end; function RelativeSourceFilename(const AFileName, ASourceRoot: string): string; var diff --git a/src/parser/PasWeave.FPCAdapter.pas b/src/parser/PasWeave.FPCAdapter.pas index 38994db..7fd7998 100644 --- a/src/parser/PasWeave.FPCAdapter.pas +++ b/src/parser/PasWeave.FPCAdapter.pas @@ -16,7 +16,7 @@ implementation uses Classes, Contnrs, SysUtils, PParser, PScanner, PasTree, - PasWeave.FPCAdapter.Symbols; + PasWeave.FPCAdapter.Symbols, PasWeave.FS; type diff --git a/src/parser/PasWeave.Lazarus.Support.pas b/src/parser/PasWeave.Lazarus.Support.pas index 8fb942a..227ad62 100644 --- a/src/parser/PasWeave.Lazarus.Support.pas +++ b/src/parser/PasWeave.Lazarus.Support.pas @@ -12,11 +12,11 @@ interface type ELazarusConfigurationError = class(Exception); -function NormalisePath(const APath: string): string; + function ChildNode(AParent: TDOMNode; const AName: string): TDOMNode; function AttributeValue(ANode: TDOMNode; const AName: string): string; function NodeValue(ANode: TDOMNode; const AName: string): string; -function IsPascalSourceFile(const AFilename: string): Boolean; + function IsTrueValue(const AValue: string): Boolean; function DefaultName(const AFilename: string): string; function ResolvePath(const ABaseDirectory, AValue: string): string; @@ -24,7 +24,7 @@ function ExpandConfiguredValue(const AValue, ABaseDirectory: string; ACompilerOptions: TCompilerOptions; out AGenerated: Boolean): string; function ResolvePackageReference(const ABaseDirectory, AValue: string; ACompilerOptions: TCompilerOptions): string; -function DirectoryIsReadable(const APath: string): Boolean; + function NormalisePackagePath(const AValue: string): string; procedure AddConfiguredPath(const AValue, ABaseDirectory: string; ACompilerOptions: TCompilerOptions; AIncludePath: Boolean); @@ -38,12 +38,7 @@ procedure ParseCompilerOptions(ANode: TDOMNode; const ABaseDirectory: string; implementation uses - Contnrs, XMLRead; - -function NormalisePath(const APath: string): string; -begin - Result := StringReplace(APath, '\', '/', [rfReplaceAll]); -end; + Contnrs, XMLRead, PasWeave.FS; function ChildNode(AParent: TDOMNode; const AName: string): TDOMNode; var @@ -78,12 +73,6 @@ function NodeValue(ANode: TDOMNode; const AName: string): string; Result := AttributeValue(ChildNode(ANode, AName), 'Value'); end; -function IsPascalSourceFile(const AFilename: string): Boolean; -begin - Result := SameText(ExtractFileExt(AFilename), '.pas') or - SameText(ExtractFileExt(AFilename), '.pp'); -end; - function IsTrueValue(const AValue: string): Boolean; begin Result := SameText(Trim(AValue), 'true') or (Trim(AValue) = '1'); @@ -127,16 +116,6 @@ function ResolvePackageReference(const ABaseDirectory, AValue: string; Result := ResolvePath(ABaseDirectory, Expanded); end; -function DirectoryIsReadable(const APath: string): Boolean; -var - Search: TSearchRec; -begin - Result := FindFirst(IncludeTrailingPathDelimiter(APath) + '*', - faAnyFile, Search) = 0; - if Result then - FindClose(Search); -end; - function NormalisePackagePath(const AValue: string): string; begin Result := Trim(AValue); diff --git a/src/parser/PasWeave.Lazarus.pas b/src/parser/PasWeave.Lazarus.pas index c41e115..8e3ff7f 100644 --- a/src/parser/PasWeave.Lazarus.pas +++ b/src/parser/PasWeave.Lazarus.pas @@ -5,7 +5,8 @@ interface uses - Classes, SysUtils, PasWeave.Compiler, PasWeave.Lazarus.Support; + Classes, SysUtils, PasWeave.Compiler, PasWeave.FS, + PasWeave.Lazarus.Support; type diff --git a/src/parser/PasWeave.Parser.pas b/src/parser/PasWeave.Parser.pas index dcfca96..962e406 100644 --- a/src/parser/PasWeave.Parser.pas +++ b/src/parser/PasWeave.Parser.pas @@ -51,12 +51,8 @@ function BuildProjectFromFiles(const ASourceRoot, AProjectName: string; implementation uses - PasWeave.Diagnostics, PasWeave.FPCAdapter, PasWeave.Validation; + PasWeave.Diagnostics, PasWeave.FPCAdapter, PasWeave.FS, PasWeave.Validation; -function NormalisePath(const APath: string): string; -begin - Result := StringReplace(APath, '\', '/', [rfReplaceAll]); -end; procedure SplitPath(const APath: string; AParts: TStrings); var @@ -262,18 +258,11 @@ function TSourceDiscoveryOptions.IsIncluded(const ARelativePath: string): Boolea MatchesAnyDiscoveryPattern(FIncludePatterns, ARelativePath); end; -function IsPascalUnitFilename(const AFilename: string): Boolean; -var - Extension: string; -begin - Extension := ExtractFileExt(AFilename); - Result := SameText(Extension, '.pas') or SameText(Extension, '.pp'); -end; function ShouldIncludeDiscoveredFile(const ARelativePath: string; AOptions: TSourceDiscoveryOptions): Boolean; begin - Result := IsPascalUnitFilename(ARelativePath); + Result := IsPascalSourceFile(ARelativePath); if not Result or not Assigned(AOptions) then Exit; if MatchesAnyDiscoveryPattern(AOptions.FExcludePatterns, @@ -290,14 +279,6 @@ function ShouldSkipDiscoveredDirectory(const ARelativePath: string; MatchesAnyDiscoveryPattern(AOptions.FExcludePatterns, ARelativePath); end; -function IsSymbolicLink(AAttributes: LongInt): Boolean; -begin - {$IFDEF UNIX} - Result := (AAttributes and faSymLink) <> 0; - {$ELSE} - Result := False; - {$ENDIF} -end; procedure DiscoverDirectoryFiles(const ARootDirectory, ARelativeDirectory: string; AOptions: TSourceDiscoveryOptions; diff --git a/src/support/PasWeave.FS.pas b/src/support/PasWeave.FS.pas new file mode 100644 index 0000000..ee26309 --- /dev/null +++ b/src/support/PasWeave.FS.pas @@ -0,0 +1,86 @@ +/// Shared filesystem helpers for source discovery and incremental builds. +/// +/// This is a leaf unit: it depends only on the RTL so parsers, discovery, and +/// output code can share one definition of path and file handling. +unit PasWeave.FS; + +{$mode objfpc}{$H+} + +interface + +uses + Classes; + +/// Converts Windows separators to `/` for stable comparisons. +function NormalisePath(const APath: string): string; +/// Reads a whole file as a byte string, preserving its encoding. +function ReadFileToString(const AFilename: string): string; +/// True for `.pas` and `.pp` source files. +function IsPascalSourceFile(const AFilename: string): Boolean; +/// True for every extension that can contribute to a build fingerprint +/// (`.pas`, `.pp`, `.inc`, `.lpi`, `.lpk`). +function HasBuildInputExtension(const AFilename: string): Boolean; +/// True when the directory can be opened for listing. +function DirectoryIsReadable(const APath: string): Boolean; +/// True when search attributes mark a symbolic link. +function IsSymbolicLink(AAttributes: LongInt): Boolean; + +implementation + +uses + SysUtils; + +function NormalisePath(const APath: string): string; +begin + Result := StringReplace(APath, '\', '/', [rfReplaceAll]); +end; + +function ReadFileToString(const AFilename: string): string; +var + Stream: TFileStream; +begin + Stream := TFileStream.Create(AFilename, fmOpenRead or fmShareDenyWrite); + try + SetLength(Result, Stream.Size); + if Stream.Size > 0 then + Stream.ReadBuffer(Result[1], Stream.Size); + finally + Stream.Free; + end; +end; + +function IsPascalSourceFile(const AFilename: string): Boolean; +begin + Result := SameText(ExtractFileExt(AFilename), '.pas') or + SameText(ExtractFileExt(AFilename), '.pp'); +end; + +function HasBuildInputExtension(const AFilename: string): Boolean; +var + Extension: string; +begin + Extension := LowerCase(ExtractFileExt(AFilename)); + Result := (Extension = '.pas') or (Extension = '.pp') or + (Extension = '.inc') or (Extension = '.lpi') or (Extension = '.lpk'); +end; + +function DirectoryIsReadable(const APath: string): Boolean; +var + Search: TSearchRec; +begin + Result := FindFirst(IncludeTrailingPathDelimiter(APath) + '*', + faAnyFile, Search) = 0; + if Result then + FindClose(Search); +end; + +function IsSymbolicLink(AAttributes: LongInt): Boolean; +begin + {$IFDEF UNIX} + Result := (AAttributes and faSymLink) <> 0; + {$ELSE} + Result := False; + {$ENDIF} +end; + +end. From e5c6fc173b464c024f4ad66a807eccc31fbe5916 Mon Sep 17 00:00:00 2001 From: Iwan Kelaiah Date: Sat, 19 Sep 2026 09:11:46 +1000 Subject: [PATCH 2/6] docs(api): document the parser, adapter, and CLI entry points Add XMLDoc comments to the Parser public API (input error, discovery options, and the BuildProject overloads), the FPCAdapter.ParseUnitFile contract, and the complete CLI exit-code contract. --- src/cli/PasWeave.CLI.pas | 4 +++- src/parser/PasWeave.FPCAdapter.pas | 14 ++++++++++++++ src/parser/PasWeave.Parser.pas | 30 ++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/cli/PasWeave.CLI.pas b/src/cli/PasWeave.CLI.pas index af1e0d9..54d2f0b 100644 --- a/src/cli/PasWeave.CLI.pas +++ b/src/cli/PasWeave.CLI.pas @@ -11,7 +11,9 @@ interface /// Runs PasWeave with the process command line. /// -/// @returns Process exit code (`0` for success, `1` for diagnostics failures). +/// @returns Process exit code: `0` success, `1` build diagnostics at or above +/// the `--fail-on` severity, `2` invalid command line or configuration, +/// `3` an unexpected internal error. function RunPasWeave: Integer; implementation diff --git a/src/parser/PasWeave.FPCAdapter.pas b/src/parser/PasWeave.FPCAdapter.pas index 7fd7998..701bbda 100644 --- a/src/parser/PasWeave.FPCAdapter.pas +++ b/src/parser/PasWeave.FPCAdapter.pas @@ -7,6 +7,20 @@ interface uses PasWeave.Comments, PasWeave.Compiler, PasWeave.Diagnostics, PasWeave.Model; +/// Parses one unit file into the documentation model. +/// +/// Reads the source, parses its interface section with `fcl-passrc`, and +/// converts the resulting elements into a `TDocUnit`. Parser failures are +/// reported as a diagnostic instead of an exception so a build can continue +/// with the remaining units. +/// +/// @param AFileName Source file to parse. +/// @param ASourceRoot Root used to store root-relative source filenames. +/// @param ACommentStyles Documentation comment styles to recognize. +/// @param ACompilerOptions Search paths, defines, and target; may be nil. +/// @param AUnit Receives the parsed unit when the result is True. +/// @param ADiagnostic Receives a stable diagnostic when the result is False. +/// @returns True when the unit parsed into the model. function ParseUnitFile(const AFileName, ASourceRoot: string; ACommentStyles: TDocumentationCommentStyles; ACompilerOptions: TCompilerOptions; diff --git a/src/parser/PasWeave.Parser.pas b/src/parser/PasWeave.Parser.pas index 962e406..3c5e0c1 100644 --- a/src/parser/PasWeave.Parser.pas +++ b/src/parser/PasWeave.Parser.pas @@ -8,8 +8,13 @@ interface Classes, SysUtils, PasWeave.Model, PasWeave.Comments, PasWeave.Compiler; type + /// Raised when the requested input or options cannot form a valid build. EPasWeaveInputError = class(Exception); + /// Controls recursive directory discovery and include/exclude globs. + /// + /// Patterns are matched case-insensitively against `/`-separated paths + /// relative to the source root. Exclusions always win over inclusions. TSourceDiscoveryOptions = class private FExcludePatterns: TStringList; @@ -19,30 +24,55 @@ TSourceDiscoveryOptions = class public constructor Create; destructor Destroy; override; + /// Adds an exclusion glob; absolute paths and parent traversal are + /// rejected with @link(EPasWeaveInputError). procedure AddExcludePattern(const APattern: string); + /// Adds an inclusion glob; absolute paths and parent traversal are + /// rejected with @link(EPasWeaveInputError). procedure AddIncludePattern(const APattern: string); + /// True when an exclude pattern matches the root-relative path. function IsExcluded(const ARelativePath: string): Boolean; + /// True when no include patterns exist or one matches the path, unless + /// the path is excluded first. function IsIncluded(const ARelativePath: string): Boolean; property ExcludePatterns: TStringList read FExcludePatterns; property IncludePatterns: TStringList read FIncludePatterns; + /// True when `--recursive`, `--include`, or `--exclude` was supplied. property HasExplicitSettings: Boolean read GetHasExplicitSettings; property Recursive: Boolean read FRecursive write FRecursive; end; +/// Builds a project from `ASourcePath` with default comment styles and +/// discovery options. function BuildProject(const ASourcePath, AProjectName: string; out AAttemptedFileCount: Integer): TDocProject; overload; +/// Builds a project from `ASourcePath` with explicit comment styles. function BuildProject(const ASourcePath, AProjectName: string; out AAttemptedFileCount: Integer; ACommentStyles: TDocumentationCommentStyles): TDocProject; overload; +/// Builds a project from `ASourcePath` with explicit discovery options. function BuildProject(const ASourcePath, AProjectName: string; out AAttemptedFileCount: Integer; ACommentStyles: TDocumentationCommentStyles; ADiscoveryOptions: TSourceDiscoveryOptions): TDocProject; overload; +/// Builds a project from `ASourcePath` with explicit comment styles, +/// discovery options, and compiler settings. +/// +/// @param ASourcePath A `.pas`/`.pp` unit, a source directory, or a Lazarus +/// `.lpi`/`.lpk` input. +/// @param AProjectName Display name; the input stem is used when empty. +/// @param AAttemptedFileCount Receives the number of parsed unit files, +/// including units that failed to parse. +/// @param ADiscoveryOptions Recursion and glob rules; may be nil. +/// @param ACompilerOptions Unit/include paths, defines, and target; may be nil. +/// @returns An owned project model; callers free it. function BuildProject(const ASourcePath, AProjectName: string; out AAttemptedFileCount: Integer; ACommentStyles: TDocumentationCommentStyles; ADiscoveryOptions: TSourceDiscoveryOptions; ACompilerOptions: TCompilerOptions): TDocProject; overload; +/// Builds a project from an explicit file list, as imported from a Lazarus +/// project or package. function BuildProjectFromFiles(const ASourceRoot, AProjectName: string; AFiles: TStrings; out AAttemptedFileCount: Integer; ACommentStyles: TDocumentationCommentStyles; From 84bfc90cd07bf21cdb46b48543923d8ef7f1291c Mon Sep 17 00:00:00 2001 From: Iwan Kelaiah Date: Sat, 19 Sep 2026 09:19:59 +1000 Subject: [PATCH 3/6] refactor(cli): centralize option value handling Replace the repeated --name and --name=value branch pairs with the MatchValueOption helper, which also advances the argument index for the separated form. ParseCommandLine drops from about 223 to 111 lines and the value syntax is handled in one place; all error messages and precedence are unchanged. Verified: make test (CLI and functional suites) and byte-identical checked-in example output. --- CHANGELOG.md | 3 + src/cli/PasWeave.CLI.pas | 227 ++++++++++++--------------------------- 2 files changed, 69 insertions(+), 161 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e184ef..cd86da7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,9 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht source-file detection, readability, symlink checks, and whole-file reads. Directory walkers remain specialized where their pruning and filtering semantics differ. +- Replaced the repeated `--name` / `--name=value` option branches with a + shared `MatchValueOption` helper, roughly halving `ParseCommandLine` and + centralizing separated-versus-equals value handling. ## [0.7.0] - 2026-09-17 diff --git a/src/cli/PasWeave.CLI.pas b/src/cli/PasWeave.CLI.pas index 54d2f0b..9f91bca 100644 --- a/src/cli/PasWeave.CLI.pas +++ b/src/cli/PasWeave.CLI.pas @@ -99,6 +99,25 @@ function RequireOptionValue(var AIndex: Integer; const AOption: string): string; Result := ParamStr(AIndex); end; +/// Matches `--name value` or `--name=value`, advancing AIndex for the +/// separated form. Returns False when the parameter is a different option. +function MatchValueOption(const AParam, AName: string; var AIndex: Integer; + out AValue: string): Boolean; +var + Prefix: string; +begin + AValue := ''; + if AParam = AName then + begin + AValue := RequireOptionValue(AIndex, AName); + Exit(True); + end; + Prefix := AName + '='; + Result := Pos(Prefix, AParam) = 1; + if Result then + AValue := Copy(AParam, Length(Prefix) + 1, MaxInt); +end; + procedure PrintDiagnostic(ADiagnostic: TDiagnostic; AVerbose: Boolean); var Location: string; @@ -136,7 +155,6 @@ function RunBuild: Integer; DiagnosticOutputFile: string; MarkdownOutputPath: string; HTMLOutputPath: string; - CommentStyleValue: string; BuildMode: string; CommentStyles: TDocumentationCommentStyles; DiscoveryOptions: TSourceDiscoveryOptions; @@ -148,7 +166,6 @@ function RunBuild: Integer; FailureSeverity: TDiagnosticSeverity; MinimumCoverage: Integer; HasMinimumCoverage: Boolean; - ThresholdValue: string; RepositoryURL: string; SourceLinkTemplate: string; SourceLinkError: string; @@ -252,207 +269,95 @@ function RunBuild: Integer; end; procedure ParseCommandLine; + var + OptionValue: string; begin I := 2; while I <= ParamCount do begin - if ParamStr(I) = '--output' then - OutputPath := RequireOptionValue(I, '--output') - else if Pos('--output=', ParamStr(I)) = 1 then - OutputPath := Copy(ParamStr(I), Length('--output=') + 1, MaxInt) - else if ParamStr(I) = '--project-name' then - begin - ProjectName := RequireOptionValue(I, '--project-name'); - ProjectNameExplicit := True; - end - else if Pos('--project-name=', ParamStr(I)) = 1 then + if MatchValueOption(ParamStr(I), '--output', I, OptionValue) then + OutputPath := OptionValue + else if MatchValueOption(ParamStr(I), '--project-name', I, OptionValue) then begin - ProjectName := Copy(ParamStr(I), Length('--project-name=') + 1, - MaxInt); + ProjectName := OptionValue; ProjectNameExplicit := True; end - else if ParamStr(I) = '--doc-comments' then + else if MatchValueOption(ParamStr(I), '--doc-comments', I, OptionValue) then begin - CommentStyleValue := RequireOptionValue(I, '--doc-comments'); - if not TryParseDocumentationCommentStyles(CommentStyleValue, - CommentStyles) then + if not TryParseDocumentationCommentStyles(OptionValue, CommentStyles) then raise EPasWeaveInputError.CreateFmt( 'invalid documentation comment styles: %s ' + '(expected slash (///), brace ({ ... }), paren ((* ... *)), ' + 'a comma-separated combination, or all)', - [CommentStyleValue]); - end - else if Pos('--doc-comments=', ParamStr(I)) = 1 then - begin - CommentStyleValue := Copy(ParamStr(I), Length('--doc-comments=') + 1, - MaxInt); - if not TryParseDocumentationCommentStyles(CommentStyleValue, - CommentStyles) then - raise EPasWeaveInputError.CreateFmt( - 'invalid documentation comment styles: %s ' + - '(expected slash (///), brace ({ ... }), paren ((* ... *)), ' + - 'a comma-separated combination, or all)', - [CommentStyleValue]); + [OptionValue]); end else if ParamStr(I) = '--recursive' then DiscoveryOptions.Recursive := True - else if ParamStr(I) = '--include' then - DiscoveryOptions.AddIncludePattern( - RequireOptionValue(I, '--include')) - else if Pos('--include=', ParamStr(I)) = 1 then - DiscoveryOptions.AddIncludePattern(Copy(ParamStr(I), - Length('--include=') + 1, MaxInt)) - else if ParamStr(I) = '--exclude' then - DiscoveryOptions.AddExcludePattern( - RequireOptionValue(I, '--exclude')) - else if Pos('--exclude=', ParamStr(I)) = 1 then - DiscoveryOptions.AddExcludePattern(Copy(ParamStr(I), - Length('--exclude=') + 1, MaxInt)) - else if ParamStr(I) = '--unit-path' then - CompilerOptions.AddUnitPath( - RequireOptionValue(I, '--unit-path')) - else if Pos('--unit-path=', ParamStr(I)) = 1 then - CompilerOptions.AddUnitPath(Copy(ParamStr(I), - Length('--unit-path=') + 1, MaxInt)) - else if ParamStr(I) = '--include-path' then - CompilerOptions.AddIncludePath( - RequireOptionValue(I, '--include-path')) - else if Pos('--include-path=', ParamStr(I)) = 1 then - CompilerOptions.AddIncludePath(Copy(ParamStr(I), - Length('--include-path=') + 1, MaxInt)) - else if ParamStr(I) = '--define' then - CompilerOptions.AddDefine(RequireOptionValue(I, '--define')) - else if Pos('--define=', ParamStr(I)) = 1 then - CompilerOptions.AddDefine(Copy(ParamStr(I), - Length('--define=') + 1, MaxInt)) - else if ParamStr(I) = '--target-os' then - CompilerOptions.SetTargetOS( - RequireOptionValue(I, '--target-os')) - else if Pos('--target-os=', ParamStr(I)) = 1 then - CompilerOptions.SetTargetOS(Copy(ParamStr(I), - Length('--target-os=') + 1, MaxInt)) - else if ParamStr(I) = '--target-cpu' then - CompilerOptions.SetTargetCPU( - RequireOptionValue(I, '--target-cpu')) - else if Pos('--target-cpu=', ParamStr(I)) = 1 then - CompilerOptions.SetTargetCPU(Copy(ParamStr(I), - Length('--target-cpu=') + 1, MaxInt)) - else if ParamStr(I) = '--build-mode' then - BuildMode := RequireOptionValue(I, '--build-mode') - else if Pos('--build-mode=', ParamStr(I)) = 1 then - BuildMode := Copy(ParamStr(I), Length('--build-mode=') + 1, MaxInt) - else if ParamStr(I) = '--package-path' then - PackagePaths.Add(RequireOptionValue(I, '--package-path')) - else if Pos('--package-path=', ParamStr(I)) = 1 then - PackagePaths.Add(Copy(ParamStr(I), Length('--package-path=') + 1, - MaxInt)) - else if ParamStr(I) = '--repository-url' then - RepositoryURL := RequireOptionValue(I, '--repository-url') - else if Pos('--repository-url=', ParamStr(I)) = 1 then - RepositoryURL := Copy(ParamStr(I), Length('--repository-url=') + 1, - MaxInt) - else if ParamStr(I) = '--source-link-template' then - SourceLinkTemplate := RequireOptionValue(I, '--source-link-template') - else if Pos('--source-link-template=', ParamStr(I)) = 1 then - SourceLinkTemplate := Copy(ParamStr(I), - Length('--source-link-template=') + 1, MaxInt) - else if ParamStr(I) = '--project-mark' then + else if MatchValueOption(ParamStr(I), '--include', I, OptionValue) then + DiscoveryOptions.AddIncludePattern(OptionValue) + else if MatchValueOption(ParamStr(I), '--exclude', I, OptionValue) then + DiscoveryOptions.AddExcludePattern(OptionValue) + else if MatchValueOption(ParamStr(I), '--unit-path', I, OptionValue) then + CompilerOptions.AddUnitPath(OptionValue) + else if MatchValueOption(ParamStr(I), '--include-path', I, OptionValue) then + CompilerOptions.AddIncludePath(OptionValue) + else if MatchValueOption(ParamStr(I), '--define', I, OptionValue) then + CompilerOptions.AddDefine(OptionValue) + else if MatchValueOption(ParamStr(I), '--target-os', I, OptionValue) then + CompilerOptions.SetTargetOS(OptionValue) + else if MatchValueOption(ParamStr(I), '--target-cpu', I, OptionValue) then + CompilerOptions.SetTargetCPU(OptionValue) + else if MatchValueOption(ParamStr(I), '--build-mode', I, OptionValue) then + BuildMode := OptionValue + else if MatchValueOption(ParamStr(I), '--package-path', I, OptionValue) then + PackagePaths.Add(OptionValue) + else if MatchValueOption(ParamStr(I), '--repository-url', I, OptionValue) then + RepositoryURL := OptionValue + else if MatchValueOption(ParamStr(I), '--source-link-template', I, OptionValue) then + SourceLinkTemplate := OptionValue + else if MatchValueOption(ParamStr(I), '--project-mark', I, OptionValue) then begin - ProjectMark := RequireOptionValue(I, '--project-mark'); - if not IsValidProjectMark(ProjectMark) then + if not IsValidProjectMark(OptionValue) then raise EPasWeaveInputError.Create( '--project-mark must be 1 to 4 alphanumeric characters'); + ProjectMark := OptionValue; HasProjectMark := True; end - else if Pos('--project-mark=', ParamStr(I)) = 1 then - begin - ProjectMark := Copy(ParamStr(I), Length('--project-mark=') + 1, - MaxInt); - if not IsValidProjectMark(ProjectMark) then - raise EPasWeaveInputError.Create( - '--project-mark must be 1 to 4 alphanumeric characters'); - HasProjectMark := True; - end - else if ParamStr(I) = '--theme-accent' then - begin - ThemeAccent := RequireOptionValue(I, '--theme-accent'); - if not IsValidThemeColor(ThemeAccent) then - raise EPasWeaveInputError.Create( - '--theme-accent must be a #RGB, #RRGGBB, or #RRGGBBAA color'); - HasThemeAccent := True; - end - else if Pos('--theme-accent=', ParamStr(I)) = 1 then + else if MatchValueOption(ParamStr(I), '--theme-accent', I, OptionValue) then begin - ThemeAccent := Copy(ParamStr(I), Length('--theme-accent=') + 1, - MaxInt); - if not IsValidThemeColor(ThemeAccent) then + if not IsValidThemeColor(OptionValue) then raise EPasWeaveInputError.Create( '--theme-accent must be a #RGB, #RRGGBB, or #RRGGBBAA color'); + ThemeAccent := OptionValue; HasThemeAccent := True; end - else if ParamStr(I) = '--theme-accent-2' then - begin - ThemeAccentAlt := RequireOptionValue(I, '--theme-accent-2'); - if not IsValidThemeColor(ThemeAccentAlt) then - raise EPasWeaveInputError.Create( - '--theme-accent-2 must be a #RGB, #RRGGBB, or #RRGGBBAA color'); - HasThemeAccentAlt := True; - end - else if Pos('--theme-accent-2=', ParamStr(I)) = 1 then + else if MatchValueOption(ParamStr(I), '--theme-accent-2', I, OptionValue) then begin - ThemeAccentAlt := Copy(ParamStr(I), Length('--theme-accent-2=') + 1, - MaxInt); - if not IsValidThemeColor(ThemeAccentAlt) then + if not IsValidThemeColor(OptionValue) then raise EPasWeaveInputError.Create( '--theme-accent-2 must be a #RGB, #RRGGBB, or #RRGGBBAA color'); + ThemeAccentAlt := OptionValue; HasThemeAccentAlt := True; end - else if ParamStr(I) = '--theme-font' then + else if MatchValueOption(ParamStr(I), '--theme-font', I, OptionValue) then begin - ThemeFont := RequireOptionValue(I, '--theme-font'); - if not IsValidThemeFont(ThemeFont) then + if not IsValidThemeFont(OptionValue) then raise EPasWeaveInputError.Create( '--theme-font must be a safe font family name'); + ThemeFont := OptionValue; HasThemeFont := True; end - else if Pos('--theme-font=', ParamStr(I)) = 1 then + else if MatchValueOption(ParamStr(I), '--min-documentation-coverage', I, OptionValue) then begin - ThemeFont := Copy(ParamStr(I), Length('--theme-font=') + 1, MaxInt); - if not IsValidThemeFont(ThemeFont) then - raise EPasWeaveInputError.Create( - '--theme-font must be a safe font family name'); - HasThemeFont := True; - end - else if ParamStr(I) = '--min-documentation-coverage' then - begin - ThresholdValue := RequireOptionValue(I, '--min-documentation-coverage'); - if not TryStrToInt(ThresholdValue, MinimumCoverage) or + if not TryStrToInt(OptionValue, MinimumCoverage) or (MinimumCoverage < 0) or (MinimumCoverage > 100) then raise EPasWeaveInputError.Create( '--min-documentation-coverage must be an integer from 0 to 100'); HasMinimumCoverage := True; end - else if Pos('--min-documentation-coverage=', ParamStr(I)) = 1 then - begin - ThresholdValue := Copy(ParamStr(I), - Length('--min-documentation-coverage=') + 1, MaxInt); - if not TryStrToInt(ThresholdValue, MinimumCoverage) or - (MinimumCoverage < 0) or (MinimumCoverage > 100) then - raise EPasWeaveInputError.Create( - '--min-documentation-coverage must be an integer from 0 to 100'); - HasMinimumCoverage := True; - end - else if ParamStr(I) = '--fail-on' then - begin - ThresholdValue := RequireOptionValue(I, '--fail-on'); - if not TryParseDiagnosticSeverity(ThresholdValue, FailureSeverity) then - raise EPasWeaveInputError.Create( - '--fail-on must be warning or error'); - end - else if Pos('--fail-on=', ParamStr(I)) = 1 then + else if MatchValueOption(ParamStr(I), '--fail-on', I, OptionValue) then begin - ThresholdValue := Copy(ParamStr(I), Length('--fail-on=') + 1, MaxInt); - if not TryParseDiagnosticSeverity(ThresholdValue, FailureSeverity) then + if not TryParseDiagnosticSeverity(OptionValue, FailureSeverity) then raise EPasWeaveInputError.Create( '--fail-on must be warning or error'); end From ce17ed5ae6941885d0b1484bcda6114cd6ef011a Mon Sep 17 00:00:00 2001 From: Iwan Kelaiah Date: Sat, 19 Sep 2026 09:24:09 +1000 Subject: [PATCH 4/6] docs(api): document the comment and compiler option contracts Add XMLDoc comments to the Comments unit (comment forms, style parsing, documentation extraction) and the Compiler unit (TCompilerOptions behavior, target normalization, and validation errors). --- src/parser/PasWeave.Comments.pas | 26 ++++++++++++++++++++++++++ src/parser/PasWeave.Compiler.pas | 24 ++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/src/parser/PasWeave.Comments.pas b/src/parser/PasWeave.Comments.pas index fba569b..59d7776 100644 --- a/src/parser/PasWeave.Comments.pas +++ b/src/parser/PasWeave.Comments.pas @@ -8,19 +8,45 @@ interface Contnrs; type + /// A documentation comment form PasWeave can recognize. TDocumentationCommentStyle = (dcsSlash, dcsBrace, dcsParen); TDocumentationCommentStyles = set of TDocumentationCommentStyle; const + /// `///` lines only; the safe default that never captures ordinary `//`. DefaultDocumentationCommentStyles: TDocumentationCommentStyles = [dcsSlash]; + /// Every supported form: `///`, `{ ... }`, and `(* ... *)`. AllDocumentationCommentStyles: TDocumentationCommentStyles = [dcsSlash, dcsBrace, dcsParen]; +/// Parses a `--doc-comments` value such as `slash,brace` or `all`. +/// +/// @param AValue Comma-separated, case-insensitive style names. +/// @param AStyles Receives the parsed set on success; emptied on failure so a +/// rejected value cannot leave partial styles behind. +/// @returns True when every name is recognized. function TryParseDocumentationCommentStyles(const AValue: string; out AStyles: TDocumentationCommentStyles): Boolean; +/// Formats a style set as stable `--doc-comments` text. +/// +/// @returns Comma-separated names in the fixed order `slash,brace,paren`. function DocumentationCommentStylesText( AStyles: TDocumentationCommentStyles): string; +/// Extracts the documentation group directly above a declaration. +/// +/// Ordinary `//` comments are never documentation. A blank line, a disabled +/// comment form, a compiler directive, or another source token ends the +/// association. Raw text keeps the original delimiters; the Markdown text has +/// them removed and structured directives stripped. +/// +/// @param ASourceText Full source text of the unit. +/// @param ADeclarationLine 1-based line of the declaration to attach to. +/// @param AStyles Comment forms to consider. +/// @param ARawDocumentation Receives the original comment text. +/// @param AMarkdownDocumentation Receives the normalized documentation body. +/// @param ADirectives Receives owned `TDocDirective` objects parsed from the +/// group; caller frees them with the list. procedure ParseDocumentationComment(const ASourceText: string; ADeclarationLine: Integer; AStyles: TDocumentationCommentStyles; out ARawDocumentation, AMarkdownDocumentation: string; diff --git a/src/parser/PasWeave.Compiler.pas b/src/parser/PasWeave.Compiler.pas index 40373a8..794e51b 100644 --- a/src/parser/PasWeave.Compiler.pas +++ b/src/parser/PasWeave.Compiler.pas @@ -8,8 +8,13 @@ interface Classes, SysUtils, PasWeave.FS; type + /// Raised when compiler settings cannot be validated or normalized. ECompilerConfigurationError = class(Exception); + /// Parser search paths, conditional defines, and target selection. + /// + /// Explicit settings take precedence over values imported from a Lazarus + /// project or package through @link(ApplyDefaultsFrom). TCompilerOptions = class private FDefines: TStringList; @@ -28,15 +33,27 @@ TCompilerOptions = class public constructor Create; destructor Destroy; override; + /// Adds a conditional define; names are normalized to uppercase and + /// duplicates ignored. Invalid identifiers raise + /// @link(ECompilerConfigurationError). procedure AddDefine(const AValue: string); + /// Adds an existing include directory; missing paths raise. procedure AddIncludePath(const AValue: string); + /// Adds an existing unit search directory; missing paths raise. procedure AddUnitPath(const AValue: string); + /// Appends `-d`, `-Fi`, `-Fu`, and target arguments for `fcl-passrc`. procedure AppendParserArguments(AArguments: TStrings); + /// Merges settings imported from a project or package into this object. procedure AppendImported(const AOptions: TCompilerOptions); + /// Applies imported settings only where this object has no explicit + /// value, so command-line options override Lazarus configuration. procedure ApplyDefaultsFrom(const AOptions: TCompilerOptions); + /// Sets and normalizes the target CPU; unsupported values raise. procedure SetTargetCPU(const AValue: string); + /// Sets and normalizes the target OS; unsupported values raise. procedure SetTargetOS(const AValue: string); property Defines: TStringList read FDefines; + /// True when any path, define, or target was set explicitly. property HasExplicitSettings: Boolean read GetHasExplicitSettings; property IncludePaths: TStringList read FIncludePaths; property TargetCPU: string read FTargetCPU; @@ -46,9 +63,16 @@ TCompilerOptions = class property UnitPaths: TStringList read FUnitPaths; end; +/// True for a plain Pascal identifier usable as a conditional define. function IsValidConditionalDefine(const AValue: string): Boolean; +/// Normalizes a CPU alias (for example `arm64` to `aarch64`). +/// +/// @returns False when the CPU is not a supported FPC target. function TryNormaliseTargetCPU(const AValue: string; out ANormalised: string): Boolean; +/// Normalizes an OS alias (for example `macOS` to `darwin`). +/// +/// @returns False when the OS is not a supported FPC target. function TryNormaliseTargetOS(const AValue: string; out ANormalised: string): Boolean; From 33a60ac835a702dd3d03007266e85f67869599d6 Mon Sep 17 00:00:00 2001 From: Iwan Kelaiah Date: Sat, 19 Sep 2026 09:25:40 +1000 Subject: [PATCH 5/6] docs(api): document the documentation model contract Add XMLDoc comments to the model types, fields, constants, and helper functions, including stable ID/anchor behavior, relationship resolution state, and the branding validators. --- src/model/PasWeave.Model.pas | 49 ++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/model/PasWeave.Model.pas b/src/model/PasWeave.Model.pas index ca4ffa2..0aeae61 100644 --- a/src/model/PasWeave.Model.pas +++ b/src/model/PasWeave.Model.pas @@ -8,6 +8,7 @@ interface Classes, Contnrs, PasWeave.Diagnostics; type + /// The declaration kind attached to a documented symbol. TSymbolKind = ( skUnit, skClass, @@ -25,6 +26,7 @@ interface skVariable ); + /// Source visibility; `svDefault` covers declarations outside a section. TSymbolVisibility = ( svDefault, svPrivate, @@ -36,96 +38,143 @@ interface svStrictProtected ); + /// How a type relates to another type. TTypeRelationshipKind = ( trkInheritance, trkImplementation ); + /// One structured documentation directive from a comment group. TDocDirective = class public + /// Directive name without the `@`, for example `param` or `returns`. Name: string; + /// Subject such as a parameter name; empty for `@returns` and `@since`. Subject: string; + /// Directive body text with the name and subject removed. Text: string; + /// Resolved symbol target for reference directives such as `@see`. TargetSymbolID: string; constructor Create(const AName, ASubject, AText: string); end; + /// A resolved or unresolved inheritance/implementation relationship. TDocTypeRelationship = class public Kind: TTypeRelationshipKind; + /// As written in the source, for example `TBase` or a specialization. TargetName: string; + /// Display text when the source spelling differs from `TargetName`. DisplayName: string; + /// Resolved target symbol ID; empty when unresolved. TargetSymbolID: string; constructor Create(AKind: TTypeRelationshipKind; const ATargetName, ADisplayName: string); end; + /// One renderable declaration in a unit. TDocSymbol = class public + /// Stable identity derived from kind, qualified name, and declaration. ID: string; Name: string; QualifiedName: string; Kind: TSymbolKind; Visibility: TSymbolVisibility; + /// Canonical, optionally wrapped declaration text. DeclarationText: string; + /// Unit-relative source path. SourceFilename: string; SourceLine: Integer; SourceColumn: Integer; + /// Original comment text including delimiters. RawDocumentation: string; + /// Normalized documentation body with directives removed. MarkdownDocumentation: string; + /// Owned `TDocDirective` objects. Directives: TObjectList; + /// Declared parameter names for routines. ParameterNames: TStringList; + /// True when a routine declares a return type. HasReturnValue: Boolean; + /// Owned `TDocTypeRelationship` objects. TypeRelationships: TObjectList; + /// Owning symbol ID for members; empty for top-level declarations. ParentSymbolID: string; + /// Sorted, deduplicated related symbol IDs. RelatedSymbolIDs: TStringList; constructor Create; destructor Destroy; override; end; + /// One parsed source unit. TDocUnit = class public Name: string; + /// Unit-relative source path. SourceFilename: string; + /// Sorted, case-insensitive names of interface `uses` dependencies. InterfaceDependencies: TStringList; + /// Owned `TDocSymbol` objects in declaration order. Symbols: TObjectList; constructor Create; destructor Destroy; override; end; + /// The complete documentation model for a build. TDocProject = class public Name: string; SourceRoot: string; RepositoryURL: string; SourceLinkTemplate: string; + /// Header brand mark; see @link(IsValidProjectMark). ProjectMark: string; + /// Primary accent color; see @link(IsValidThemeColor). ThemeAccent: string; + /// Secondary accent color. ThemeAccentAlt: string; + /// Body font family; see @link(IsValidThemeFont). ThemeFont: string; + /// Owned `TDocUnit` objects in discovery order. Units: TObjectList; + /// Owned warnings (`TDiagnostic`). Warnings: TObjectList; + /// Owned errors (`TDiagnostic`). Errors: TObjectList; constructor Create; destructor Destroy; override; + /// Total declaration count across all units, including unit symbols. function SymbolCount: Integer; end; const + /// Brand mark used when none is configured. DefaultProjectMark = 'PW'; DefaultThemeAccent = '#5b4ee6'; DefaultThemeAccentAlt = '#0e8f81'; DefaultThemeFont = 'Inter'; +/// Stable lowercase name for a symbol kind, used in JSON and diagnostics. function SymbolKindName(AKind: TSymbolKind): string; +/// Stable lowercase name for a visibility, used in JSON and diagnostics. function SymbolVisibilityName(AVisibility: TSymbolVisibility): string; +/// Stable relationship verb (`inherits` or `implements`). function TypeRelationshipKindName(AKind: TTypeRelationshipKind): string; +/// Deterministic, human-readable, overload-safe anchor for a symbol. function DocumentationSymbolAnchor(ASymbol: TDocSymbol): string; +/// Finds a symbol by exact ID inside one unit. function FindSymbolByID(AUnit: TDocUnit; const AID: string): TDocSymbol; +/// Finds a symbol by exact ID across a project. +/// +/// @param AUnit Receives the owning unit, or nil when not found. function FindProjectSymbolByID(AProject: TDocProject; const AID: string; out AUnit: TDocUnit): TDocSymbol; +/// True for a 1-4 character alphanumeric branding mark. function IsValidProjectMark(const AValue: string): Boolean; +/// True for a `#RGB`, `#RRGGBB`, or `#RRGGBBAA` color value. function IsValidThemeColor(const AValue: string): Boolean; +/// True for a safe CSS font family name. function IsValidThemeFont(const AValue: string): Boolean; implementation From c08e591eb8df80ef6936d6606e598d090b7e7373 Mon Sep 17 00:00:00 2001 From: Iwan Kelaiah Date: Sat, 19 Sep 2026 09:27:02 +1000 Subject: [PATCH 6/6] docs(api): document the HTML and Markdown renderer entry points Describe each public renderer function and the page sites written by WriteHTMLDocumentation and WriteMarkdownDocumentation. --- src/render/PasWeave.Render.HTML.pas | 12 ++++++++++++ src/render/PasWeave.Render.Markdown.pas | 5 +++++ 2 files changed, 17 insertions(+) diff --git a/src/render/PasWeave.Render.HTML.pas b/src/render/PasWeave.Render.HTML.pas index ecc6aee..e1902f3 100644 --- a/src/render/PasWeave.Render.HTML.pas +++ b/src/render/PasWeave.Render.HTML.pas @@ -8,16 +8,28 @@ interface uses PasWeave.Model; +/// Route-relative filename of a unit page, for example `Demo.Core.html`. function HTMLUnitFilename(AUnit: TDocUnit): string; +/// Fixed filename of the symbol index page. function HTMLSymbolIndexFilename: string; +/// Stable anchor of a symbol on its unit page. function HTMLSymbolAnchor(ASymbol: TDocSymbol): string; +/// Mermaid source for the unit dependency graph. function RenderMermaidDependencyGraph(AProject: TDocProject): UTF8String; +/// Mermaid source for the class/interface relationship graph; empty when the +/// project has no relationships. function RenderMermaidTypeRelationshipGraph( AProject: TDocProject): UTF8String; +/// Renders the project index page (summary, Browse API, units, diagrams). function RenderHTMLIndex(AProject: TDocProject): UTF8String; +/// Renders the A-Z/# symbol index page. function RenderHTMLSymbolIndex(AProject: TDocProject): UTF8String; +/// Renders one unit page with symbols, anchors, and source links. function RenderHTMLUnit(AProject: TDocProject; AUnit: TDocUnit): UTF8String; +/// Renders the offline search index as a JavaScript asset. function RenderHTMLSearchIndex(AProject: TDocProject): UTF8String; +/// Writes the complete HTML site, including offline assets, under +/// `AOutputDirectory`. procedure WriteHTMLDocumentation(AProject: TDocProject; const AOutputDirectory: string); diff --git a/src/render/PasWeave.Render.Markdown.pas b/src/render/PasWeave.Render.Markdown.pas index ec26fef..0d0df00 100644 --- a/src/render/PasWeave.Render.Markdown.pas +++ b/src/render/PasWeave.Render.Markdown.pas @@ -8,11 +8,16 @@ interface uses PasWeave.Model; +/// Route-relative filename of a unit page, for example `Demo.Core.md`. function MarkdownUnitFilename(AUnit: TDocUnit): string; +/// Stable anchor shared with the HTML renderer. function MarkdownSymbolAnchor(ASymbol: TDocSymbol): string; +/// Renders the Markdown project index with the units table and diagnostics. function RenderMarkdownIndex(AProject: TDocProject): UTF8String; +/// Renders one unit page with symbols, documentation, and source links. function RenderMarkdownUnit(AProject: TDocProject; AUnit: TDocUnit): UTF8String; +/// Writes the Markdown index and unit pages under `AOutputDirectory`. procedure WriteMarkdownDocumentation(AProject: TDocProject; const AOutputDirectory: string);