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