diff --git a/docs/design/api-mark-vhdl/vhdl-emitter-gradual-disclosure.md b/docs/design/api-mark-vhdl/vhdl-emitter-gradual-disclosure.md index a24afd1..2997b8f 100644 --- a/docs/design/api-mark-vhdl/vhdl-emitter-gradual-disclosure.md +++ b/docs/design/api-mark-vhdl/vhdl-emitter-gradual-disclosure.md @@ -55,14 +55,18 @@ index page. **VhdlEmitterGradualDisclosure.EmitEntityPage** (private static): Writes a single entity detail page. -- H1 entity name, summary, details, Generics table (Name/Type/Default/Description), - Ports table (Name/Direction/Type/Description), Architectures section (inline — - one bold entry per architecture with optional details paragraph). +- H1 entity name, `*Entity declared in \`{fileName}\`*` attribution paragraph, + summary, details, Generics section (H2 — table when generics are present, + `NoItemsPlaceholder` paragraph when empty), Ports table + (Name/Direction/Type/Description), Architectures section (inline — one bold + entry per architecture formatted as `**{name}** (\`{fileName}\`): {summary}` + with optional details paragraph). **VhdlEmitterGradualDisclosure.EmitPackagePage** (private static): Writes a single package detail page and calls `EmitSubprogramDetailPage` for each subprogram. -- H1 package name, summary, details, Types paragraphs, Constants paragraphs, +- H1 package name, `*Package declared in \`{fileName}\`*` attribution paragraph, + summary, details, Types paragraphs, Constants paragraphs, Components as `**name** — summary` paragraphs, Subprograms section with links to per-subprogram detail pages. @@ -83,7 +87,8 @@ one `{packageName}/{subprogramName}.md` detail file. ### Dependencies - **VhdlEmitter** (internal) — instantiates this class and supplies `Options` and - shared helpers (`GetSummary`, `DescriptionColumnHeader`, `NoDescriptionPlaceholder`). + shared helpers (`GetSummary`, `DescriptionColumnHeader`, `NoDescriptionPlaceholder`, + `NoItemsPlaceholder`). - **IMarkdownWriterFactory** (ApiMarkCore) — used to create each per-file Markdown writer. - **VhdlAstModel** (internal) — consumes `VhdlFileModel`, `VhdlEntityDecl`, diff --git a/docs/design/api-mark-vhdl/vhdl-emitter-single-file.md b/docs/design/api-mark-vhdl/vhdl-emitter-single-file.md index d52df37..067844a 100644 --- a/docs/design/api-mark-vhdl/vhdl-emitter-single-file.md +++ b/docs/design/api-mark-vhdl/vhdl-emitter-single-file.md @@ -52,14 +52,18 @@ file. **VhdlEmitterSingleFile.EmitEntitySection** (private static): Writes the per-entity block within the single-file output. -- H{depth+2} entity name, summary, details, optional Generics table (H{depth+3}), - optional Ports table (H{depth+3}), optional Architectures sub-section (H{depth+3}, - one bold paragraph per architecture with optional details). +- H{depth+2} entity name, `*Entity declared in \`{fileName}\`*` attribution + paragraph, summary, details, Generics section (H{depth+3} — table when generics + are present, `NoItemsPlaceholder` paragraph when empty), optional Ports table + (H{depth+3}), optional Architectures sub-section (H{depth+3}, one bold paragraph + per architecture formatted as `**{name}** (\`{fileName}\`): {summary}` with + optional details). **VhdlEmitterSingleFile.EmitPackageSection** (private static): Writes the per-package block within the single-file output. -- H{depth+2} package name, summary, details, optional Types section (H{depth+3}), +- H{depth+2} package name, `*Package declared in \`{fileName}\`*` attribution + paragraph, summary, details, optional Types section (H{depth+3}), optional Constants section (H{depth+3}), optional Components section (H{depth+3}), then calls `EmitSubprogramSection` for each subprogram. @@ -80,7 +84,8 @@ per-subprogram block within the single-file output. ### Dependencies - **VhdlEmitter** (internal) — instantiates this class and supplies `Options` and - shared helpers (`GetSummary`, `DescriptionColumnHeader`, `NoDescriptionPlaceholder`). + shared helpers (`GetSummary`, `DescriptionColumnHeader`, `NoDescriptionPlaceholder`, + `NoItemsPlaceholder`). - **IMarkdownWriterFactory** (ApiMarkCore) — used to create the single Markdown writer. - **VhdlAstModel** (internal) — consumes `VhdlFileModel`, `VhdlEntityDecl`, `VhdlArchitectureDecl`, and `VhdlPackageDecl` record types. diff --git a/src/ApiMark.Vhdl/VhdlEmitter.cs b/src/ApiMark.Vhdl/VhdlEmitter.cs index dbaa74c..9fc8062 100644 --- a/src/ApiMark.Vhdl/VhdlEmitter.cs +++ b/src/ApiMark.Vhdl/VhdlEmitter.cs @@ -12,6 +12,9 @@ internal sealed class VhdlEmitter : IApiEmitter /// Placeholder text for members without documentation. internal const string NoDescriptionPlaceholder = "*No description provided.*"; + /// Placeholder text for sections that have no items. + internal const string NoItemsPlaceholder = "*None.*"; + /// Object-class keywords stripped from subprogram parameter types before display. private static readonly HashSet ObjectClassKeywords = new(StringComparer.OrdinalIgnoreCase) { "SIGNAL", "VARIABLE", "CONSTANT", "FILE" }; diff --git a/src/ApiMark.Vhdl/VhdlEmitterGradualDisclosure.cs b/src/ApiMark.Vhdl/VhdlEmitterGradualDisclosure.cs index fe34bc7..f024088 100644 --- a/src/ApiMark.Vhdl/VhdlEmitterGradualDisclosure.cs +++ b/src/ApiMark.Vhdl/VhdlEmitterGradualDisclosure.cs @@ -28,24 +28,30 @@ internal void Emit(IMarkdownWriterFactory factory, EmitConfig config, IContext c _ = config; _ = context; - // Collect all entities, architectures, packages across all files - var allEntities = _fileModels.SelectMany(f => f.Entities).ToList(); - var allArchitectures = _fileModels.SelectMany(f => f.Architectures).ToList(); - var allPackages = _fileModels.SelectMany(f => f.Packages).ToList(); + // Collect all entities, architectures (with source filename), packages across all files + var allEntities = _fileModels + .SelectMany(f => f.Entities.Select(e => (Entity: e, FileName: Path.GetFileName(f.FilePath)))) + .ToList(); + var allArchitectures = _fileModels + .SelectMany(f => f.Architectures.Select(a => (Arch: a, FileName: Path.GetFileName(f.FilePath)))) + .ToList(); + var allPackages = _fileModels + .SelectMany(f => f.Packages.Select(p => (Package: p, FileName: Path.GetFileName(f.FilePath)))) + .ToList(); // Emit api.md index page - EmitApiIndexPage(factory, allEntities, allPackages); + EmitApiIndexPage(factory, allEntities.Select(t => t.Entity).ToList(), allPackages.Select(t => t.Package).ToList()); // Emit entity detail pages - foreach (var entity in allEntities) + foreach (var (entity, fileName) in allEntities) { - EmitEntityPage(factory, entity, allArchitectures); + EmitEntityPage(factory, entity, fileName, allArchitectures); } // Emit package detail pages and per-subprogram detail files - foreach (var pkg in allPackages) + foreach (var (pkg, fileName) in allPackages) { - EmitPackagePage(factory, pkg); + EmitPackagePage(factory, pkg, fileName); } } @@ -94,6 +100,7 @@ private void EmitApiIndexPage( /// /// Factory for creating the per-entity Markdown writer. /// The entity declaration to emit. + /// Base filename of the source file containing this entity declaration. /// /// All architecture declarations across all parsed files; filtered to those /// whose entity name matches . @@ -101,11 +108,15 @@ private void EmitApiIndexPage( private static void EmitEntityPage( IMarkdownWriterFactory factory, VhdlEntityDecl entity, - List allArchitectures) + string fileName, + List<(VhdlArchitectureDecl Arch, string FileName)> allArchitectures) { using var writer = factory.CreateMarkdown("", VhdlEmitter.SanitizeFileName(entity.Name)); writer.WriteHeading(1, entity.Name); + // Attribution: kind and source file + writer.WriteParagraph($"*Entity declared in `{fileName}`*"); + var summary = VhdlEmitter.GetSummary(entity.Doc) ?? VhdlEmitter.NoDescriptionPlaceholder; writer.WriteParagraph(summary); @@ -115,9 +126,9 @@ private static void EmitEntityPage( writer.WriteParagraph(details); } + writer.WriteHeading(2, "Generics"); if (entity.Generics.Count > 0) { - writer.WriteHeading(2, "Generics"); var headers = new[] { "Name", "Type", "Default", VhdlEmitter.DescriptionColumnHeader }; var rows = entity.Generics.Select(g => new[] { @@ -128,6 +139,10 @@ private static void EmitEntityPage( }); writer.WriteTable(headers, rows); } + else + { + writer.WriteParagraph(VhdlEmitter.NoItemsPlaceholder); + } if (entity.Ports.Count > 0) { @@ -145,18 +160,18 @@ private static void EmitEntityPage( // List architectures that implement this entity var archsForEntity = allArchitectures - .Where(a => string.Equals(a.EntityName, entity.Name, StringComparison.OrdinalIgnoreCase)) + .Where(t => string.Equals(t.Arch.EntityName, entity.Name, StringComparison.OrdinalIgnoreCase)) .ToList(); if (archsForEntity.Count > 0) { writer.WriteHeading(2, "Architectures"); - foreach (var arch in archsForEntity) + foreach (var (arch, archFileName) in archsForEntity) { - // Emit architecture summary as bold-name paragraph + // Emit architecture as: **name** (`filename`): summary or **name** (`filename`) var archSummary = VhdlEmitter.GetSummary(arch.Doc); writer.WriteParagraph(!string.IsNullOrEmpty(archSummary) - ? $"**{arch.Name}**: {archSummary}" - : $"**{arch.Name}**"); + ? $"**{arch.Name}** (`{archFileName}`): {archSummary}" + : $"**{arch.Name}** (`{archFileName}`)"); // Emit extended architecture details as a follow-on paragraph if present var archDetails = arch.Doc?.Details; @@ -174,11 +189,15 @@ private static void EmitEntityPage( /// /// Factory for creating Markdown writers for the package page and subprogram pages. /// The package declaration to emit. - private static void EmitPackagePage(IMarkdownWriterFactory factory, VhdlPackageDecl pkg) + /// Base filename of the source file containing this package declaration. + private static void EmitPackagePage(IMarkdownWriterFactory factory, VhdlPackageDecl pkg, string fileName) { using var writer = factory.CreateMarkdown("", VhdlEmitter.SanitizeFileName(pkg.Name)); writer.WriteHeading(1, pkg.Name); + // Attribution: kind and source file + writer.WriteParagraph($"*Package declared in `{fileName}`*"); + // Emit package summary paragraph var summary = VhdlEmitter.GetSummary(pkg.Doc); writer.WriteParagraph(!string.IsNullOrEmpty(summary) ? summary : VhdlEmitter.NoDescriptionPlaceholder); diff --git a/src/ApiMark.Vhdl/VhdlEmitterSingleFile.cs b/src/ApiMark.Vhdl/VhdlEmitterSingleFile.cs index ad88719..8ce64d2 100644 --- a/src/ApiMark.Vhdl/VhdlEmitterSingleFile.cs +++ b/src/ApiMark.Vhdl/VhdlEmitterSingleFile.cs @@ -29,9 +29,15 @@ internal void Emit(IMarkdownWriterFactory factory, EmitConfig config, IContext c var depth = config.HeadingDepth; - var allEntities = _fileModels.SelectMany(f => f.Entities).ToList(); - var allArchitectures = _fileModels.SelectMany(f => f.Architectures).ToList(); - var allPackages = _fileModels.SelectMany(f => f.Packages).ToList(); + var allEntities = _fileModels + .SelectMany(f => f.Entities.Select(e => (Entity: e, FileName: Path.GetFileName(f.FilePath)))) + .ToList(); + var allArchitectures = _fileModels + .SelectMany(f => f.Architectures.Select(a => (Arch: a, FileName: Path.GetFileName(f.FilePath)))) + .ToList(); + var allPackages = _fileModels + .SelectMany(f => f.Packages.Select(p => (Package: p, FileName: Path.GetFileName(f.FilePath)))) + .ToList(); using var writer = factory.CreateMarkdown("", "api"); @@ -51,9 +57,9 @@ internal void Emit(IMarkdownWriterFactory factory, EmitConfig config, IContext c if (allEntities.Count > 0) { writer.WriteHeading(depth + 1, "Entities"); - foreach (var entity in allEntities) + foreach (var (entity, fileName) in allEntities) { - EmitEntitySection(writer, entity, allArchitectures, depth); + EmitEntitySection(writer, entity, fileName, allArchitectures, depth); } } @@ -61,9 +67,9 @@ internal void Emit(IMarkdownWriterFactory factory, EmitConfig config, IContext c if (allPackages.Count > 0) { writer.WriteHeading(depth + 1, "Packages"); - foreach (var pkg in allPackages) + foreach (var (pkg, fileName) in allPackages) { - EmitPackageSection(writer, pkg, depth); + EmitPackageSection(writer, pkg, fileName, depth); } } } @@ -74,6 +80,7 @@ internal void Emit(IMarkdownWriterFactory factory, EmitConfig config, IContext c /// /// Markdown writer to emit into. /// The entity declaration to emit. + /// Base filename of the source file containing this entity declaration. /// /// All architecture declarations across all parsed files; filtered to those /// whose entity name matches . @@ -82,11 +89,15 @@ internal void Emit(IMarkdownWriterFactory factory, EmitConfig config, IContext c private static void EmitEntitySection( IMarkdownWriter writer, VhdlEntityDecl entity, - List allArchitectures, + string fileName, + List<(VhdlArchitectureDecl Arch, string FileName)> allArchitectures, int depth) { writer.WriteHeading(depth + 2, entity.Name); + // Attribution: kind and source file + writer.WriteParagraph($"*Entity declared in `{fileName}`*"); + var summary = VhdlEmitter.GetSummary(entity.Doc); writer.WriteParagraph(!string.IsNullOrEmpty(summary) ? summary : VhdlEmitter.NoDescriptionPlaceholder); @@ -96,9 +107,9 @@ private static void EmitEntitySection( writer.WriteParagraph(details); } + writer.WriteHeading(depth + 3, "Generics"); if (entity.Generics.Count > 0) { - writer.WriteHeading(depth + 3, "Generics"); var headers = new[] { "Name", "Type", "Default", VhdlEmitter.DescriptionColumnHeader }; var rows = entity.Generics.Select(g => new[] { @@ -109,6 +120,10 @@ private static void EmitEntitySection( }); writer.WriteTable(headers, rows); } + else + { + writer.WriteParagraph(VhdlEmitter.NoItemsPlaceholder); + } if (entity.Ports.Count > 0) { @@ -125,18 +140,18 @@ private static void EmitEntitySection( } var archsForEntity = allArchitectures - .Where(a => string.Equals(a.EntityName, entity.Name, StringComparison.OrdinalIgnoreCase)) + .Where(t => string.Equals(t.Arch.EntityName, entity.Name, StringComparison.OrdinalIgnoreCase)) .ToList(); if (archsForEntity.Count > 0) { writer.WriteHeading(depth + 3, "Architectures"); - foreach (var arch in archsForEntity) + foreach (var (arch, archFileName) in archsForEntity) { - // Emit architecture summary as bold-name paragraph + // Emit architecture as: **name** (`filename`): summary or **name** (`filename`) var archSummary = VhdlEmitter.GetSummary(arch.Doc); writer.WriteParagraph(!string.IsNullOrEmpty(archSummary) - ? $"**{arch.Name}**: {archSummary}" - : $"**{arch.Name}**"); + ? $"**{arch.Name}** (`{archFileName}`): {archSummary}" + : $"**{arch.Name}** (`{archFileName}`)"); // Emit extended architecture details as a follow-on paragraph if present var archDetails = arch.Doc?.Details; @@ -154,11 +169,15 @@ private static void EmitEntitySection( /// /// Markdown writer to emit into. /// The package declaration to emit. + /// Base filename of the source file containing this package declaration. /// Base heading depth for the library root; package heading uses depth+2. - private static void EmitPackageSection(IMarkdownWriter writer, VhdlPackageDecl pkg, int depth) + private static void EmitPackageSection(IMarkdownWriter writer, VhdlPackageDecl pkg, string fileName, int depth) { writer.WriteHeading(depth + 2, pkg.Name); + // Attribution: kind and source file + writer.WriteParagraph($"*Package declared in `{fileName}`*"); + // Emit package summary paragraph var summary = VhdlEmitter.GetSummary(pkg.Doc); writer.WriteParagraph(!string.IsNullOrEmpty(summary) ? summary : VhdlEmitter.NoDescriptionPlaceholder); diff --git a/test/ApiMark.Vhdl.Tests/VhdlEmitterGradualDisclosureTests.cs b/test/ApiMark.Vhdl.Tests/VhdlEmitterGradualDisclosureTests.cs index 9974dd1..3daf493 100644 --- a/test/ApiMark.Vhdl.Tests/VhdlEmitterGradualDisclosureTests.cs +++ b/test/ApiMark.Vhdl.Tests/VhdlEmitterGradualDisclosureTests.cs @@ -164,6 +164,108 @@ public void VhdlEmitterGradualDisclosure_Emit_WithArchitecture_EntityPageHasInli Assert.Contains(headings, h => h.Text.Equals("Architectures", StringComparison.Ordinal)); } + /// Validates that the architecture paragraph on an entity page includes the source filename. + [Fact] + public void VhdlEmitterGradualDisclosure_Emit_WithArchitecture_ArchitectureParagraphContainsFilename() + { + // Arrange + var factory = new InMemoryMarkdownWriterFactory(); + var (emitter, fileModels) = BuildDataWithArchitecture(); + + // Act + new VhdlEmitterGradualDisclosure(emitter, fileModels).Emit(factory, new EmitConfig(), new InMemoryContext()); + + // Assert: an architecture paragraph must contain both the bold architecture name and the source filename, + // distinguishing it from the entity attribution paragraph which also contains the filename + var entityWriter = factory.Writers.Values.FirstOrDefault(w => + w.Operations.OfType().Any(h => h.Text.Equals("MyEntity", StringComparison.Ordinal))); + Assert.NotNull(entityWriter); + var paragraphs = entityWriter.Operations.OfType().ToList(); + Assert.Contains(paragraphs, p => + p.Text.Contains("**behavioral**", StringComparison.Ordinal) && + p.Text.Contains("`test.vhd`", StringComparison.Ordinal)); + } + + /// Validates that an entity with no generics still emits a Generics section heading. + [Fact] + public void VhdlEmitterGradualDisclosure_Emit_EntityWithNoGenerics_EmitsGenericsHeading() + { + // Arrange + var factory = new InMemoryMarkdownWriterFactory(); + var (emitter, fileModels) = BuildMinimalData(); + + // Act + new VhdlEmitterGradualDisclosure(emitter, fileModels).Emit(factory, new EmitConfig(), new InMemoryContext()); + + // Assert: Generics heading must appear on the entity page even when the entity has no generics + var entityWriter = factory.Writers.Values.FirstOrDefault(w => + w.Operations.OfType().Any(h => h.Text.Equals("MyEntity", StringComparison.Ordinal))); + Assert.NotNull(entityWriter); + var headings = entityWriter.Operations.OfType().ToList(); + Assert.Contains(headings, h => h.Text.Equals("Generics", StringComparison.Ordinal)); + } + + /// Validates that an entity with no generics emits a none-placeholder paragraph in the Generics section. + [Fact] + public void VhdlEmitterGradualDisclosure_Emit_EntityWithNoGenerics_EmitsNonePlaceholderInGenericsSection() + { + // Arrange + var factory = new InMemoryMarkdownWriterFactory(); + var (emitter, fileModels) = BuildMinimalData(); + + // Act + new VhdlEmitterGradualDisclosure(emitter, fileModels).Emit(factory, new EmitConfig(), new InMemoryContext()); + + // Assert: none-placeholder paragraph must appear on the entity page + var entityWriter = factory.Writers.Values.FirstOrDefault(w => + w.Operations.OfType().Any(h => h.Text.Equals("MyEntity", StringComparison.Ordinal))); + Assert.NotNull(entityWriter); + var paragraphs = entityWriter.Operations.OfType().ToList(); + Assert.Contains(paragraphs, p => p.Text.Equals(VhdlEmitter.NoItemsPlaceholder, StringComparison.Ordinal)); + } + + /// Validates that the entity page includes an attribution paragraph naming the source file. + [Fact] + public void VhdlEmitterGradualDisclosure_Emit_Entity_PageContainsEntityAttributionParagraph() + { + // Arrange + var factory = new InMemoryMarkdownWriterFactory(); + var (emitter, fileModels) = BuildMinimalData(); + + // Act + new VhdlEmitterGradualDisclosure(emitter, fileModels).Emit(factory, new EmitConfig(), new InMemoryContext()); + + // Assert: attribution paragraph must identify kind and source filename + var entityWriter = factory.Writers.Values.FirstOrDefault(w => + w.Operations.OfType().Any(h => h.Text.Equals("MyEntity", StringComparison.Ordinal))); + Assert.NotNull(entityWriter); + var paragraphs = entityWriter.Operations.OfType().ToList(); + Assert.Contains(paragraphs, p => + p.Text.Contains("Entity", StringComparison.Ordinal) && + p.Text.Contains("`test.vhd`", StringComparison.Ordinal)); + } + + /// Validates that the package page includes an attribution paragraph naming the source file. + [Fact] + public void VhdlEmitterGradualDisclosure_Emit_Package_PageContainsPackageAttributionParagraph() + { + // Arrange + var factory = new InMemoryMarkdownWriterFactory(); + var (emitter, fileModels) = BuildDataWithPackageMembers(); + + // Act + new VhdlEmitterGradualDisclosure(emitter, fileModels).Emit(factory, new EmitConfig(), new InMemoryContext()); + + // Assert: attribution paragraph must identify kind and source filename + var pkgWriter = factory.Writers.Values.FirstOrDefault(w => + w.Operations.OfType().Any(h => h.Text.Equals("my_pkg", StringComparison.Ordinal))); + Assert.NotNull(pkgWriter); + var paragraphs = pkgWriter.Operations.OfType().ToList(); + Assert.Contains(paragraphs, p => + p.Text.Contains("Package", StringComparison.Ordinal) && + p.Text.Contains("`test.vhd`", StringComparison.Ordinal)); + } + /// Validates that package with members emits Types section on its detail page. [Fact] public void VhdlEmitterGradualDisclosure_Emit_PackageWithTypes_EmitsTypesSection() diff --git a/test/ApiMark.Vhdl.Tests/VhdlEmitterSingleFileTests.cs b/test/ApiMark.Vhdl.Tests/VhdlEmitterSingleFileTests.cs index a902cba..e0b72fc 100644 --- a/test/ApiMark.Vhdl.Tests/VhdlEmitterSingleFileTests.cs +++ b/test/ApiMark.Vhdl.Tests/VhdlEmitterSingleFileTests.cs @@ -212,6 +212,98 @@ public void VhdlEmitterSingleFile_Emit_EntityWithArchitecture_ArchitectureSectio Assert.Contains(headings, h => h.Text.Equals("Architectures", StringComparison.Ordinal)); } + /// Validates that the architecture paragraph in single-file output includes the source filename. + [Fact] + public void VhdlEmitterSingleFile_Emit_EntityWithArchitecture_ArchitectureParagraphContainsFilename() + { + // Arrange + var factory = new InMemoryMarkdownWriterFactory(); + var (emitter, fileModels) = BuildEntityWithArchData(); + + // Act + new VhdlEmitterSingleFile(emitter, fileModels).Emit(factory, new EmitConfig { Format = OutputFormat.SingleFile }, new InMemoryContext()); + + // Assert: an architecture paragraph must contain both the bold architecture name and the source filename, + // distinguishing it from the entity attribution paragraph which also contains the filename + var apiWriter = factory.GetWriter("", "api"); + var paragraphs = apiWriter.Operations.OfType().ToList(); + Assert.Contains(paragraphs, p => + p.Text.Contains("**behavioral**", StringComparison.Ordinal) && + p.Text.Contains("`test.vhd`", StringComparison.Ordinal)); + } + + /// Validates that an entity with no generics still emits a Generics section heading in single-file output. + [Fact] + public void VhdlEmitterSingleFile_Emit_EntityWithNoGenerics_EmitsGenericsHeading() + { + // Arrange + var factory = new InMemoryMarkdownWriterFactory(); + var (emitter, fileModels) = BuildMinimalData(); + + // Act + new VhdlEmitterSingleFile(emitter, fileModels).Emit(factory, new EmitConfig { Format = OutputFormat.SingleFile }, new InMemoryContext()); + + // Assert: Generics heading must appear even when the entity has no generics + var apiWriter = factory.GetWriter("", "api"); + var headings = apiWriter.Operations.OfType().ToList(); + Assert.Contains(headings, h => h.Text.Equals("Generics", StringComparison.Ordinal)); + } + + /// Validates that an entity with no generics emits a none-placeholder paragraph in single-file output. + [Fact] + public void VhdlEmitterSingleFile_Emit_EntityWithNoGenerics_EmitsNonePlaceholderInGenericsSection() + { + // Arrange + var factory = new InMemoryMarkdownWriterFactory(); + var (emitter, fileModels) = BuildMinimalData(); + + // Act + new VhdlEmitterSingleFile(emitter, fileModels).Emit(factory, new EmitConfig { Format = OutputFormat.SingleFile }, new InMemoryContext()); + + // Assert: none-placeholder paragraph must appear in the api output + var apiWriter = factory.GetWriter("", "api"); + var paragraphs = apiWriter.Operations.OfType().ToList(); + Assert.Contains(paragraphs, p => p.Text.Equals(VhdlEmitter.NoItemsPlaceholder, StringComparison.Ordinal)); + } + + /// Validates that an entity section includes an attribution paragraph naming the source file in single-file output. + [Fact] + public void VhdlEmitterSingleFile_Emit_Entity_SectionContainsEntityAttributionParagraph() + { + // Arrange + var factory = new InMemoryMarkdownWriterFactory(); + var (emitter, fileModels) = BuildMinimalData(); + + // Act + new VhdlEmitterSingleFile(emitter, fileModels).Emit(factory, new EmitConfig { Format = OutputFormat.SingleFile }, new InMemoryContext()); + + // Assert: attribution paragraph must identify kind and source filename + var apiWriter = factory.GetWriter("", "api"); + var paragraphs = apiWriter.Operations.OfType().ToList(); + Assert.Contains(paragraphs, p => + p.Text.Contains("Entity", StringComparison.Ordinal) && + p.Text.Contains("`test.vhd`", StringComparison.Ordinal)); + } + + /// Validates that a package section includes an attribution paragraph naming the source file in single-file output. + [Fact] + public void VhdlEmitterSingleFile_Emit_Package_SectionContainsPackageAttributionParagraph() + { + // Arrange + var factory = new InMemoryMarkdownWriterFactory(); + var (emitter, fileModels) = BuildPackageWithTypesData(); + + // Act + new VhdlEmitterSingleFile(emitter, fileModels).Emit(factory, new EmitConfig { Format = OutputFormat.SingleFile }, new InMemoryContext()); + + // Assert: attribution paragraph must identify kind and source filename + var apiWriter = factory.GetWriter("", "api"); + var paragraphs = apiWriter.Operations.OfType().ToList(); + Assert.Contains(paragraphs, p => + p.Text.Contains("Package", StringComparison.Ordinal) && + p.Text.Contains("`test.vhd`", StringComparison.Ordinal)); + } + /// Builds data with a subprogram that has formal parameters for Parameters-section tests. private static (VhdlEmitter emitter, IReadOnlyList fileModels) BuildPackageWithParameterizedSubprogramData() {