CodeBlock razor repro
-
- Four <CodeBlock> invocations authored in a real
- Razor @@page. Confirms that the Razor-side component
- runs the Roslyn preprocessor when a modifier is present.
-
Reference a symbol by xmldocid. Pennington.Roslyn pulls the source and hands it to the real semantic highlighter. Renames don’t break your docs.
Reference a member by name path with :symbol. Pennington.TreeSitter extracts the declaration straight from source — no copy to drift out of sync.
```csharp:xmldocid,bodyonly -M:Pennington.BlogSite. - BlogSiteServiceExtensions. - AddBlogSite(IServiceCollection) +```csharp:symbol,bodyonly +src/Pennington.BlogSite/ + BlogSiteServiceExtensions.cs > + BlogSiteServiceExtensions.AddBlogSite ```
` elements taller than 25 lines — those are candidates for a `,bodyonly` or member-scoped follow-up pass.
-- Rename `Tokenize` to `Split` in `examples/FocusedCodeSamplesExample/ModularWordCounter.cs` and rebuild — the build report surfaces an unresolved `M:…Tokenize(…)` rather than silently rendering nothing.
+- Rename `Tokenize` to `Split` in `examples/FocusedCodeSamplesExample/ModularWordCounter.cs` and rebuild — the build report surfaces an unresolved `ModularWordCounter.Tokenize` reference rather than silently rendering nothing.
## Related
-- Reference: [Markdown extensions catalog](xref:reference.markdown.extensions) — the full fence grammar including `xmldocid`, `xmldocid,bodyonly`, `xmldocid-diff`, and `path`.
+- Reference: [Markdown extensions catalog](xref:reference.markdown.extensions) — the full fence grammar including `symbol`, `symbol,bodyonly`, and `symbol-diff`.
- Reference: [Code-block argument reference](xref:reference.markdown.code-block-args) — info-string parser details and the full list of suffix forms.
- How-to: [Annotate code blocks](xref:how-to.code-samples.code-annotations) — per-line `[!code highlight]` / `[!code ++]` directives that compose with the fence forms on this page.
-- Reference: [RoslynOptions](xref:reference.api.roslyn-options) — the `SolutionPath` setting that lets the preprocessor resolve `T:` / `M:` / `P:` targets.
diff --git a/docs/Pennington.Docs/Content/how-to/content-services/auto-api-reference.md b/docs/Pennington.Docs/Content/how-to/content-services/auto-api-reference.md
index ad7ec284..16c4d4be 100644
--- a/docs/Pennington.Docs/Content/how-to/content-services/auto-api-reference.md
+++ b/docs/Pennington.Docs/Content/how-to/content-services/auto-api-reference.md
@@ -1,37 +1,21 @@
---
title: "Auto-generate an API reference tree for a class library"
-description: "Wire a metadata backend (Roslyn workspace or a compiled .dll + .xml pair), call AddApiReference, and get one /reference/api/{type}/ page per public type plus inline Mdazor components for member tables, summaries, and extension-method catalogs."
+description: "Wire the reflection metadata backend (a compiled .dll + .xml pair), call AddApiReference, and get one /reference/api/{type}/ page per public type plus inline Mdazor components for member tables, summaries, and extension-method catalogs."
uid: how-to.content-services.auto-api-reference
order: 208020
sectionLabel: "Content Services"
-tags: [extensibility, roslyn, reflection, xmldoc, api-reference, content-service]
+tags: [extensibility, reflection, xmldoc, api-reference, content-service]
---
To ship a DocSite whose reference section stays in sync with a class library's public surface, register a metadata backend and call `AddApiReference()`. One Razor template renders every public type, and a handful of Mdazor components (``, ``, ``, ``) are available inline in markdown for hand-authored reference pages. Every downstream page, search entry, and xref keys off a single pass over the configured backend.
-Two backends are available:
-
-- `Pennington.Roslyn.ApiMetadata.AddApiMetadataFromRoslyn()` — walks a live Roslyn workspace. Use when you build the documented library from source alongside the docs host.
-- `Pennington.ApiMetadata.Reflection.AddApiMetadataFromCompiledAssembly()` — reflects over a compiled `.dll` and parses the companion xmldoc `.xml` file. Use when you document a third-party assembly (for example, a NuGet package) without vendoring its source.
+`Pennington.ApiMetadata.Reflection.AddApiMetadataFromCompiledAssembly()` is the metadata backend: it reflects over a compiled `.dll` and parses the companion xmldoc `.xml` file. It documents any assembly — one you build alongside the docs host, or a third-party NuGet package — without needing its source.
## Before you begin
- `AddDocSite` is already wired: `AddApiReference` appends its own assembly to `DocSiteOptions.AdditionalRoutingAssemblies` at registration time, so it must run after `AddDocSite`.
- One metadata backend is registered before `AddApiReference`. Without one, the content service has nothing to publish.
-## Wire the Roslyn backend
-
-Add project references to `Pennington.Roslyn` and `Pennington.DocSite.Api`, then call `AddApiMetadataFromRoslyn` followed by `AddApiReference` after `AddDocSite`. With no options, the default `ProjectFilter` excludes `*.Tests` / `*.IntegrationTests` projects and the entry assembly itself.
-
-```csharp
-builder.Services.AddDocSite(() => new DocSiteOptions { /* ... */ });
-builder.Services.AddPenningtonRoslyn(r => r.SolutionPath = "../MyLibrary.slnx");
-builder.Services.AddApiMetadataFromRoslyn();
-builder.Services.AddApiReference();
-```
-
-The target library needs `true ` — without that, `ISymbol.GetDocumentationCommentXml()` returns empty strings and the generated pages have no prose.
-
## Wire the reflection backend
Add a project reference to `Pennington.ApiMetadata.Reflection`, add a `` to the library you want to document, and have Pennington resolve the assembly by simple name. A complete single-package DocSite host:
@@ -49,7 +33,7 @@ builder.Services.AddApiMetadataFromCompiledAssembly(opts =>
opts.AssemblyFiles.Add(Path.Combine(builder.Environment.ContentRootPath, "lib", "net9.0", "Foo.dll")));
```
-The reflection backend inspects metadata without running the assembly's code — no MSBuild workspace, no source needed. `` and the `:xmldocid` source fence require a live symbol graph and are unavailable under this backend.
+The reflection backend inspects metadata without running the assembly's code — no MSBuild workspace, no source needed. It resolves ` `, union cases, and `` from metadata. Only the `:xmldocid` source fence is unavailable under this backend, since it extracts source text rather than metadata.
## Customize the route prefix
@@ -90,31 +74,19 @@ Each `FromPackageReference` call resolves one DLL from its matching ``.
-Pass a predicate when a single solution mixes libraries you want to document with ones you do not — integration fixtures, sample apps, unrelated utility projects. The predicate receives the Roslyn `Project` directly, so filters on `Name`, `AssemblyName`, or language work equally well.
+Use `AssemblyFiles` to document an explicit list of `.dll` paths when a folder holds more assemblies than you want documented — for example, dependencies copied alongside the target only so `MetadataLoadContext` can resolve them:
```csharp
-builder.Services.AddApiMetadataFromRoslyn(opts =>
+builder.Services.AddApiMetadataFromCompiledAssembly(opts =>
{
- opts.ProjectFilter = project =>
- project.Name.StartsWith("MyLibrary", StringComparison.Ordinal)
- && !project.Name.EndsWith(".Tests", StringComparison.Ordinal);
+ opts.AssemblyFiles.Add(Path.Combine(libDir, "MyLibrary.dll"));
+ opts.AssemblyFiles.Add(Path.Combine(libDir, "MyLibrary.Extensions.dll"));
});
```
-### Hide individual types with `TypeFilter`
-
-`TypeFilter` runs on top of the built-in rules (public, non-delegate, non-attribute, non-`ComponentBase`, has xmldoc). Use it to drop a namespace that is public only by build necessity, or to skip types tagged with a marker attribute.
-
-```csharp
-builder.Services.AddApiMetadataFromRoslyn(opts =>
-{
- opts.TypeFilter = type =>
- type.ContainingNamespace.ToDisplayString() != "MyLibrary.Internal"
- && !type.GetAttributes().Any(a => a.AttributeClass?.Name == "InternalApiAttribute");
-});
-```
+Use `AssemblyDirectories` instead to document every `.dll`/`.xml` pair in a folder — the typical NuGet `lib//` layout.
## Render reference fragments inline
@@ -150,7 +122,7 @@ Pass a method xmldocid (`M:...`). The table pulls parameter names and types from
### Catalog extension methods by receiver with ``
-Groups every public extension method in the workspace by the unqualified short name of its first (receiver) parameter. `Receiver="IServiceCollection"` gathers every `services.AddX()` helper the library ships. Roslyn-only; the DocFx backend returns an empty list.
+Groups every public extension method in the assembly by the unqualified short name of its first (receiver) parameter. `Receiver="IServiceCollection"` gathers every `services.AddX()` helper the library ships.
````markdown
@@ -176,6 +148,5 @@ Xref links like `` resolve, the pages flow
## Related
-- Tutorial: — the `AddPenningtonRoslyn` / `SolutionPath` wire-up the Roslyn backend builds on.
- How-to: — hand-write an `IContentService` when `AddApiReference`'s discovery rules are not the right shape.
- Reference: , .
diff --git a/docs/Pennington.Docs/Content/how-to/markdown-pipeline/code-block-preprocessor.md b/docs/Pennington.Docs/Content/how-to/markdown-pipeline/code-block-preprocessor.md
index 579fbe81..5d493548 100644
--- a/docs/Pennington.Docs/Content/how-to/markdown-pipeline/code-block-preprocessor.md
+++ b/docs/Pennington.Docs/Content/how-to/markdown-pipeline/code-block-preprocessor.md
@@ -14,7 +14,7 @@ The recipe references `examples/ExtensibilityLabExample/LineCountPreprocessor.cs
## Before you begin
- An existing Pennington site with markdown rendering wired (see if not).
-- A chosen fence identifier — either a full `languageId` (`linecount`) or a `:modifier` suffix (`csharp:xmldocid`).
+- A chosen fence identifier — either a full `languageId` (`linecount`) or a `:modifier` suffix (`csharp:symbol`).
## Write the preprocessor
@@ -28,11 +28,11 @@ The returned `CodeBlockPreprocessResult` carries the pre-rendered HTML, the `Bas
## Pick a Priority value
-`CodeHighlightRenderer` sorts preprocessors by `Priority` descending and returns the first non-null result. The shipped `RoslynCodeBlockPreprocessor` uses `100`; `LineCountPreprocessor` uses `500` so its `linecount` fence is never intercepted by a lower-priority modifier preprocessor. Pick above `100` to win against the Roslyn preprocessor on a contested `:modifier`; pick below `100` to fall through to Roslyn first.
+`CodeHighlightRenderer` sorts preprocessors by `Priority` descending and returns the first non-null result. `LineCountPreprocessor` uses `500` so its `linecount` fence is never intercepted by a lower-priority modifier preprocessor. Pick a value above any preprocessor you need to beat on a contested `:modifier`, or below it to fall through first.
## Register the implementation
-Pennington collects every `ICodeBlockPreprocessor` from DI. Register anywhere after `AddPennington` — there is no `PenningtonOptions` knob. `AddPenningtonRoslyn` performs the equivalent registration for `RoslynCodeBlockPreprocessor`.
+Pennington collects every `ICodeBlockPreprocessor` from DI. Register anywhere after `AddPennington` — there is no `PenningtonOptions` knob. `AddPenningtonTreeSitter` performs the equivalent registration for its `:symbol` preprocessor.
```csharp
builder.Services.AddSingleton();
diff --git a/docs/Pennington.Docs/Content/llms.txt b/docs/Pennington.Docs/Content/llms.txt
index 738bcb65..f82ab0e9 100644
--- a/docs/Pennington.Docs/Content/llms.txt
+++ b/docs/Pennington.Docs/Content/llms.txt
@@ -2,4 +2,4 @@
> A content engine for .NET that transforms markdown into static documentation sites and blogs.
-Pennington is a library for building content-driven .NET websites. It provides a pipeline that discovers markdown files, parses YAML front matter, renders HTML with syntax highlighting, and generates static sites. Key features include hot reload during development, SPA-style navigation, pluggable code highlighting (including Roslyn-powered semantic highlighting), and utility-first CSS via MonorailCSS.
+Pennington is a library for building content-driven .NET websites. It provides a pipeline that discovers markdown files, parses YAML front matter, renders HTML with syntax highlighting, and generates static sites. Key features include hot reload during development, SPA-style navigation, pluggable code highlighting, and utility-first CSS via MonorailCSS.
diff --git a/docs/Pennington.Docs/Content/reference/host/extensions.md b/docs/Pennington.Docs/Content/reference/host/extensions.md
index 8e5154d2..2a2ce7e1 100644
--- a/docs/Pennington.Docs/Content/reference/host/extensions.md
+++ b/docs/Pennington.Docs/Content/reference/host/extensions.md
@@ -1,6 +1,6 @@
---
title: "DI and middleware extension methods"
-description: "Index of every AddPennington/UsePennington/Run* extension method across the Pennington, DocSite, BlogSite, MonorailCSS, and Roslyn packages."
+description: "Index of every AddPennington/UsePennington/Run* extension method across the Pennington, DocSite, BlogSite, and MonorailCSS packages."
sectionLabel: "Host Integration"
order: 406010
tags: [host, di, middleware, extensions]
diff --git a/docs/Pennington.Docs/Content/reference/markdown/code-block-args.md b/docs/Pennington.Docs/Content/reference/markdown/code-block-args.md
index cdffedc1..bf5b1797 100644
--- a/docs/Pennington.Docs/Content/reference/markdown/code-block-args.md
+++ b/docs/Pennington.Docs/Content/reference/markdown/code-block-args.md
@@ -14,10 +14,8 @@ The fence info-string grammar: the opening-fence text after the three backticks,
```text
info-string := language [ ":" suffix ] ( WS attribute )*
language := IDENT
-suffix := "path"
- | "xmldocid" xmldocid-flags
- | "xmldocid-diff" [ ",bodyonly" ]
-xmldocid-flags := ( "," ( "bodyonly" | "usings" ) )*
+suffix := "symbol" [ ",bodyonly" ]
+ | "symbol-diff" [ ",bodyonly" ]
attribute := key "=" value
key := IDENT
value := bare-value | "'" quoted-value "'" | '"' quoted-value '"'
@@ -25,7 +23,7 @@ bare-value := any run of non-whitespace chars
quoted-value := any chars up to the matching quote
```
-`language` is typically `csharp`, `razor`, `text`, etc. `xmldocid-flags` may appear in any order. Quoting is required only when a value contains whitespace. Markdig exposes the language and colon-suffix on `FencedCodeBlock.Info` and the attribute tail on `FencedCodeBlock.Arguments`; attribute keys are matched case-insensitively.
+`language` is typically `csharp`, `razor`, `text`, etc. Quoting is required only when a value contains whitespace. Markdig exposes the language and colon-suffix on `FencedCodeBlock.Info` and the attribute tail on `FencedCodeBlock.Arguments`; attribute keys are matched case-insensitively.
## Attributes
@@ -38,13 +36,11 @@ quoted-value := any chars up to the matching quote
| Form | Body shape | Description |
|---|---|---|
-| `:path` | one file path relative to the solution directory | Embeds the entire file contents. Accepts any file type. |
-| `:xmldocid` | one XmlDocId per line (`T:`, `M:`, `P:`, `F:`, `E:`) | Embeds each symbol's declaration and body, concatenated in order. |
-| `:xmldocid,bodyonly` | one XmlDocId per line | Embeds only the member body, stripping the declaration line and enclosing braces. |
-| `:xmldocid,usings` | one XmlDocId per line | Prepends the file-local `using` directives the fragment references, unioned across all listed XmlDocIds. Composes with `,bodyonly` in any order. Skips `global using` directives and implicit usings. |
-| `:xmldocid-diff` | exactly two XmlDocIds, before then after | Emits a unified diff between the two symbols' source text. Accepts the `,bodyonly` suffix. |
+| `:symbol` | one `` path, optionally followed by `> Member.Path`, per line | Embeds the whole file, or the named member's declaration and body. Concatenated in order. |
+| `:symbol,bodyonly` | same as `:symbol` | Embeds only the member body, stripping the declaration line and enclosing braces. |
+| `:symbol-diff` | exactly two references, before then after | Emits a unified diff between the two members' source text. Accepts the `,bodyonly` suffix. |
-Suffix forms are resolved by an `ICodeBlockPreprocessor`; `Pennington.Roslyn` ships the implementations for `xmldocid` and `xmldocid-diff`.
+Suffix forms are resolved by an `ICodeBlockPreprocessor`; `Pennington.TreeSitter` ships the implementations for `symbol` and `symbol-diff`.
## `[!code …]` directives
diff --git a/docs/Pennington.Docs/Content/reference/ui/content.md b/docs/Pennington.Docs/Content/reference/ui/content.md
index 8e54f509..962a953c 100644
--- a/docs/Pennington.Docs/Content/reference/ui/content.md
+++ b/docs/Pennington.Docs/Content/reference/ui/content.md
@@ -131,7 +131,7 @@ Run `dotnet run` and visit `http://localhost:5000/`.
## `CodeBlock`
-Razor-page entry to the shared code-block rendering pipeline — registered `ICodeBlockPreprocessor` implementations (including Roslyn `:xmldocid` and `:path` fences when `AddPenningtonRoslyn` is wired), highlighter dispatch via `HighlightingService`, `[!code …]` line transformations, and the standard `code-highlight-wrapper` container. Not registered with Mdazor — markdown authors should use a fenced code block (same pipeline, same output shape) instead.
+Razor-page entry to the shared code-block rendering pipeline — registered `ICodeBlockPreprocessor` implementations (including tree-sitter `:symbol` fences when `AddPenningtonTreeSitter` is wired), highlighter dispatch via `HighlightingService`, `[!code …]` line transformations, and the standard `code-highlight-wrapper` container. Not registered with Mdazor — markdown authors should use a fenced code block (same pipeline, same output shape) instead.
### Parameters
diff --git a/docs/Pennington.Docs/Content/tutorials/beyond-basics/connect-roslyn.md b/docs/Pennington.Docs/Content/tutorials/beyond-basics/connect-roslyn.md
deleted file mode 100644
index 1e249361..00000000
--- a/docs/Pennington.Docs/Content/tutorials/beyond-basics/connect-roslyn.md
+++ /dev/null
@@ -1,287 +0,0 @@
----
-title: "Connect to a Roslyn solution for live API snippets"
-description: "Point Pennington at a .sln, pull method and class source into markdown with xmldocid fences, and watch hot reload refresh snippets when the source changes."
-sectionLabel: Beyond the Basics
-order: 104020
-tags: [roslyn, api-docs, xmldocid, hot-reload]
-uid: tutorials.beyond-basics.connect-roslyn
----
-
-By the end of this tutorial, your DocSite host loads a sibling C# class library through an inner `.slnx` and renders live `Calculator` and `Greeter` source inside a markdown page via `csharp:xmldocid` fences. You'll see how to register `Pennington.Roslyn`, set `SolutionPath`, write the three fence variants (`:xmldocid`, `:xmldocid,bodyonly`, and multi-symbol), and confirm that hot reload refreshes snippets when the backing source changes.
-
-## Prerequisites
-
-- .NET 11 SDK installed
-- Completed [Scaffold a documentation site with DocSite](xref:tutorials.docsite.scaffold) (or have an equivalent DocSite host ready)
-- A C# project or class library to fence into docs (we'll build a tiny one in unit 1)
-
-The finished code for this tutorial lives in [`examples/BeyondRoslynExample`](https://github.com/usepennington/pennington/tree/main/examples/BeyondRoslynExample).
-
----
-
-## 1. Give your host a sibling library to fence
-
-Before `Pennington.Roslyn` can pull source, there needs to be a `.slnx` listing the project that holds the types to embed. This unit stands up the dual-project shape the rest of the tutorial uses.
-
-
-
-
-**Review the starting DocSite host**
-
-This is the plain DocSite from the scaffold tutorial with no Roslyn wired yet. A `csharp:xmldocid` fence dropped into a markdown page right now renders as a literal code block, because no preprocessor is registered.
-
-```csharp:symbol,bodyonly
-examples/BeyondRoslynExample/Stage1_NoRoslyn.cs > Stage1.Run
-```
-
-
-
-
-**Add a sibling `Sample` class library**
-
-From the host folder, scaffold the class library and add `GenerateDocumentationFile=true` so XmlDocId lookups resolve:
-
-```bash
-dotnet new classlib -n BeyondRoslynExample.Sample -o Sample
-```
-
-Then add this property to the host csproj — without it, the two projects compete over the same `.cs` files and the host build fails with duplicate-compile errors:
-
-```xml
-$(DefaultItemExcludes);Sample\**
-```
-
-`$(DefaultItemExcludes)` preserves the SDK's own excludes (bin/obj); the semicolon-separated `Sample\**` glob adds the sibling library on top.
-
-
-
-
-**Add two small types to fence**
-
-Drop the two source files below into `Sample/`. They are the symbols the rest of the tutorial points at; the XML doc comments are what make them addressable by XmlDocId. (The fences below render from the in-repo example so the page can show what the disk file looks like — they are not yet wired into your local project.)
-
-```csharp:symbol
-examples/BeyondRoslynExample/Sample/Calculator.cs > Calculator
-```
-
-```csharp:symbol
-examples/BeyondRoslynExample/Sample/Greeter.cs > Greeter
-```
-
-
-
-
-**Write an inner `BeyondRoslynExample.slnx`**
-
-Create an inner `.slnx` that registers only the Sample library. `SolutionPath` points at this file rather than the outer repo-level solution, so the MSBuild workspace loads exactly the source to fence into docs.
-
-```xml:symbol
-examples/BeyondRoslynExample/BeyondRoslynExample.slnx
-```
-
-> [!NOTE]
-> On the .NET 11 preview SDK, `dotnet new sln` emits an XML `.slnx` by default. If you prefer the legacy `.sln` format, pass `--format sln`. `SolutionPath` accepts either extension — `Pennington.Roslyn` uses `Microsoft.CodeAnalysis.MSBuild.MSBuildWorkspace`, which opens both.
-
-
-
-
-
-
-- Run `dotnet build` on both csprojs — they compile independently
-- `BeyondRoslynExample.slnx` lives next to the host csproj and lists only `Sample/BeyondRoslynExample.Sample.csproj`
-- Run `dotnet run` on the host — DocSite still serves, nothing has changed in the browser yet
-
-
-
----
-
-## 2. Register `Pennington.Roslyn` and set `SolutionPath`
-
-A single DI call turns on the xmldocid preprocessor. Once `AddPenningtonRoslyn` runs with `SolutionPath` set, every markdown page in the content folder gains the `:xmldocid`, `:xmldocid,bodyonly`, `:xmldocid-diff`, and `:path` fence modifiers.
-
-> [!IMPORTANT]
-> **`Pennington.Roslyn` requires three package references**, not one. `Pennington.Roslyn` itself, `Microsoft.CodeAnalysis.Workspaces.MSBuild`, and `Microsoft.Build.Framework` (with runtime excluded). Skipping either of the last two leaves the MSBuild workspace unable to launch its out-of-process `BuildHost`, and every `csharp:xmldocid` fence renders an error comment instead of source. The full csproj fragment is in the next step.
-
-
-
-
-**Add the three package references**
-
-Add all three to the host csproj. `Pennington.Roslyn` brings in [`SyntaxHighlighter`](xref:reference.api.syntax-highlighter) and [`RoslynCodeBlockPreprocessor`](xref:reference.api.roslyn-code-block-preprocessor); the other two are runtime requirements of `Microsoft.CodeAnalysis.MSBuild.MSBuildWorkspace`.
-
-```xml
-
-
-
-```
-
-`Microsoft.CodeAnalysis.Workspaces.MSBuild` ships the `BuildHost-netcore/` content DLLs the workspace launches at solution-load time. Without it, every `csharp:xmldocid` fence renders an `` comment. The `Microsoft.Build.Framework` reference (with runtime excluded) silences the MSBuild-locator resolution error without changing runtime behaviour.
-
-
-
-
-**Call `AddPenningtonRoslyn`**
-
-Point it at the inner `.slnx`. That's the whole wire-up — no middleware call, no extra endpoint.
-
-```csharp
-builder.Services.AddPenningtonRoslyn(opts =>
- opts.SolutionPath = "path/to/your.slnx");
-```
-
-`SolutionPath` is resolved with `Path.GetFullPath`, so a relative value is interpreted against the **process working directory** — that is, the folder you run `dotnet run` from, which is normally the host csproj folder. The example string `"BeyondRoslynExample.slnx"` works because the inner `.slnx` sits next to the csproj. To point at a sibling folder, use a relative path like `"../OtherProject/Other.slnx"`; an absolute path also works.
-
-For the full `RoslynOptions` surface — including `ProjectFilter`, which narrows the workspace when the `.slnx` lists more than the docs need — see .
-
-The complete stage 2 host adds one `AddPenningtonRoslyn` call to the stage 1 setup; nothing else changes.
-
-```csharp:symbol,bodyonly
-examples/BeyondRoslynExample/Stage2_AddRoslyn.cs > Stage2.Run
-```
-
-
-
-
-
-
-- Run `dotnet run` on the host
-- The first request takes a beat longer while [`ISolutionWorkspaceService`](xref:reference.api.i-solution-workspace-service) loads the inner slnx
-- No errors in the console — the workspace is loaded and ready to resolve XmlDocIds
-
-
-
----
-
-## 3. Write your first `xmldocid` fence
-
-Now that [`RoslynCodeBlockPreprocessor`](xref:reference.api.roslyn-code-block-preprocessor) is registered, any fenced code block whose info string ends in `:xmldocid` has its body parsed as one XmlDocId per line and resolved against the loaded workspace.
-
-
-
-
-**Create a new markdown page**
-
-Add `Content/api-pulls.md` with the front matter and heading below. The next step adds a fence to it.
-
-```markdown
----
-title: API pulls
-description: Live source from the Sample library.
-order: 30
----
-
-# API pulls
-```
-
-
-
-
-**Fence a whole type with `T:`**
-
-The fence language is `csharp:xmldocid`. The body is a single XmlDocId — `T:` for a type, `M:` for a method, `P:` for a property, `F:` for a field.
-
-```csharp:symbol
-examples/BeyondRoslynExample/Sample/Calculator.cs > Calculator
-```
-
-
-
-
-**Fence a single method with `M:`**
-
-Method XmlDocIds include full parameter types. The Sample library's `Add` method takes two `int` parameters, so the XmlDocId reads `M:...Add(System.Int32,System.Int32)`.
-
-```csharp:symbol
-examples/BeyondRoslynExample/Sample/Calculator.cs > Calculator.Add
-```
-
-
-
-
-
-
-- Run `dotnet run` and visit `http://localhost:5000/api-pulls`
-- The `Calculator` class and the `Add` method render as syntax-highlighted C#, pulled directly from `Sample/Calculator.cs`
-- Right-click → View Source: the markup is real `` with TextMate-style token spans, not an image
-
-
-
----
-
-## 4. Watch hot reload refresh the snippet
-
-The workspace re-reads source on change. Edit the fenced method, request the page again, and Pennington serves the updated snippet without a manual rebuild.
-
-
-
-
-**Start the host in watch mode**
-
-Run `dotnet watch` on the host csproj so file changes trigger a reload of the MSBuild workspace. Leave the browser open on `/api-pulls`.
-
-
-
-
-**Edit the Sample library**
-
-Change the body of `Add` in `Sample/Calculator.cs` — add a comment or rename a local variable. Save the file.
-
-
-
-
-
-
-- Refresh `/api-pulls`
-- The `Add` method snippet now shows the change, pulled fresh from `Calculator.cs`
-- No manual docs rebuild was required — the workspace picked it up
-
-
-
----
-
-## 5. Use the `,bodyonly` variant and stack multiple symbols
-
-Two fence options let you control what renders: append `,bodyonly` to strip the declaration line, or list multiple XmlDocIds in one fence to concatenate their source.
-
-
-
-
-**Strip the declaration with `,bodyonly`**
-
-Appending `,bodyonly` to the fence language returns only the block contents, or the expression-body expression for arrow members. Use it when the declaration is noise and the snippet should show what happens inside.
-
-```csharp:symbol,bodyonly
-examples/BeyondRoslynExample/Sample/Calculator.cs > Calculator.Multiply
-```
-
-
-
-
-**Concatenate multiple XmlDocIds**
-
-Place multiple XmlDocIds in one fence, one per line. The preprocessor renders them all in the order listed — useful for pairing two related members in the same code block.
-
-```csharp:symbol
-examples/BeyondRoslynExample/Sample/Greeter.cs > Greeter.Greet
-examples/BeyondRoslynExample/Sample/Calculator.cs > Calculator.Mean
-```
-
-
-
-
-
-
-- Refresh `/api-pulls`
-- The `Multiply` fence shows the `return a * b;` line only — no `public int Multiply(...)` declaration
-- The concatenated fence shows `Greet` and `Mean` back-to-back in one highlighted code block
-
-
-
----
-
-## Summary
-
-- A dual-project shape now stands up — a DocSite host plus a sibling Sample library wired through an inner slnx.
-- `Pennington.Roslyn` is active via a single `AddPenningtonRoslyn` call and `RoslynOptions.SolutionPath`.
-- `csharp:xmldocid` fences cover types (`T:`), methods (`M:`), body-only snippets (`,bodyonly`), and multi-symbol blocks.
-- Hot reload refreshes rendered snippets when the backing source changes.
diff --git a/docs/Pennington.Docs/Content/tutorials/getting-started/navigation.md b/docs/Pennington.Docs/Content/tutorials/getting-started/navigation.md
index ee9bb5e5..2686609a 100644
--- a/docs/Pennington.Docs/Content/tutorials/getting-started/navigation.md
+++ b/docs/Pennington.Docs/Content/tutorials/getting-started/navigation.md
@@ -120,4 +120,4 @@ Run `dotnet run` and open `http://localhost:5000/`.
- A folder without an `index.md` becomes a section node — that is why `guides/` rendered as a labelled group.
- The bare host now serves a complete site: a content pipeline, a styled layout, and navigation — all on `AddPennington`.
-That is the whole getting-started arc. `AddPennington` is the path when you want full control — you wire the pipeline, the layout, and the navigation yourself, and you have now done each part. If you do not need that level of control, the [DocSite](xref:tutorials.docsite.scaffold) and [BlogSite](xref:tutorials.blogsite.scaffold) templates package this same wiring — layout, navigation, search — into a single call for two specific shapes; reach for one when your site *is* a documentation site or a blog. [Connect a Roslyn project](xref:tutorials.beyond-basics.connect-roslyn) and the other [beyond-basics tutorials](xref:tutorials.beyond-basics.custom-razor-component) build on the host you just finished.
+That is the whole getting-started arc. `AddPennington` is the path when you want full control — you wire the pipeline, the layout, and the navigation yourself, and you have now done each part. If you do not need that level of control, the [DocSite](xref:tutorials.docsite.scaffold) and [BlogSite](xref:tutorials.blogsite.scaffold) templates package this same wiring — layout, navigation, search — into a single call for two specific shapes; reach for one when your site *is* a documentation site or a blog. The [beyond-basics tutorials](xref:tutorials.beyond-basics.custom-razor-component) build on the host you just finished.
diff --git a/docs/Pennington.Docs/Pennington.Docs.csproj b/docs/Pennington.Docs/Pennington.Docs.csproj
index 31c5ba90..f22ed240 100644
--- a/docs/Pennington.Docs/Pennington.Docs.csproj
+++ b/docs/Pennington.Docs/Pennington.Docs.csproj
@@ -12,7 +12,11 @@
-
+
+
+
diff --git a/docs/Pennington.Docs/Program.cs b/docs/Pennington.Docs/Program.cs
index 96c9465e..177333b5 100644
--- a/docs/Pennington.Docs/Program.cs
+++ b/docs/Pennington.Docs/Program.cs
@@ -1,4 +1,5 @@
using Mdazor;
+using Pennington.ApiMetadata.Reflection;
using Pennington.Docs;
using Pennington.Docs.ApiReference;
using Pennington.Docs.Components.Reference;
@@ -6,8 +7,6 @@
using Pennington.DocSite.Api;
using Pennington.Infrastructure;
using Pennington.MonorailCss;
-using Pennington.Roslyn;
-using Pennington.Roslyn.ApiMetadata;
using Pennington.TreeSitter;
var builder = WebApplication.CreateBuilder(args);
@@ -94,37 +93,20 @@
treeSitter.ContentRoot = "../..";
});
-// Roslyn stays for the reflection-backed API reference only, scoped to a .slnf that lists just
-// the Pennington.* projects — so the workspace loads ~13 projects instead of the whole solution.
-// EnableCodeFragmentFences is off because tree-sitter now owns :symbol extraction.
-builder.Services.AddPenningtonRoslyn(roslyn =>
+// Reflection-backed API metadata: read the built Pennington.* assemblies and their companion
+// xmldoc files straight from this app's output folder — no MSBuild workspace, no compilation.
+// Every referenced Pennington library lands in bin/, so glob them (excluding this entry app)
+// to document the full set of referenced Pennington libraries.
+var entryAssembly = System.Reflection.Assembly.GetEntryAssembly()?.GetName().Name;
+builder.Services.AddApiMetadataFromCompiledAssembly(opts =>
{
- roslyn.SolutionPath = "../../Pennington.slnf";
- roslyn.EnableCodeFragmentFences = false;
-});
-
-// Roslyn-backed API metadata: enumerate public types from the live Pennington
-// workspace. Scope to Pennington.* projects so example apps in Pennington.slnx
-// don't leak into /reference/api/.
-var defaultApiFilter = ApiReferenceOptions.DefaultProjectFilter();
-builder.Services.AddApiMetadataFromRoslyn(configure: opts =>
-{
- opts.ProjectFilter = project =>
+ foreach (var dll in Directory.EnumerateFiles(AppContext.BaseDirectory, "Pennington*.dll"))
{
- if (!defaultApiFilter(project))
+ if (!string.Equals(Path.GetFileNameWithoutExtension(dll), entryAssembly, StringComparison.Ordinal))
{
- return false;
+ opts.AssemblyFiles.Add(dll);
}
-
- var name = project.Name;
- var paren = name.IndexOf('(');
- if (paren > 0)
- {
- name = name[..paren];
- }
-
- return name == "Pennington" || name.StartsWith("Pennington.", StringComparison.Ordinal);
- };
+ }
});
// Auto-publishes /reference/api/{slug}/ pages and registers the reference
diff --git a/examples/AUDIT_LOG.md b/examples/AUDIT_LOG.md
index 29b1144f..3c0b23a1 100644
--- a/examples/AUDIT_LOG.md
+++ b/examples/AUDIT_LOG.md
@@ -538,32 +538,6 @@ All 25 examples build and run. Highlights worth triaging:
**Fixes applied.**
-## 4. BeyondRoslynExample
-
-**README claim:** `AddPenningtonRoslyn` against the sibling `Sample/` library. Markdown fences resolve `:xmldocid`, `:xmldocid,bodyonly`, `:xmldocid-diff`, and `:path` against the inner `BeyondRoslynExample.slnx`.
-
-**Verified in browser (`dotnet run`, port 5000):**
-- Symbol warmup logs `Symbol extraction warmup completed in 1809ms`. ✓
-- `/api-pulls/` renders 5 distinct code blocks:
- 1. `T:Calculator` — full class with xmldoc comments. ✓
- 2. `M:Calculator.Add(...)` — method with xmldoc. ✓
- 3. `M:Calculator.Multiply(...)` + `,bodyonly` — single line `return a * b;`. ✓ (correctly strips declaration)
- 4. `T:Greeter` — full class with xmldoc. ✓
- 5. Two M-ids in one fence — both members concatenated. ✓
-- Console: clean. ✓
-
-**Findings:**
-- **[DOC+APP] (minor)** README explicitly advertises `:xmldocid-diff` as part of the teaching surface but no markdown in this example uses it. Tutorial body (line 88) also lists `:xmldocid-diff` in the fence-modifier menu, then never demonstrates it. Either add a section in `api-pulls.md` showing a before/after with two `M:` IDs and the diff fence, or drop `:xmldocid-diff` from both the README and the tutorial fence-modifier list and surface it in an explanation-quadrant page instead.
-- **[DOC] (minor)** README "Tutorial stages" section is a placeholder: it says only "The inner `BeyondRoslynExample.slnx` + `Sample/` library is part of the teaching surface." but the example has actual staged C# files (`Stage1_NoRoslyn.cs`, `Stage2_AddRoslyn.cs`). The standard pattern from `examples/CLAUDE.md` is `Stage1 → Stage2 → …` — call them out by name in the README so consumers know to look for them.
-- **[DOC] (minor)** README's "Concepts" list mentions `` keeping `Sample/` out of the host's compile — this is true but the tutorial buries it as a one-liner in step 1.2 ("set `DefaultItemExcludes` … otherwise the two projects compete over the same `.cs` files"). Promote it: a reader copying the csproj fragment from the tutorial today won't see the `` line, only a generic instruction to add it.
-
-**Resolved 2026-05-13:**
-- DOC+APP `:xmldocid-diff` demo — added a "Diff two symbols" section in `examples/BeyondRoslynExample/Content/api-pulls.md` that fences `:xmldocid-diff,bodyonly` against `Calculator.Add` vs `Calculator.Multiply`. Verified the rendered page shows `return a + b;` / `return a * b;` with full syntax highlighting.
-- DOC Tutorial stages placeholder — README now reads `Stage1_NoRoslyn.cs → Stage2_AddRoslyn.cs.` per the `examples/CLAUDE.md` convention, plus a one-line note on what the inner slnx and `Sample/` library teach.
-- DOC `` promotion — Step 1.2 of the tutorial now ships a real `` snippet showing the exact `$(DefaultItemExcludes);Sample\** ` line, with one sentence explaining why `$(DefaultItemExcludes)` is preserved.
-
-**Fixes applied.**
-
## 3. BeyondLocaleExample
**README claim:** DocSite + `ConfigureLocalization` adds a second URL-prefixed locale (`es`); content lives under `Content//`; `LanguageSwitcher` appears once `Locales.Count > 1`; translations registered via `ConfigurePennington` escape hatch.
diff --git a/examples/BeyondRoslynExample/BeyondRoslynExample.csproj b/examples/BeyondRoslynExample/BeyondRoslynExample.csproj
deleted file mode 100644
index 3115ce5d..00000000
--- a/examples/BeyondRoslynExample/BeyondRoslynExample.csproj
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
- net11.0
- preview
- enable
- enable
-
- $(DefaultItemExcludes);Sample\**
-
-
-
-
-
-
-
-
-
diff --git a/examples/BeyondRoslynExample/BeyondRoslynExample.slnx b/examples/BeyondRoslynExample/BeyondRoslynExample.slnx
deleted file mode 100644
index 01bc711d..00000000
--- a/examples/BeyondRoslynExample/BeyondRoslynExample.slnx
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/examples/BeyondRoslynExample/Components/CodeBlockRazorPage.razor b/examples/BeyondRoslynExample/Components/CodeBlockRazorPage.razor
deleted file mode 100644
index fd39c80d..00000000
--- a/examples/BeyondRoslynExample/Components/CodeBlockRazorPage.razor
+++ /dev/null
@@ -1,25 +0,0 @@
-@page "/codeblock-razor"
-@namespace BeyondRoslynExample.Components
-
-CodeBlock razor repro
-
-
- CodeBlock razor repro
-
- Four <CodeBlock> invocations authored in a real
- Razor @@page. Confirms that the Razor-side component
- runs the Roslyn preprocessor when a modifier is present.
-
-
- Whole type via Code
-
-
- Body only via ChildContent
- M:BeyondRoslynExample.Sample.Calculator.Multiply(System.Int32,System.Int32)
-
- Plain csharp sentinel
- var x = 1;
-
- Method with declaration via ChildContent
- M:BeyondRoslynExample.Sample.Calculator.Add(System.Int32,System.Int32)
-
diff --git a/examples/BeyondRoslynExample/Components/_Imports.razor b/examples/BeyondRoslynExample/Components/_Imports.razor
deleted file mode 100644
index 8bdcfd7d..00000000
--- a/examples/BeyondRoslynExample/Components/_Imports.razor
+++ /dev/null
@@ -1,2 +0,0 @@
-@using Microsoft.AspNetCore.Components.Web
-@using Pennington.UI.Components
diff --git a/examples/BeyondRoslynExample/Content/api-pulls.md b/examples/BeyondRoslynExample/Content/api-pulls.md
deleted file mode 100644
index c5f50524..00000000
--- a/examples/BeyondRoslynExample/Content/api-pulls.md
+++ /dev/null
@@ -1,72 +0,0 @@
----
-title: API pulls
-description: Live xmldocid fences pulling real source from the Sample library.
-order: 20
----
-
-# API pulls
-
-Each fenced block below resolves an XmlDocId against the inner slnx that
-`AddPenningtonRoslyn` loaded. When the Sample library source changes on disk,
-hot reload re-reads it and the next request serves the new snippet.
-
-## Whole type — `T:...`
-
-Embed the entire `Calculator` class. Fence language is `csharp:xmldocid`;
-the fence body is a single line holding the XmlDocId.
-
-```csharp:xmldocid
-T:BeyondRoslynExample.Sample.Calculator
-```
-
-## Single method — `M:...`
-
-One fence, one method. XmlDocIds for methods include parameter types.
-
-```csharp:xmldocid
-M:BeyondRoslynExample.Sample.Calculator.Add(System.Int32,System.Int32)
-```
-
-## Method body only — `,bodyonly`
-
-Appending `,bodyonly` strips the declaration and returns just the block
-contents (or expression-body expression).
-
-```csharp:xmldocid,bodyonly
-M:BeyondRoslynExample.Sample.Calculator.Multiply(System.Int32,System.Int32)
-```
-
-## A second type
-
-The `Greeter` class lives in the same Sample library.
-
-```csharp:xmldocid
-T:BeyondRoslynExample.Sample.Greeter
-```
-
-## Multiple symbols in one fence
-
-List multiple XmlDocIds on separate lines inside one fence — they're
-concatenated in the rendered output.
-
-```csharp:xmldocid
-M:BeyondRoslynExample.Sample.Greeter.Greet(System.String)
-M:BeyondRoslynExample.Sample.Calculator.Mean(System.Collections.Generic.IReadOnlyList{System.Int32})
-```
-
-## Diff two symbols — `:xmldocid-diff`
-
-The `:xmldocid-diff` fence takes exactly two XmlDocIds and renders the
-unified diff between their bodies. Use it in explanation-quadrant pages to
-compare a "before" and "after" implementation side by side without keeping
-two prose snippets in sync. Here we diff `Add` against `Multiply` — both
-are two-parameter `int` methods, so the diff narrows to the operator and
-parameter names. Stack the same `,bodyonly` suffix to strip the
-declarations when the change you want to surface is purely inside the
-method body.
-
-```csharp:xmldocid-diff,bodyonly
-M:BeyondRoslynExample.Sample.Calculator.Add(System.Int32,System.Int32)
-M:BeyondRoslynExample.Sample.Calculator.Multiply(System.Int32,System.Int32)
-```
-
diff --git a/examples/BeyondRoslynExample/Content/index.md b/examples/BeyondRoslynExample/Content/index.md
deleted file mode 100644
index 899d5842..00000000
--- a/examples/BeyondRoslynExample/Content/index.md
+++ /dev/null
@@ -1,23 +0,0 @@
----
-title: Beyond Roslyn
-description: How this tutorial pulls live source into rendered docs.
-order: 10
----
-
-# Beyond Roslyn
-
-This example backs tutorial §1.4.20 of the Pennington docs. It points
-`Pennington.Roslyn` at a sibling library (`BeyondRoslynExample.Sample`) via
-the inner `BeyondRoslynExample.slnx` and then embeds symbols from that
-library directly into markdown pages with `csharp:xmldocid` fences.
-
-See the [API pulls page](./api-pulls) for the fences in action.
-
-## What's wired
-
-- `AddDocSite(...)` — the same DocSite host used in tutorials 1.2.*
-- `AddPenningtonRoslyn(options => options.SolutionPath = "BeyondRoslynExample.slnx")`
- — turns on `:xmldocid` / `:xmldocid,bodyonly` / `:path` code-fence modifiers
-- `BeyondRoslynExample.slnx` — inner slnx that registers only the Sample
- library, scoping the MSBuild workspace to exactly the source we want to
- fence into docs
diff --git a/examples/BeyondRoslynExample/Program.cs b/examples/BeyondRoslynExample/Program.cs
deleted file mode 100644
index 4b397aa0..00000000
--- a/examples/BeyondRoslynExample/Program.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-using Pennington.DocSite;
-using Pennington.Roslyn;
-
-var builder = WebApplication.CreateBuilder(args);
-
-// Same DocSite host shape as the previous tutorials — the difference here is
-// the extra `AddPenningtonRoslyn` line below, which lights up `:xmldocid`,
-// `:xmldocid,bodyonly`, `:xmldocid-diff`, and `:path` code-fence modifiers.
-// With those wired, doc markdown can reference real types and methods by
-// their XmlDocId and Pennington will pull the current source straight into
-// the rendered page.
-builder.Services.AddDocSite(() => new DocSiteOptions
-{
- SiteTitle = "Beyond Roslyn",
- Description = "Pulling live code snippets into docs with xmldocid fences.",
- GitHubUrl = "https://github.com/usepennington/pennington",
- HeaderContent = """Beyond Roslyn""",
- FooterContent = """""",
- AdditionalRoutingAssemblies = [typeof(Program).Assembly],
-});
-
-// Point Pennington.Roslyn at the *inner* slnx next to this Program.cs. The
-// path is resolved relative to the host's working directory (the folder that
-// contains this csproj when `dotnet run` is invoked), so "BeyondRoslynExample.slnx"
-// is enough — no need to walk up to the repo root. The inner slnx registers
-// only the `Sample` library whose types the markdown fences reference.
-//
-// `AddPenningtonRoslyn` takes a `RoslynOptions` action. When `SolutionPath` is
-// set it registers `RoslynCodeBlockPreprocessor`, the MSBuild workspace, the
-// symbol-extraction service, and the xmldoc-HTML renderer — everything needed
-// for `csharp:xmldocid` fences to resolve.
-builder.Services.AddPenningtonRoslyn(roslyn =>
-{
- roslyn.SolutionPath = "BeyondRoslynExample.slnx";
-});
-
-var app = builder.Build();
-
-app.UseDocSite();
-
-await app.RunDocSiteAsync(args);
\ No newline at end of file
diff --git a/examples/BeyondRoslynExample/README.md b/examples/BeyondRoslynExample/README.md
deleted file mode 100644
index 75dd6f12..00000000
--- a/examples/BeyondRoslynExample/README.md
+++ /dev/null
@@ -1,19 +0,0 @@
-# BeyondRoslynExample
-
-Pulling live code into docs with `:xmldocid`, `:xmldocid,bodyonly`, `:xmldocid-diff`, and `:path` fence modifiers. `AddPenningtonRoslyn` points an MSBuild workspace at the sibling `Sample/` library; markdown fences resolve real symbols from there.
-
-## Concepts
-
-- `AddPenningtonRoslyn` (MSBuild workspace, symbol extraction, xmldoc → HTML)
-- `RoslynOptions.SolutionPath` — relative to the host's working directory
-- `` in the csproj keeping `Sample/` out of the host's compile
-
-## Tutorial stages
-
-`Stage1_NoRoslyn.cs` → `Stage2_AddRoslyn.cs`.
-
-The inner `BeyondRoslynExample.slnx` plus the `Sample/` class library are part of the teaching surface — the tutorial pulls `T:BeyondRoslynExample.Sample.Calculator` and `M:...Greeter.Greet(System.String)` through `csharp:xmldocid` fences from those projects.
-
-## Referenced from
-
-- `docs/.../tutorials/beyond-basics/connect-roslyn.md`
diff --git a/examples/BeyondRoslynExample/Sample/BeyondRoslynExample.Sample.csproj b/examples/BeyondRoslynExample/Sample/BeyondRoslynExample.Sample.csproj
deleted file mode 100644
index 1f5de283..00000000
--- a/examples/BeyondRoslynExample/Sample/BeyondRoslynExample.Sample.csproj
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
- net11.0
- preview
- enable
- enable
- true
- $(NoWarn);CS1591
- BeyondRoslynExample.Sample
-
-
diff --git a/examples/BeyondRoslynExample/Sample/Calculator.cs b/examples/BeyondRoslynExample/Sample/Calculator.cs
deleted file mode 100644
index 3123dc6f..00000000
--- a/examples/BeyondRoslynExample/Sample/Calculator.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-namespace BeyondRoslynExample.Sample;
-
-///
-/// A tiny arithmetic helper used as the tutorial's xmldocid target. Nothing
-/// about this class is clever — the point is that the tutorial's doc prose
-/// can fence M:BeyondRoslynExample.Sample.Calculator.Add(System.Int32,System.Int32)
-/// and pull the real source into rendered HTML.
-///
-public sealed class Calculator
-{
- /// Adds two integers.
- /// First addend.
- /// Second addend.
- /// The sum of and .
- public int Add(int a, int b)
- {
- return a + b;
- }
-
- /// Multiplies two integers.
- /// First factor.
- /// Second factor.
- /// The product of and .
- public int Multiply(int a, int b)
- {
- return a * b;
- }
-
- /// Returns the arithmetic mean of a non-empty sequence.
- /// Values to average. Must contain at least one element.
- /// Thrown if is empty.
- public double Mean(IReadOnlyList values)
- {
- if (values.Count == 0)
- {
- throw new ArgumentException("At least one value is required.", nameof(values));
- }
-
- var total = 0L;
- foreach (var v in values)
- {
- total += v;
- }
-
- return (double)total / values.Count;
- }
-}
\ No newline at end of file
diff --git a/examples/BeyondRoslynExample/Sample/Greeter.cs b/examples/BeyondRoslynExample/Sample/Greeter.cs
deleted file mode 100644
index 51be60dc..00000000
--- a/examples/BeyondRoslynExample/Sample/Greeter.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-namespace BeyondRoslynExample.Sample;
-
-///
-/// Builds friendly greetings. Exists so the tutorial's second xmldocid fence
-/// can reference a type other than .
-///
-public sealed class Greeter
-{
- /// The greeting prefix, e.g. "Hello" or "Bonjour" .
- public string Prefix { get; }
-
- /// Creates a greeter with the supplied .
- public Greeter(string prefix)
- {
- Prefix = prefix;
- }
-
- ///
- /// Builds a greeting for using .
- ///
- /// The recipient's display name.
- /// A string of the form "{Prefix}, {name}! ".
- public string Greet(string name)
- {
- return $"{Prefix}, {name}!";
- }
-}
\ No newline at end of file
diff --git a/examples/BeyondRoslynExample/Stage1_NoRoslyn.cs b/examples/BeyondRoslynExample/Stage1_NoRoslyn.cs
deleted file mode 100644
index b86947fe..00000000
--- a/examples/BeyondRoslynExample/Stage1_NoRoslyn.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-namespace BeyondRoslynExample;
-
-using Pennington.DocSite;
-
-///
-/// Stage 1 — the pre-Roslyn host. DocSite is wired, pages render, but any
-/// csharp:xmldocid fence in markdown just renders as a literal code
-/// block because no /
-///
-/// is registered in DI. Tutorial prose extracts the body of
-/// via xmldocid,bodyonly . This class is never instantiated.
-///
-public static class Stage1
-{
- /// A DocSite host with no Roslyn integration yet.
- public static async Task Run(string[] args)
- {
- var builder = WebApplication.CreateBuilder(args);
-
- builder.Services.AddDocSite(() => new DocSiteOptions
- {
- SiteTitle = "Beyond Roslyn",
- Description = "Pulling live code snippets into docs with xmldocid fences.",
- GitHubUrl = "https://github.com/usepennington/pennington",
- HeaderContent = """Beyond Roslyn""",
- FooterContent = """""",
- });
-
- var app = builder.Build();
-
- app.UseDocSite();
-
- await app.RunDocSiteAsync(args);
- }
-}
\ No newline at end of file
diff --git a/examples/BeyondRoslynExample/Stage2_AddRoslyn.cs b/examples/BeyondRoslynExample/Stage2_AddRoslyn.cs
deleted file mode 100644
index d2a0873c..00000000
--- a/examples/BeyondRoslynExample/Stage2_AddRoslyn.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-namespace BeyondRoslynExample;
-
-using Pennington.DocSite;
-using Pennington.Roslyn; // [!code ++]
-
-///
-/// Stage 2 — adds AddPenningtonRoslyn pointed at the inner
-/// BeyondRoslynExample.slnx . With this one extra service registration,
-/// the markdown preprocessor for :xmldocid / :xmldocid,bodyonly
-/// / :xmldocid-diff / :path fence modifiers lights up and every
-/// doc page that references a Sample-library symbol starts rendering real
-/// source. Tutorial prose extracts the body of via
-/// xmldocid,bodyonly . This class is never instantiated.
-///
-public static class Stage2
-{
- /// DocSite host with Roslyn integration wired.
- public static async Task Run(string[] args)
- {
- var builder = WebApplication.CreateBuilder(args);
-
- builder.Services.AddDocSite(() => new DocSiteOptions
- {
- SiteTitle = "Beyond Roslyn",
- Description = "Pulling live code snippets into docs with xmldocid fences.",
- GitHubUrl = "https://github.com/usepennington/pennington",
- HeaderContent = """Beyond Roslyn""",
- FooterContent = """""",
- });
-
- builder.Services.AddPenningtonRoslyn(roslyn => // [!code ++]
- { // [!code ++]
- roslyn.SolutionPath = "BeyondRoslynExample.slnx"; // [!code ++]
- }); // [!code ++]
-
- var app = builder.Build();
-
- app.UseDocSite();
-
- await app.RunDocSiteAsync(args);
- }
-}
\ No newline at end of file
diff --git a/examples/BeyondTreeSitterExample/Content/index.md b/examples/BeyondTreeSitterExample/Content/index.md
index ff953adf..79996106 100644
--- a/examples/BeyondTreeSitterExample/Content/index.md
+++ b/examples/BeyondTreeSitterExample/Content/index.md
@@ -4,9 +4,9 @@ title: Beyond Tree-sitter
# Multi-language snippets with `:symbol`
-`Pennington.Roslyn` can pull C#/VB declarations into docs by their XmlDocId.
-`Pennington.TreeSitter` does the same idea for **any** tree-sitter-supported
-language, addressing a declaration by its **name path** (`Type.Member`).
+`Pennington.TreeSitter` pulls declarations into docs for **any**
+tree-sitter-supported language, addressing a declaration by its **name path**
+(`Type.Member`).
Each fenced block uses the `:symbol` info-string. The body is one
` > ` reference per line, resolved under the configured
diff --git a/examples/BeyondTreeSitterExample/Program.cs b/examples/BeyondTreeSitterExample/Program.cs
index 4ef35670..07b2fc36 100644
--- a/examples/BeyondTreeSitterExample/Program.cs
+++ b/examples/BeyondTreeSitterExample/Program.cs
@@ -6,8 +6,8 @@
// Same DocSite host shape as the other tutorials. The extra line is
// `AddPenningtonTreeSitter` below, which lights up the `:symbol` code-fence
// modifier for *any* tree-sitter-supported language (Python, Rust, Go,
-// TypeScript, …). Where `Pennington.Roslyn` resolves C#/VB by XmlDocId,
-// tree-sitter resolves a member by name path across many languages.
+// TypeScript, …). Tree-sitter resolves a member by name path across many
+// languages.
builder.Services.AddDocSite(() => new DocSiteOptions
{
SiteTitle = "Beyond Tree-sitter",
diff --git a/examples/BeyondTreeSitterExample/README.md b/examples/BeyondTreeSitterExample/README.md
index 101b2c71..5488ad36 100644
--- a/examples/BeyondTreeSitterExample/README.md
+++ b/examples/BeyondTreeSitterExample/README.md
@@ -7,12 +7,11 @@ doc markdown via the `:symbol` fence modifier.
- `AddPenningtonTreeSitter(o => o.ContentRoot = "Samples")` lights up the
`:symbol` fence for every tree-sitter-supported language.
-- Addressing a declaration by **name path** (`Type.Member`) rather than by
- XmlDocId, so non-C# languages work the same way C# does under `Pennington.Roslyn`.
+- Addressing a declaration by **name path** (`Type.Member`), which works
+ uniformly across languages.
- The fence body format: ` > ` (one per line); a bare
`` embeds the whole file; `,bodyonly` returns just the body.
-- `:symbol-diff` over two references emits a before/after unified diff,
- the multi-language counterpart to `Pennington.Roslyn`'s `:xmldocid-diff`.
+- `:symbol-diff` over two references emits a before/after unified diff.
- Cross-language resolution quirks: Rust methods resolve through their
`impl` block; Go/TypeScript/Python all work from the same generic resolver.
@@ -31,6 +30,5 @@ dotnet run --project examples/BeyondTreeSitterExample
Where it is referenced from the docs site: _(reference for `Pennington.TreeSitter`)_.
-> The `:symbol` modifier is multi-language and syntactic. For C#/VB with full
-> semantic resolution (XmlDocId, `inheritdoc`, required usings), use
-> `Pennington.Roslyn` and its `:xmldocid` fence instead.
+> The `:symbol` modifier is multi-language and syntactic — it matches
+> declarations by name path, without semantic or type resolution.
diff --git a/examples/CLAUDE.md b/examples/CLAUDE.md
index 88822924..560c7a9c 100644
--- a/examples/CLAUDE.md
+++ b/examples/CLAUDE.md
@@ -3,7 +3,7 @@
## Naming
All example projects suffix `Example`. Categories:
- `GettingStarted*` — baseline Pennington setup, used by tutorials
-- `Beyond*` — advanced features (Roslyn, locales, custom Razor components)
+- `Beyond*` — advanced features (tree-sitter fragments, locales, custom Razor components)
- `DocSite*` / `BlogSite*` — template-specific (scaffold, kitchen sink, sections, authors)
- `Focused*` / `Multiple*` — feature-focused (code samples, multi-source)
@@ -30,24 +30,22 @@ Every example folder has a `README.md` describing its purpose, the concepts it t
| `BareHostRazorPageExample` | Render a Razor component as a full response on a bare `AddPennington` host via `HtmlRenderer` + `MapGet`. | `how-to/response-pipeline/razor-page-on-bare-host.md` |
| `BareHostSearchExample` | Light up the Pennington.UI search modal on a bare (non-DocSite) host — reference `Pennington.UI`, load `dewey-search.js` + `scripts.js`, add an `id="search-input"` trigger; styled by the `PenningtonApplies` safelist. Mounts the `_shared/Bramble` corpus (blog excluded). | `how-to/discovery/search-on-a-bare-host.md` |
| `BeyondCustomRazorComponentExample` | Author a Razor component (`PricingCard`) and register it with Mdazor via `AddMdazorComponent()`. | `tutorials/beyond-basics/custom-razor-component.md` |
-| `BeyondLocaleExample` | Add a second URL-prefixed locale to a DocSite via `ConfigureLocalization` + `Content//`. | `tutorials/beyond-basics/add-a-locale.md`, `how-to/discovery/localization.md` |
-| `BeyondRoslynExample` | `AddPenningtonRoslyn` against a sibling slnx — markdown fences resolve `:xmldocid` / `:xmldocid,bodyonly` / `:xmldocid-diff` / `:path`. | `tutorials/beyond-basics/connect-roslyn.md` |
-| `BeyondTreeSitterExample` | `AddPenningtonTreeSitter` against a `Samples/` folder — `:symbol` fences extract declarations by name path (`Type.Member`) across Python/Rust/Go/TypeScript, plus `,bodyonly` and whole-file forms. | _(reference for `Pennington.TreeSitter`)_ |
+| `BeyondLocaleExample` | Add a second URL-prefixed locale to a DocSite via `ConfigureLocalization` + `Content//`. | `tutorials/beyond-basics/add-a-locale.md`, `how-to/discovery/localization.md` || `BeyondTreeSitterExample` | `AddPenningtonTreeSitter` against a `Samples/` folder — `:symbol` fences extract declarations by name path (`Type.Member`) across Python/Rust/Go/TypeScript, plus `,bodyonly` and whole-file forms. | _(reference for `Pennington.TreeSitter`)_ |
| `BeyondTranslationAuditExample` | Wire `AddPenningtonTranslationAudit` so missing translations surface in the dev overlay and build report. | _(reference for `Pennington.TranslationAudit`)_ |
| `BeyondTuiExample` | Opt the host into the dev-time TUI dashboard via `AddPenningtonTui`. Build mode no-ops. | _(reference for `Pennington.Tui`)_ |
-| `BlogKitchenSinkExample` | Wide BlogSite configuration (hero, projects, socials, RSS, sitemap, JSON-LD) split across helpers for xmldocid fencing. | `how-to/feeds/rss.md`, `how-to/feeds/sitemap.md` |
+| `BlogKitchenSinkExample` | Wide BlogSite configuration (hero, projects, socials, RSS, sitemap, JSON-LD) split across helpers for symbol fencing. | `how-to/feeds/rss.md`, `how-to/feeds/sitemap.md` |
| `BlogSiteFirstPostExample` | Extend the BlogSite scaffold with a fully populated post exercising every `BlogSiteFrontMatter` field. | `tutorials/blogsite/first-post.md`, `reference/front-matter/keys.md` |
| `BlogSiteHeroProjectsSocialsExample` | Populate `HeroContent`, `MyWork`, `Socials`, and `MainSiteLinks` with the built-in `SocialIcons` fragments. | `tutorials/blogsite/hero-projects-socials.md`, `how-to/feeds/blogsite-homepage.md` |
| `BlogSiteScaffoldExample` | Smallest BlogSite — `AddBlogSite` / `UseBlogSite` / `RunBlogSiteAsync` with one post under `Content/Blog/`. | `tutorials/blogsite/scaffold.md`, `reference/blogsite/routes.md` |
| `DocSiteBlogExample` | DocSite with a `Content/blog/` folder — the folder convention activates the blog index, post pages, `/blog/tags/` pages, the "Blog" header link, and `/rss.xml` with no `Program.cs` wiring. | `tutorials/docsite/add-a-blog.md` |
| `DocSitePagesAndLinksExample` | Single-area DocSite with two content pages (`install`, `configure`) and a hub `index` demonstrating relative, absolute, and `uid:`-based linking, plus `Components/Index.razor` — a Razor landing page routed at `/` with `FullWidthLayout`. `snippets/markdown-{alert,tabs}-example.md` back the alerts/tabs sections of the markdown reference. | `tutorials/docsite/first-doc-page.md`, `tutorials/docsite/landing-page.md`, `reference/markdown/extensions.md` |
| `DocSiteChromeOverridesExample` | Override DocSite chrome via `DocSiteOptions` + head-slot fragment + custom routed `@page` + `AdditionalRoutingAssemblies`. | `how-to/response-pipeline/override-docsite-components.md` |
-| `DocSiteKitchenSinkExample` | Wide DocSite configuration (areas, locales, theming, fonts, custom front matter, custom Mdazor component) split across helpers for xmldocid fencing. | `how-to/navigation/{customize-sidebar,cross-references}.md`, `how-to/theming/{monorail-css,fonts}.md`, `how-to/pages/{front-matter,redirects}.md`, `how-to/discovery/{search,multiple-sources}.md`, `how-to/feeds/llms-txt.md`, `how-to/rich-content/ui-components-in-markdown.md`, `reference/front-matter/keys.md` |
+| `DocSiteKitchenSinkExample` | Wide DocSite configuration (areas, locales, theming, fonts, custom front matter, custom Mdazor component) split across helpers for symbol fencing. | `how-to/navigation/{customize-sidebar,cross-references}.md`, `how-to/theming/{monorail-css,fonts}.md`, `how-to/pages/{front-matter,redirects}.md`, `how-to/discovery/{search,multiple-sources}.md`, `how-to/feeds/llms-txt.md`, `how-to/rich-content/ui-components-in-markdown.md`, `reference/front-matter/keys.md` |
| `DocSiteScaffoldExample` | Smallest DocSite — `AddDocSite` / `UseDocSite` / `RunDocSiteAsync` with two areas. | `tutorials/docsite/scaffold.md`, `reference/host/extensions.md` |
| `DocSiteSectionsExample` | Structure `Content/` into areas and subfolder-backed sections; `order:` / `section:` drive sidebar grouping. | `tutorials/docsite/sections-and-areas.md` |
| `DocSiteSharedCorpusExample` | DocSite with no `Content/` of its own — mounts the shared `_shared/Bramble` corpus via a relative `ContentRootPath`, four Diátaxis areas + auto-activated blog. A site-at-scale fixture host. | _(fixture/scale host)_ |
| `ExtensibilityLabExample` | Bare-host lab exercising every Pennington extension seam in one project: custom highlighter, code-block preprocessor, custom/emit-only `IContentService`, response processor, diagnostics processor, HTML rewriter, MonorailCSS customization, llms.txt opt-in, tabbed-code class override. | `how-to/markdown-pipeline/{custom-highlighter,code-block-preprocessor}.md`, `how-to/content-services/{custom-content-service,emit-generated-artifacts}.md`, `how-to/response-pipeline/{response-processor,html-rewriter}.md`, `how-to/theming/monorail-css.md`, `how-to/feeds/llms-txt.md`, `how-to/code-samples/tabbed-code.md`, `explanation/positioning/docsite-positioning.md`, `reference/diagnostics/request-context.md` |
-| `FocusedCodeSamplesExample` | Console app — two implementations of a word-counter (`Monolith`/`Modular`) for narrating a refactor via xmldocid fences. | `how-to/code-samples/focused-code-samples.md` |
+| `FocusedCodeSamplesExample` | Console app — two implementations of a word-counter (`Monolith`/`Modular`) for narrating a refactor via symbol fences. | `how-to/code-samples/focused-code-samples.md` |
| `FusionCacheDocSiteExample` | Real-target DocSite — API reference generated from `ZiggyCreatures.FusionCache` via `AddApiMetadataFromCompiledAssembly` + `AddApiReference`. | _(reference for `Pennington.ApiMetadata.Reflection`)_ |
| `GettingStartedBlazorPagesExample` | Replace the bare `MapGet` host with a Blazor Server `@page` catch-all (`MarkdownPage.razor`) that renders markdown through the same content pipeline. | `tutorials/getting-started/first-page.md` |
| `GettingStartedMinimalSiteExample` | Smallest viable Pennington host — `AddPennington` + `AddMarkdownContent` + a catch-all `MapGet`. | `tutorials/getting-started/first-site.md` |
@@ -61,17 +59,14 @@ Every example folder has a `README.md` describing its purpose, the concepts it t
## Staged tutorial files
Examples used by step-by-step tutorials split the teaching into per-stage artifacts:
-- **C# stages** live at the project root as `StageN_ {highlightedCode}""";
- }
-
- private static async Task{combinedHtml}""";
-
- return new CodeBlockPreprocessResult(wrappedHtml, baseLanguage, SkipTransform: false);
- }
- catch (Exception ex)
- {
- Diagnostics?.AddError($"Error processing xmldocid: {ex.Message}");
- var errorHtml = $"{WebUtility.HtmlEncode(code)}";
- return new CodeBlockPreprocessResult(errorHtml, baseLanguage, SkipTransform: true);
- }
- }
-
- private CodeBlockPreprocessResult? ProcessXmlDocIdDiff(string baseLanguage, string code, bool bodyOnly = false)
- {
- try
- {
- var xmlDocIds = code.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
-
- if (xmlDocIds.Length != 2)
- {
- Diagnostics?.AddError($"xmldocid-diff requires exactly 2 XmlDocIds, got {xmlDocIds.Length}");
- var errorHtml = $"{WebUtility.HtmlEncode(code)}";
- return new CodeBlockPreprocessResult(errorHtml, baseLanguage, SkipTransform: true);
- }
-
- var fragment1 = AsyncHelpers.RunSync(() => _symbolService.ExtractCodeFragmentAsync(xmlDocIds[0], bodyOnly));
- var fragment2 = AsyncHelpers.RunSync(() => _symbolService.ExtractCodeFragmentAsync(xmlDocIds[1], bodyOnly));
-
- // Handle errors
- var errors = new List{errorHtml}",
- baseLanguage,
- SkipTransform: true);
- }
-
- // Highlight both fragments
- var lang = DetectHighlighterLanguage(baseLanguage);
- var highlighted1 = _highlighter.Highlight(fragment1, lang);
- var highlighted2 = _highlighter.Highlight(fragment2, lang);
-
- var html1 = ExtractInnerCodeHtml(highlighted1);
- var html2 = ExtractInnerCodeHtml(highlighted2);
-
- var diffResult = ComputeAndRenderDiff(html1, html2, fragment1, fragment2);
-
- var preClass = diffResult.HasDifferences ? " class=\"has-diff\"" : "";
- var wrappedHtml = $"{diffResult.Html}";
-
- return new CodeBlockPreprocessResult(wrappedHtml, baseLanguage, SkipTransform: true);
- }
- catch (Exception ex)
- {
- Diagnostics?.AddError($"Error processing xmldocid-diff: {ex.Message}");
- var errorHtml = $"{WebUtility.HtmlEncode(code)}";
- return new CodeBlockPreprocessResult(errorHtml, baseLanguage, SkipTransform: true);
- }
- }
-
- private CodeBlockPreprocessResult? ProcessPath(string baseLanguage, string code)
- {
- try
- {
- var relativePath = code.Trim();
-
- // Validate path to prevent directory traversal
- if (relativePath.Contains("..") || Path.IsPathRooted(relativePath))
- {
- Diagnostics?.AddError($"Invalid file path: {relativePath}");
- var errorHtml = $"{WebUtility.HtmlEncode(code)}";
- return new CodeBlockPreprocessResult(errorHtml, baseLanguage, SkipTransform: true);
- }
-
- if (string.IsNullOrEmpty(_options.SolutionPath))
- {
- Diagnostics?.AddError("Solution path not configured for :path code block");
- var errorHtml = $"{WebUtility.HtmlEncode(code)}";
- return new CodeBlockPreprocessResult(errorHtml, baseLanguage, SkipTransform: true);
- }
-
- // Path.GetDirectoryName returns empty for bare filenames ("foo.slnx") with
- // no directory component. Normalize via GetFullPath so a bare SolutionPath —
- // which the runtime resolves against the process CWD — produces the same
- // directory we'd use at runtime.
- var solutionDir = Path.GetDirectoryName(Path.GetFullPath(_options.SolutionPath));
- if (string.IsNullOrEmpty(solutionDir))
- {
- Diagnostics?.AddError("Solution directory not found for :path code block");
- var errorHtml = $"{WebUtility.HtmlEncode(code)}";
- return new CodeBlockPreprocessResult(errorHtml, baseLanguage, SkipTransform: true);
- }
-
- var fullPath = Path.Combine(solutionDir, relativePath);
- if (!File.Exists(fullPath))
- {
- Diagnostics?.AddWarning($"File not found for :path code block: {relativePath}");
- var errorHtml = $"{WebUtility.HtmlEncode(code)}";
- return new CodeBlockPreprocessResult(errorHtml, baseLanguage, SkipTransform: true);
- }
-
- var content = File.ReadAllText(fullPath);
- var highlighted = _highlightingService.Highlight(content, baseLanguage);
-
- return new CodeBlockPreprocessResult(highlighted, baseLanguage, SkipTransform: false);
- }
- catch (Exception ex)
- {
- Diagnostics?.AddError($"Error loading file for :path code block: {ex.Message}");
- var errorHtml = $"{WebUtility.HtmlEncode(code)}";
- return new CodeBlockPreprocessResult(errorHtml, baseLanguage, SkipTransform: true);
- }
- }
-
- private static DiffRenderResult ComputeAndRenderDiff(
- string highlightedHtml1,
- string highlightedHtml2,
- string plainText1,
- string plainText2)
- {
- var htmlLines1 = SplitNewLines(highlightedHtml1);
- var htmlLines2 = SplitNewLines(highlightedHtml2);
-
- var differ = new DiffPlex.Differ();
- var diffResult = differ.CreateLineDiffs(plainText1, plainText2, ignoreWhitespace: true);
-
- var outputLines = new List...
- // We need to find the content between the opening code tag and the closing tags.
- const string closeTag = "";
-
- var codeTagStart = html.IndexOf("', codeTagStart) + 1;
- if (contentStart == 0)
- {
- return html;
- }
-
- var endIndex = html.IndexOf(closeTag, contentStart, StringComparison.Ordinal);
- if (endIndex == -1)
- {
- return html;
- }
-
- return html[contentStart..endIndex];
- }
-
- private static string[] SplitNewLines(string s)
- {
- return s.ReplaceLineEndings(Environment.NewLine).Split(Environment.NewLine);
- }
-
- private sealed record DiffRenderResult(string Html, bool HasDifferences);
-}
\ No newline at end of file
diff --git a/src/Pennington.Roslyn/RoslynExtensions.cs b/src/Pennington.Roslyn/RoslynExtensions.cs
deleted file mode 100644
index 8379e971..00000000
--- a/src/Pennington.Roslyn/RoslynExtensions.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-namespace Pennington.Roslyn;
-
-using Highlighting;
-using Markdown.Extensions;
-using Microsoft.Extensions.DependencyInjection;
-using Pennington.ApiMetadata;
-using Pennington.Highlighting;
-using Preprocessing;
-using Symbols;
-using Workspace;
-
-/// Dependency injection extensions for registering the Pennington Roslyn integration.
-public static class RoslynExtensions
-{
- /// Add Roslyn-based code analysis and highlighting.
- public static IServiceCollection AddPenningtonRoslyn(this IServiceCollection services, Action? configure = null)
- {
- var options = new RoslynOptions();
- configure?.Invoke(options);
- services.AddSingleton(options);
-
- // Always register basic highlighter (works without solution)
- services.AddSingleton();
- services.AddSingleton();
-
- // If solution path configured, register workspace + symbols + preprocessor
- if (!string.IsNullOrEmpty(options.SolutionPath))
- {
- services.AddSingleton();
- services.AddSingleton(sp =>
- {
- var symbolService = ActivatorUtilities.CreateInstance(sp);
- if (sp.GetRequiredService() is SolutionWorkspaceService sws)
- {
- sws.SymbolExtractionService = symbolService;
- }
-
- return symbolService;
- });
- if (options.EnableCodeFragmentFences)
- {
- services.AddSingleton();
- }
-
- services.AddSingleton();
- services.AddSingleton();
-
- services.AddHostedService();
- }
-
- return services;
- }
-}
\ No newline at end of file
diff --git a/src/Pennington.Roslyn/RoslynOptions.cs b/src/Pennington.Roslyn/RoslynOptions.cs
deleted file mode 100644
index f7aa2153..00000000
--- a/src/Pennington.Roslyn/RoslynOptions.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-namespace Pennington.Roslyn;
-
-/// Configuration for Roslyn integration.
-public sealed class RoslynOptions
-{
- /// Path to a .sln, .slnx, or .slnf file. If null, only basic highlighting is enabled.
- public string? SolutionPath { get; set; }
-
- /// Optional project filter.
- public ProjectFilter? ProjectFilter { get; set; }
-
- /// When true (the default), registers the :xmldocid /:path code-block preprocessor. Set false to keep the workspace and API-metadata services for reflection while delegating code-fragment fences to another preprocessor (for example tree-sitter :symbol ).
- public bool EnableCodeFragmentFences { get; set; } = true;
-}
-
-/// Filter for which projects to analyze.
-public record ProjectFilter
-{
- /// Project names to include; when non-null, only these projects are analyzed.
- public HashSet? IncludedProjects { get; init; }
- /// Project names to exclude from analysis.
- public HashSet? ExcludedProjects { get; init; }
-}
\ No newline at end of file
diff --git a/src/Pennington.Roslyn/Symbols/CodeFragmentExtractor.cs b/src/Pennington.Roslyn/Symbols/CodeFragmentExtractor.cs
deleted file mode 100644
index 11b70c68..00000000
--- a/src/Pennington.Roslyn/Symbols/CodeFragmentExtractor.cs
+++ /dev/null
@@ -1,175 +0,0 @@
-namespace Pennington.Roslyn.Symbols;
-
-using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.CSharp.Syntax;
-
-///
-/// Extracts code text from a syntax node.
-/// When bodyOnly=false , returns the full declaration text.
-/// When bodyOnly=true , returns expression body or block body content for methods,
-/// or content between braces for types.
-///
-internal static class CodeFragmentExtractor
-{
- ///
- /// Extracts the declaration signature (no body, no accessor list) for a method, constructor, operator,
- /// conversion operator, or property-style event. Falls back to the full node text for any other kind.
- ///
- public static Task ExtractSignatureAsync(SyntaxNode node, string fullText)
- {
- var end = node switch
- {
- MethodDeclarationSyntax m => m.ConstraintClauses.Count > 0
- ? m.ConstraintClauses[^1].Span.End
- : m.ParameterList?.Span.End ?? m.Identifier.Span.End,
- ConstructorDeclarationSyntax c => c.Initializer?.Span.End ?? c.ParameterList.Span.End,
- OperatorDeclarationSyntax o => o.ParameterList.Span.End,
- ConversionOperatorDeclarationSyntax cv => cv.ParameterList.Span.End,
- EventDeclarationSyntax ev => ev.Identifier.Span.End,
- _ => -1,
- };
-
- if (end < 0)
- {
- return Task.FromResult(ToStringWithLineIndent(node, fullText));
- }
-
- var spanStart = node.SpanStart;
- var lineStart = spanStart;
- while (lineStart > 0 && fullText[lineStart - 1] is ' ' or '\t')
- {
- lineStart--;
- }
-
- return Task.FromResult(fullText[lineStart..end] + ";");
- }
-
- ///
- /// Extracts a code fragment from the given syntax node.
- /// When is false, leading comments/xmldoc
- /// attached to the node as leading trivia are stripped — useful when the caller
- /// renders parsed xmldoc separately and does not want a duplicate comment block.
- ///
- public static Task ExtractCodeFragmentAsync(SyntaxNode node, string fullText, bool bodyOnly, bool includeLeadingTrivia = true)
- {
- if (!bodyOnly)
- {
- return Task.FromResult(includeLeadingTrivia ? node.ToFullString() : ToStringWithLineIndent(node, fullText));
- }
-
- var body = ExtractBody(node);
- return Task.FromResult(body ?? (includeLeadingTrivia ? node.ToFullString() : ToStringWithLineIndent(node, fullText)));
- }
-
- // When leading trivia is stripped, node.ToString() drops the first line's
- // leading whitespace too, leaving the first line at column 0 while subsequent
- // lines keep their source indent. Walk back over ' '/'\t' (stopping at a newline
- // or any non-whitespace so same-line xmldoc/comments stay excluded) and prepend
- // that whitespace so every line shares a baseline for downstream dedent.
- private static string ToStringWithLineIndent(SyntaxNode node, string fullText)
- {
- var spanStart = node.SpanStart;
- var lineStart = spanStart;
- while (lineStart > 0 && fullText[lineStart - 1] is ' ' or '\t')
- {
- lineStart--;
- }
-
- return fullText[lineStart..spanStart] + node.ToString();
- }
-
- private static string? ExtractBody(SyntaxNode node)
- {
- return node switch
- {
- MethodDeclarationSyntax method => ExtractMethodBody(method),
- ConstructorDeclarationSyntax constructor => ExtractBlockBody(constructor.Body),
- DestructorDeclarationSyntax destructor => ExtractBlockBody(destructor.Body),
- AccessorDeclarationSyntax accessor => ExtractBlockBody(accessor.Body),
- OperatorDeclarationSyntax op => ExtractMethodLikeBody(op.Body, op.ExpressionBody),
- ConversionOperatorDeclarationSyntax conv => ExtractMethodLikeBody(conv.Body, conv.ExpressionBody),
- PropertyDeclarationSyntax property => ExtractPropertyBody(property),
- TypeDeclarationSyntax typeDecl => ExtractBraceContent(typeDecl.OpenBraceToken, typeDecl.CloseBraceToken),
- EnumDeclarationSyntax enumDecl => ExtractBraceContent(enumDecl.OpenBraceToken, enumDecl.CloseBraceToken),
- NamespaceDeclarationSyntax nsDecl => ExtractBraceContent(nsDecl.OpenBraceToken, nsDecl.CloseBraceToken),
- _ => null,
- };
- }
-
- private static string? ExtractMethodBody(MethodDeclarationSyntax method)
- {
- if (method.ExpressionBody is { } expressionBody)
- {
- return expressionBody.Expression.ToFullString().TrimEnd();
- }
-
- return ExtractBlockBody(method.Body);
- }
-
- private static string? ExtractMethodLikeBody(BlockSyntax? body, ArrowExpressionClauseSyntax? expressionBody)
- {
- if (expressionBody is not null)
- {
- return expressionBody.Expression.ToFullString().TrimEnd();
- }
-
- return ExtractBlockBody(body);
- }
-
- private static string? ExtractBlockBody(BlockSyntax? block)
- {
- if (block is null)
- {
- return null;
- }
-
- // Return everything between the braces, trimming outer whitespace
- var openBrace = block.OpenBraceToken;
- var closeBrace = block.CloseBraceToken;
-
- var start = openBrace.Span.End;
- var end = closeBrace.SpanStart;
-
- if (end <= start)
- {
- return string.Empty;
- }
-
- var fullText = block.SyntaxTree.GetText().ToString();
- return fullText[start..end].TrimEnd();
- }
-
- private static string? ExtractPropertyBody(PropertyDeclarationSyntax property)
- {
- if (property.ExpressionBody is { } expressionBody)
- {
- return expressionBody.Expression.ToFullString().TrimEnd();
- }
-
- if (property.AccessorList is not null)
- {
- return property.AccessorList.ToString();
- }
-
- return null;
- }
-
- private static string? ExtractBraceContent(SyntaxToken openBrace, SyntaxToken closeBrace)
- {
- if (openBrace.IsMissing || closeBrace.IsMissing)
- {
- return null;
- }
-
- var start = openBrace.Span.End;
- var end = closeBrace.SpanStart;
-
- if (end <= start)
- {
- return string.Empty;
- }
-
- var fullText = openBrace.SyntaxTree!.GetText().ToString();
- return fullText[start..end].TrimEnd();
- }
-}
\ No newline at end of file
diff --git a/src/Pennington.Roslyn/Symbols/CodeFragmentResult.cs b/src/Pennington.Roslyn/Symbols/CodeFragmentResult.cs
deleted file mode 100644
index 1b443454..00000000
--- a/src/Pennington.Roslyn/Symbols/CodeFragmentResult.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Pennington.Roslyn.Symbols;
-
-using System.Collections.Immutable;
-
-/// The dedented source-text fragment for a symbol plus the file-local using directives the fragment depends on.
-/// Dedented source text for the symbol's declaration or body.
-/// Required file-local using directives in their original source order, each as the verbatim directive text (e.g. using System.Text; ).
-public sealed record CodeFragmentResult(string Fragment, ImmutableList Usings);
\ No newline at end of file
diff --git a/src/Pennington.Roslyn/Symbols/ISymbolExtractionService.cs b/src/Pennington.Roslyn/Symbols/ISymbolExtractionService.cs
deleted file mode 100644
index ce5b113d..00000000
--- a/src/Pennington.Roslyn/Symbols/ISymbolExtractionService.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-namespace Pennington.Roslyn.Symbols;
-
-using Microsoft.CodeAnalysis;
-
-/// Extracts symbol metadata and source fragments from the configured Roslyn , keyed by xmldocid.
-public interface ISymbolExtractionService
-{
- /// Walks the given projects and returns a map of xmldocid to for every documented symbol. Callers pass the workspace's filtered project set (one per multi-targeted csproj) so symbols aren't extracted once per target framework.
- Task> ExtractSymbolsAsync(IEnumerable projects);
-
- /// Returns the for the given xmldocid, or if no symbol matches.
- Task FindSymbolAsync(string xmlDocId);
-
- /// Returns the source-code fragment for the symbol identified by , optionally limited to the member body and optionally including leading trivia (comments/attributes).
- Task ExtractCodeFragmentAsync(string xmlDocId, bool bodyOnly = false, bool includeLeadingTrivia = true);
-
- /// Returns the source-code fragment for the symbol identified by together with the file-local using directives the fragment depends on. is empty when the symbol cannot be resolved.
- Task ExtractCodeFragmentWithUsingsAsync(string xmlDocId, bool bodyOnly = false, bool includeLeadingTrivia = true);
-
- /// Returns the declaration signature (no body, no accessor list) for the member identified by . Falls back to the full declaration for symbols that have no separable body.
- Task ExtractDeclarationSignatureAsync(string xmlDocId);
-
- /// Clears any cached symbol lookups so the next query re-walks the solution.
- void ClearCache();
-
- /// Triggers the symbol table walk if it has not already run. Safe to call concurrently with real queries — both share one task.
- Task WarmupAsync(CancellationToken cancellationToken = default);
-}
\ No newline at end of file
diff --git a/src/Pennington.Roslyn/Symbols/RequiredUsingsAnalyzer.cs b/src/Pennington.Roslyn/Symbols/RequiredUsingsAnalyzer.cs
deleted file mode 100644
index 1cf9e1d8..00000000
--- a/src/Pennington.Roslyn/Symbols/RequiredUsingsAnalyzer.cs
+++ /dev/null
@@ -1,265 +0,0 @@
-namespace Pennington.Roslyn.Symbols;
-
-using System.Collections.Immutable;
-using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.CSharp;
-using Microsoft.CodeAnalysis.CSharp.Syntax;
-
-///
-/// Computes the subset of file-local using directives that a code fragment actually depends on.
-/// Goal: turn an extracted method body or type declaration into a copy-pasteable sample by
-/// prepending only the imports the body's referenced symbols need — not the file's full using block.
-///
-internal static class RequiredUsingsAnalyzer
-{
- ///
- /// Returns the subset of 's file-local using directives
- /// (no global using directives, no implicit usings) that the symbols referenced inside
- /// require, preserving the directives' original source order.
- ///
- public static ImmutableList Analyze(
- SyntaxNode fragmentNode,
- SemanticModel semanticModel,
- CompilationUnitSyntax fileRoot)
- {
- if (fragmentNode.SyntaxTree != fileRoot.SyntaxTree)
- {
- return ImmutableList.Empty;
- }
-
- var candidates = CollectFileLocalUsings(fileRoot);
- if (candidates.Count == 0)
- {
- return ImmutableList.Empty;
- }
-
- var ambientNamespaces = CollectAmbientNamespaces(fragmentNode);
- var referencedNamespaces = new HashSet(StringComparer.Ordinal);
- var referencedStaticTypes = new HashSet(StringComparer.Ordinal);
- var referencedAliases = new HashSet(StringComparer.Ordinal);
-
- // Walk only SimpleNameSyntax nodes (IdentifierName + GenericName). These are the
- // smallest source positions where the author wrote a name, so:
- // - PredefinedTypeSyntax (`string`, `int`) is naturally skipped, avoiding spurious
- // `using System;` from `string` keyword usage.
- // - The SimpleName is the right node for the "is this reference qualified?" check
- // used to gate `using static`.
- // - GetAliasInfo on an alias-qualified identifier returns the IAliasSymbol, which
- // GetSymbolInfo would otherwise resolve straight to the underlying type.
- foreach (var name in fragmentNode.DescendantNodesAndSelf().OfType())
- {
- var alias = semanticModel.GetAliasInfo(name);
- if (alias is not null)
- {
- referencedAliases.Add(alias.Name);
- continue;
- }
-
- RecordSymbol(semanticModel.GetSymbolInfo(name).Symbol, name, ambientNamespaces, referencedNamespaces, referencedStaticTypes);
- RecordType(semanticModel.GetTypeInfo(name).Type, ambientNamespaces, referencedNamespaces);
- }
-
- var keep = ImmutableList.CreateBuilder();
- foreach (var directive in candidates)
- {
- if (IsRequired(directive, referencedNamespaces, referencedStaticTypes, referencedAliases))
- {
- keep.Add(directive);
- }
- }
-
- return keep.ToImmutable();
- }
-
- private static List CollectFileLocalUsings(CompilationUnitSyntax fileRoot)
- {
- var list = new List();
-
- foreach (var directive in fileRoot.Usings)
- {
- if (!directive.GlobalKeyword.IsKind(SyntaxKind.GlobalKeyword))
- {
- list.Add(directive);
- }
- }
-
- // Usings can also live inside file-scoped or block-form namespaces.
- foreach (var ns in fileRoot.DescendantNodes().OfType())
- {
- foreach (var directive in ns.Usings)
- {
- if (!directive.GlobalKeyword.IsKind(SyntaxKind.GlobalKeyword))
- {
- list.Add(directive);
- }
- }
- }
-
- return list;
- }
-
- private static HashSet CollectAmbientNamespaces(SyntaxNode fragmentNode)
- {
- // Symbols declared in the same namespace as the fragment don't need a using.
- // Walk every BaseNamespaceDeclarationSyntax that contains the fragment and
- // record its full name plus any ancestor namespaces (a nested `namespace A.B { namespace C { ... } }`
- // makes both A.B and A.B.C ambient).
- var ambient = new HashSet(StringComparer.Ordinal);
- for (var current = fragmentNode.Parent; current is not null; current = current.Parent)
- {
- if (current is BaseNamespaceDeclarationSyntax ns)
- {
- ambient.Add(ns.Name.ToString());
- }
- }
-
- return ambient;
- }
-
- private static void RecordSymbol(
- ISymbol? symbol,
- SimpleNameSyntax referenceNode,
- HashSet ambientNamespaces,
- HashSet referencedNamespaces,
- HashSet referencedStaticTypes)
- {
- if (symbol is null)
- {
- return;
- }
-
- switch (symbol)
- {
- case INamedTypeSymbol type:
- RecordType(type, ambientNamespaces, referencedNamespaces);
- break;
- case IMethodSymbol method:
- if (method.IsExtensionMethod)
- {
- var origin = method.ReducedFrom ?? method;
- AddNamespace(origin.ContainingNamespace, ambientNamespaces, referencedNamespaces);
- }
-
- if (method.IsStatic && !method.IsExtensionMethod && IsUnqualifiedReference(referenceNode))
- {
- AddStaticType(method.ContainingType, referencedStaticTypes);
- }
-
- foreach (var typeArg in method.TypeArguments)
- {
- RecordType(typeArg, ambientNamespaces, referencedNamespaces);
- }
-
- break;
- case IPropertySymbol property when property.IsStatic && IsUnqualifiedReference(referenceNode):
- AddStaticType(property.ContainingType, referencedStaticTypes);
- break;
- case IFieldSymbol field when field.IsStatic && IsUnqualifiedReference(referenceNode):
- AddStaticType(field.ContainingType, referencedStaticTypes);
- break;
- case IEventSymbol ev when ev.IsStatic && IsUnqualifiedReference(referenceNode):
- AddStaticType(ev.ContainingType, referencedStaticTypes);
- break;
- }
- }
-
- private static void RecordType(ITypeSymbol? type, HashSet ambientNamespaces, HashSet referencedNamespaces)
- {
- if (type is null)
- {
- return;
- }
-
- switch (type)
- {
- case INamedTypeSymbol named:
- var outermost = OutermostContainingType(named);
- AddNamespace(outermost.ContainingNamespace, ambientNamespaces, referencedNamespaces);
- foreach (var typeArg in named.TypeArguments)
- {
- RecordType(typeArg, ambientNamespaces, referencedNamespaces);
- }
-
- break;
- case IArrayTypeSymbol array:
- RecordType(array.ElementType, ambientNamespaces, referencedNamespaces);
- break;
- case IPointerTypeSymbol pointer:
- RecordType(pointer.PointedAtType, ambientNamespaces, referencedNamespaces);
- break;
- }
- }
-
- private static INamedTypeSymbol OutermostContainingType(INamedTypeSymbol type)
- {
- var current = type;
- while (current.ContainingType is { } parent)
- {
- current = parent;
- }
-
- return current;
- }
-
- private static void AddNamespace(INamespaceSymbol? ns, HashSet ambientNamespaces, HashSet referencedNamespaces)
- {
- if (ns is null || ns.IsGlobalNamespace)
- {
- return;
- }
-
- var name = ns.ToDisplayString();
- if (ambientNamespaces.Contains(name))
- {
- return;
- }
-
- referencedNamespaces.Add(name);
- }
-
- private static void AddStaticType(INamedTypeSymbol? type, HashSet referencedStaticTypes)
- {
- if (type is null)
- {
- return;
- }
-
- referencedStaticTypes.Add(type.ToDisplayString());
- }
-
- private static bool IsUnqualifiedReference(SimpleNameSyntax referenceNode)
- {
- // A reference is "unqualified" — and therefore needs `using static` to bind — when the
- // simple name isn't the right-hand side of a `Type.Member` access. `Type.Method()` already
- // qualifies the member, so it doesn't drive a static-using requirement.
- return referenceNode.Parent is not MemberAccessExpressionSyntax memberAccess
- || memberAccess.Name != referenceNode;
- }
-
- private static bool IsRequired(
- UsingDirectiveSyntax directive,
- HashSet referencedNamespaces,
- HashSet referencedStaticTypes,
- HashSet referencedAliases)
- {
- var target = directive.Name?.ToString();
- if (string.IsNullOrEmpty(target))
- {
- return false;
- }
-
- if (directive.Alias is { Name: { } aliasName })
- {
- return referencedAliases.Contains(aliasName.Identifier.ValueText);
- }
-
- if (directive.StaticKeyword.IsKind(SyntaxKind.StaticKeyword))
- {
- return referencedStaticTypes.Contains(target);
- }
-
- // Plain `using X.Y;` — exact namespace match. C# does not flow `using X.Y;` to `X.Y.Sub`,
- // so we don't expand prefixes either.
- return referencedNamespaces.Contains(target);
- }
-}
\ No newline at end of file
diff --git a/src/Pennington.Roslyn/Symbols/SymbolExtractionService.cs b/src/Pennington.Roslyn/Symbols/SymbolExtractionService.cs
deleted file mode 100644
index 6fce4bb3..00000000
--- a/src/Pennington.Roslyn/Symbols/SymbolExtractionService.cs
+++ /dev/null
@@ -1,426 +0,0 @@
-namespace Pennington.Roslyn.Symbols;
-
-using System.Collections.Concurrent;
-using System.Collections.Immutable;
-using Infrastructure;
-using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.CSharp.Syntax;
-using Microsoft.CodeAnalysis.Text;
-using Microsoft.Extensions.Logging;
-using Utilities;
-using Workspace;
-
-///
-/// Extracts all symbols from a Roslyn Solution and enables lookup by XML documentation ID.
-/// Uses for lazy one-time symbol table loading with retry on failure.
-///
-internal sealed class SymbolExtractionService : ISymbolExtractionService
-{
- private readonly ISolutionWorkspaceService _workspaceService;
- private readonly ILogger _logger;
- private readonly AsyncLazy> _symbolsLazy;
-
- public SymbolExtractionService(
- ISolutionWorkspaceService workspaceService,
- ILogger logger)
- {
- ArgumentNullException.ThrowIfNull(workspaceService);
- ArgumentNullException.ThrowIfNull(logger);
-
- _workspaceService = workspaceService;
- _logger = logger;
- _symbolsLazy = new AsyncLazy>(LoadAllSymbolsAsync);
- }
-
- public async Task> ExtractSymbolsAsync(IEnumerable projects)
- {
- var symbols = new ConcurrentDictionary(StringComparer.Ordinal);
- var documentByPath = new ConcurrentDictionary(StringComparer.Ordinal);
- var fallbackByProject = new ConcurrentDictionary();
-
- await Parallel.ForEachAsync(projects, async (project, ct) =>
- {
- var compilation = await project.GetCompilationAsync(ct);
- if (compilation is null)
- {
- _logger.LogWarning("Failed to get compilation for project {ProjectName}", project.Name);
- return;
- }
-
- await Parallel.ForEachAsync(project.Documents, async (document, ct2) =>
- {
- try
- {
- if (document.FilePath is { Length: > 0 } path)
- {
- documentByPath[path] = (document, project);
- fallbackByProject.TryAdd(project.Id, (document, project));
- }
- await ExtractDocumentSymbolsAsync(document, compilation, project, symbols, ct2);
- }
- catch (Exception ex)
- {
- _logger.LogWarning(ex, "Failed to extract symbols from {DocumentPath}", document.FilePath);
- }
- });
-
- // Fallback pass — walk compilation symbols for types the syntax-only
- // pass missed (e.g. C# 15 unions whose declaration node is not a
- // TypeDeclarationSyntax, members synthesized from primary-constructor
- // parameter lists like record `#ctor` overloads, or Razor-generated
- // component classes whose syntax tree path is not a user document).
- try
- {
- await ExtractCompilationSymbolsAsync(compilation, documentByPath, fallbackByProject, project, symbols);
- }
- catch (Exception ex)
- {
- _logger.LogWarning(ex, "Failed to extract compilation-level symbols for {ProjectName}", project.Name);
- }
- });
-
- _logger.LogDebug("Extracted {Count} symbols from solution", symbols.Count);
- return symbols;
- }
-
- private async Task ExtractCompilationSymbolsAsync(
- Compilation compilation,
- ConcurrentDictionary documentByPath,
- ConcurrentDictionary fallbackByProject,
- Project currentProject,
- ConcurrentDictionary symbols)
- {
- var visited = new HashSet(SymbolEqualityComparer.Default);
- var namespaceQueue = new Queue();
- var typeQueue = new Queue();
- namespaceQueue.Enqueue(compilation.Assembly.GlobalNamespace);
-
- while (namespaceQueue.Count > 0)
- {
- var ns = namespaceQueue.Dequeue();
- foreach (var member in ns.GetMembers())
- {
- switch (member)
- {
- case INamespaceSymbol child:
- namespaceQueue.Enqueue(child);
- break;
- case INamedTypeSymbol type when visited.Add(type):
- typeQueue.Enqueue(type);
- break;
- }
- }
- }
-
- while (typeQueue.Count > 0)
- {
- var type = typeQueue.Dequeue();
- await TryAddSymbolFromCompilationAsync(type, documentByPath, fallbackByProject, currentProject, symbols);
- foreach (var typeMember in type.GetMembers())
- {
- if (typeMember is INamedTypeSymbol nested)
- {
- if (visited.Add(nested))
- {
- typeQueue.Enqueue(nested);
- }
-
- continue;
- }
- await TryAddSymbolFromCompilationAsync(typeMember, documentByPath, fallbackByProject, currentProject, symbols);
- }
- }
- }
-
- private async Task TryAddSymbolFromCompilationAsync(
- ISymbol symbol,
- ConcurrentDictionary documentByPath,
- ConcurrentDictionary fallbackByProject,
- Project currentProject,
- ConcurrentDictionary symbols)
- {
- var docId = symbol.GetDocumentationCommentId();
- if (string.IsNullOrEmpty(docId))
- {
- return;
- }
-
- var normalizedId = XmlDocIdNormalizer.Normalize(docId);
- if (symbols.ContainsKey(normalizedId))
- {
- return;
- }
-
- var syntaxRef = symbol.DeclaringSyntaxReferences.FirstOrDefault();
- var path = syntaxRef?.SyntaxTree.FilePath;
- SyntaxNode node;
- TextSpan span;
-
- if (syntaxRef is not null && !string.IsNullOrEmpty(path) && documentByPath.TryGetValue(path, out var entry))
- {
- node = await syntaxRef.GetSyntaxAsync();
- span = node.Span;
- }
- else if (syntaxRef is not null && fallbackByProject.TryGetValue(currentProject.Id, out entry))
- {
- // Compiler-synthesized symbols (e.g. record primary constructors whose
- // syntax reference points to the record declaration but whose path
- // lookup misses) register with a placeholder span; downstream source
- // extraction will no-op against it.
- node = await syntaxRef.GetSyntaxAsync();
- span = default;
- }
- else
- {
- return;
- }
-
- var sourceText = await entry.Document.GetTextAsync();
-
- var info = new SymbolInfo(
- Symbol: symbol,
- Document: entry.Document,
- SyntaxNode: node,
- SourceText: sourceText,
- TextSpan: span,
- Project: entry.Project
- );
-
- symbols.TryAdd(normalizedId, info);
- }
-
- public async Task FindSymbolAsync(string xmlDocId)
- {
- var normalizedId = XmlDocIdNormalizer.Normalize(xmlDocId);
- var symbols = await _symbolsLazy.Value;
-
- if (symbols.TryGetValue(normalizedId, out var symbolInfo))
- {
- return symbolInfo;
- }
-
- _logger.LogTrace("Symbol not found for xmlDocId: {XmlDocId} (normalized: {NormalizedId})", xmlDocId, normalizedId);
- return null;
- }
-
- public async Task ExtractCodeFragmentAsync(string xmlDocId, bool bodyOnly = false, bool includeLeadingTrivia = true)
- {
- var symbolInfo = await FindSymbolAsync(xmlDocId);
- if (symbolInfo is null)
- {
- _logger.LogWarning("Cannot extract code fragment — symbol not found: {XmlDocId}", xmlDocId);
- return string.Empty;
- }
-
- var root = await symbolInfo.Document.GetSyntaxRootAsync();
- if (root is null)
- {
- return string.Empty;
- }
-
- // Find the node in the current syntax tree by its span
- var node = root.FindNode(symbolInfo.TextSpan);
- if (node is null)
- {
- return string.Empty;
- }
-
- var sourceText = await symbolInfo.Document.GetTextAsync();
- var fullText = sourceText.ToString();
-
- var fragment = await CodeFragmentExtractor.ExtractCodeFragmentAsync(node, fullText, bodyOnly, includeLeadingTrivia);
- return TextFormatter.NormalizeIndents(fragment);
- }
-
- public async Task ExtractCodeFragmentWithUsingsAsync(string xmlDocId, bool bodyOnly = false, bool includeLeadingTrivia = true)
- {
- var symbolInfo = await FindSymbolAsync(xmlDocId);
- if (symbolInfo is null)
- {
- _logger.LogWarning("Cannot extract code fragment with usings — symbol not found: {XmlDocId}", xmlDocId);
- return new CodeFragmentResult(string.Empty, ImmutableList.Empty);
- }
-
- var root = await symbolInfo.Document.GetSyntaxRootAsync();
- if (root is null)
- {
- return new CodeFragmentResult(string.Empty, ImmutableList.Empty);
- }
-
- var node = symbolInfo.TextSpan.Length > 0 ? root.FindNode(symbolInfo.TextSpan) : null;
- if (node is null)
- {
- return new CodeFragmentResult(string.Empty, ImmutableList.Empty);
- }
-
- var sourceText = await symbolInfo.Document.GetTextAsync();
- var fullText = sourceText.ToString();
-
- var fragment = await CodeFragmentExtractor.ExtractCodeFragmentAsync(node, fullText, bodyOnly, includeLeadingTrivia);
- var dedented = TextFormatter.NormalizeIndents(fragment);
-
- var usings = await ResolveRequiredUsingsAsync(symbolInfo, root, node);
- return new CodeFragmentResult(dedented, usings);
- }
-
- private async Task> ResolveRequiredUsingsAsync(SymbolInfo symbolInfo, SyntaxNode root, SyntaxNode fragmentNode)
- {
- if (root is not CompilationUnitSyntax compilationUnit)
- {
- return ImmutableList.Empty;
- }
-
- var compilation = await symbolInfo.Project.GetCompilationAsync();
- if (compilation is null)
- {
- return ImmutableList