Embed focused code samples
Scope :symbol fences to one member, strip declaration noise with bodyonly, and refactor long methods into named helpers so each section of a walkthrough shows one idea.
To limit a code fence to the one member a walkthrough discusses — rather than dumping the whole enclosing file with every sibling member — use the :symbol preprocessor's member-scoped form. The recipes below scope a fence to one member, strip declaration noise with ,bodyonly, carry the file's imports with ,imports, walk a multi-phase method through named helpers, outline a type's shape with ,signatures, diff two implementations with symbol-diff, and embed a whole file with a bare path. Address a member by its name path (Type.Member) rather than a hard-coded line range — a name path survives the line shifts that silently break a range. For the fence grammar itself, see Code-block argument reference.
Before you begin
- An existing Pennington site (see Create your first Pennington site if not), with
Pennington.TreeSitterwired throughAddTreeSitterandContentRootpointing at the root that holds the source to fence. - Comfort authoring markdown code fences — the techniques on this page are all info-string changes on a
csharp:symbolfence.
For a working setup, see examples/FocusedCodeSamplesExample. MonolithWordCounter carries one long CountWords method; ModularWordCounter splits the same logic into Tokenize, Tally, and Format. Both are referenced by the fences below.
Fence one member, not the whole type
When the surrounding prose is about one method, reach for Type.Method instead of a bare Type. A member path shrinks the fence to the member the reader cares about; a Type reference (or a bare file path with no >) pulls the full type or file.
The wide form, which lands on a page that only discusses CountWords:
```csharp:symbol
examples/FocusedCodeSamplesExample/MonolithWordCounter.cs > MonolithWordCounter
```
The narrow form, scoped to the method under discussion:
```csharp:symbol
examples/FocusedCodeSamplesExample/MonolithWordCounter.cs > MonolithWordCounter.CountWords
```
Which renders as:
public static string CountWords(string text, int topN)
{
// Tokenize: split on whitespace, lowercase, strip surrounding punctuation.
var words = new List<string>();
foreach (var raw in text.Split([' ', '\t', '\n', '\r'], StringSplitOptions.RemoveEmptyEntries))
{
var word = raw.Trim('.', ',', '!', '?', ';', ':', '"', '\'').ToLowerInvariant();
if (word.Length > 0)
{
words.Add(word);
}
}
// Tally: count occurrences and rank by frequency desc, then alphabetically.
var counts = new Dictionary<string, int>();
foreach (var w in words)
{
counts[w] = counts.GetValueOrDefault(w, 0) + 1;
}
var ranked = counts
.OrderByDescending(kv => kv.Value)
.ThenBy(kv => kv.Key)
.Take(topN)
.ToList();
// Format: header line plus one word-count row per entry.
var sb = new StringBuilder();
sb.AppendLine($"Top {ranked.Count} words:");
foreach (var kv in ranked)
{
sb.Append(kv.Key.PadRight(12));
sb.Append(' ');
sb.AppendLine(kv.Value.ToString());
}
return sb.ToString();
}
The name-path grammar — Type, Type.Member, nested Type.Inner.Member — is listed in Code-block argument reference.
Strip declaration noise with ,bodyonly
Even a member-scoped fence still carries the signature and any leading doc comment. When the prose has already named the method and summarized what it does, both are redundant. Appending ,bodyonly renders only the body between the braces.
```csharp:symbol,bodyonly
examples/FocusedCodeSamplesExample/MonolithWordCounter.cs > MonolithWordCounter.CountWords
```
Which renders as:
// Tokenize: split on whitespace, lowercase, strip surrounding punctuation.
var words = new List<string>();
foreach (var raw in text.Split([' ', '\t', '\n', '\r'], StringSplitOptions.RemoveEmptyEntries))
{
var word = raw.Trim('.', ',', '!', '?', ';', ':', '"', '\'').ToLowerInvariant();
if (word.Length > 0)
{
words.Add(word);
}
}
// Tally: count occurrences and rank by frequency desc, then alphabetically.
var counts = new Dictionary<string, int>();
foreach (var w in words)
{
counts[w] = counts.GetValueOrDefault(w, 0) + 1;
}
var ranked = counts
.OrderByDescending(kv => kv.Value)
.ThenBy(kv => kv.Key)
.Take(topN)
.ToList();
// Format: header line plus one word-count row per entry.
var sb = new StringBuilder();
sb.AppendLine($"Top {ranked.Count} words:");
foreach (var kv in ranked)
{
sb.Append(kv.Key.PadRight(12));
sb.Append(' ');
sb.AppendLine(kv.Value.ToString());
}
return sb.ToString();
,bodyonly also works on types (members between the braces, skipping the type header) and properties (the get/set accessors).
Carry the imports with ,imports
A member-scoped fence drops the file's using / import / require lines, so a reader can't see where the types resolve from. Append ,imports to prepend the file's top-of-file imports above the snippet, separated by a blank line.
```csharp:symbol,imports
examples/FocusedCodeSamplesExample/MonolithWordCounter.cs > MonolithWordCounter.CountWords
```
Which renders as:
using System.Text;
public static string CountWords(string text, int topN)
{
// Tokenize: split on whitespace, lowercase, strip surrounding punctuation.
var words = new List<string>();
foreach (var raw in text.Split([' ', '\t', '\n', '\r'], StringSplitOptions.RemoveEmptyEntries))
{
var word = raw.Trim('.', ',', '!', '?', ';', ':', '"', '\'').ToLowerInvariant();
if (word.Length > 0)
{
words.Add(word);
}
}
// Tally: count occurrences and rank by frequency desc, then alphabetically.
var counts = new Dictionary<string, int>();
foreach (var w in words)
{
counts[w] = counts.GetValueOrDefault(w, 0) + 1;
}
var ranked = counts
.OrderByDescending(kv => kv.Value)
.ThenBy(kv => kv.Key)
.Take(topN)
.ToList();
// Format: header line plus one word-count row per entry.
var sb = new StringBuilder();
sb.AppendLine($"Top {ranked.Count} words:");
foreach (var kv in ranked)
{
sb.Append(kv.Key.PadRight(12));
sb.Append(' ');
sb.AppendLine(kv.Value.ToString());
}
return sb.ToString();
}
,imports prepends every import at the top of the file, not only the ones the member references. It composes with ,bodyonly — the imports lead, the body follows — and is a no-op for whole-file embeds and for languages with no syntactic import statement (Ruby's require is a method call, not a declaration).
Walk a multi-phase method through named helpers
When the target method runs 25+ lines across distinct phases, fence each phase as its own helper instead of fencing the monolith. ModularWordCounter is the same logic as MonolithWordCounter split into three helpers — Tokenize, Tally, and Format — orchestrated by a short CountWords. A whole-type fence gives the reader the full picture in one place:
```csharp:symbol
examples/FocusedCodeSamplesExample/ModularWordCounter.cs > ModularWordCounter
```
Which renders as:
public static class ModularWordCounter
{
/// <summary>
/// Returns a column-aligned report of the <paramref name="topN"/> most
/// frequent words in <paramref name="text"/> by orchestrating the three
/// helpers below.
/// </summary>
/// <param name="text">Free-form text to analyse.</param>
/// <param name="topN">Number of top-frequency words to include.</param>
/// <returns>A multi-line string suitable for console output.</returns>
public static string CountWords(string text, int topN)
{
var words = Tokenize(text);
var ranked = Tally(words, topN);
return Format(ranked);
}
/// <summary>
/// Splits <paramref name="text"/> on whitespace, lower-cases every token,
/// and strips surrounding punctuation. Empty tokens are dropped.
/// </summary>
public static List<string> Tokenize(string text)
{
var words = new List<string>();
foreach (var raw in text.Split([' ', '\t', '\n', '\r'], StringSplitOptions.RemoveEmptyEntries))
{
var word = raw.Trim('.', ',', '!', '?', ';', ':', '"', '\'').ToLowerInvariant();
if (word.Length > 0)
{
words.Add(word);
}
}
return words;
}
/// <summary>
/// Groups <paramref name="words"/>, counts occurrences, and returns the
/// top <paramref name="topN"/> ranked by frequency descending then
/// alphabetically.
/// </summary>
public static List<KeyValuePair<string, int>> Tally(List<string> words, int topN)
{
var counts = new Dictionary<string, int>();
foreach (var w in words)
{
counts[w] = counts.GetValueOrDefault(w, 0) + 1;
}
return counts
.OrderByDescending(kv => kv.Value)
.ThenBy(kv => kv.Key)
.Take(topN)
.ToList();
}
/// <summary>
/// Renders <paramref name="ranked"/> as a header line plus one
/// column-aligned row per entry.
/// </summary>
public static string Format(List<KeyValuePair<string, int>> ranked)
{
var sb = new StringBuilder();
sb.AppendLine($"Top {ranked.Count} words:");
foreach (var kv in ranked)
{
sb.Append(kv.Key.PadRight(12));
sb.Append(' ');
sb.AppendLine(kv.Value.ToString());
}
return sb.ToString();
}
/// <summary>
/// Same output as <see cref="Format"/>, but rents its
/// <see cref="StringBuilder"/> from <see cref="StringBuilderPool"/>
/// instead of allocating a fresh one each call. Exists to pair with
/// <see cref="Format"/> inside an <c>xmldocid-diff</c> fence so the
/// delta is small and focused on one mechanical change.
/// </summary>
public static string FormatV2(List<KeyValuePair<string, int>> ranked)
{
var sb = StringBuilderPool.Get();
sb.AppendLine($"Top {ranked.Count} words:");
foreach (var kv in ranked)
{
sb.Append(kv.Key.PadRight(12));
sb.Append(' ');
sb.AppendLine(kv.Value.ToString());
}
var result = sb.ToString();
StringBuilderPool.Return(sb);
return result;
}
}
In a walkthrough, fence each helper separately so each section carries one idea:
```csharp:symbol,bodyonly
examples/FocusedCodeSamplesExample/ModularWordCounter.cs > ModularWordCounter.CountWords
```
```csharp:symbol,bodyonly
examples/FocusedCodeSamplesExample/ModularWordCounter.cs > ModularWordCounter.Tokenize
```
```csharp:symbol,bodyonly
examples/FocusedCodeSamplesExample/ModularWordCounter.cs > ModularWordCounter.Tally
```
```csharp:symbol,bodyonly
examples/FocusedCodeSamplesExample/ModularWordCounter.cs > ModularWordCounter.Format
```
The orchestrator renders as a three-liner that reads top-to-bottom as the outline for the walkthrough:
var words = Tokenize(text);
var ranked = Tally(words, topN);
return Format(ranked);
Tokenize:
var words = new List<string>();
foreach (var raw in text.Split([' ', '\t', '\n', '\r'], StringSplitOptions.RemoveEmptyEntries))
{
var word = raw.Trim('.', ',', '!', '?', ';', ':', '"', '\'').ToLowerInvariant();
if (word.Length > 0)
{
words.Add(word);
}
}
return words;
Tally:
var counts = new Dictionary<string, int>();
foreach (var w in words)
{
counts[w] = counts.GetValueOrDefault(w, 0) + 1;
}
return counts
.OrderByDescending(kv => kv.Value)
.ThenBy(kv => kv.Key)
.Take(topN)
.ToList();
Format:
var sb = new StringBuilder();
sb.AppendLine($"Top {ranked.Count} words:");
foreach (var kv in ranked)
{
sb.Append(kv.Key.PadRight(12));
sb.Append(' ');
sb.AppendLine(kv.Value.ToString());
}
return sb.ToString();
:symbol resolves a member by name path within the file, so give each helper a distinct name — overloads resolve to the first declaration and can't be told apart.
Show a type's shape with ,signatures
When the point is a type's public members — what it exposes, not how each member works — append ,signatures. Every member body collapses to { … }, leaving the declarations, signatures, doc comments, and member order intact for an at-a-glance outline.
```csharp:symbol,signatures
examples/FocusedCodeSamplesExample/ModularWordCounter.cs > ModularWordCounter
```
Which renders as:
public static class ModularWordCounter
{
/// <summary>
/// Returns a column-aligned report of the <paramref name="topN"/> most
/// frequent words in <paramref name="text"/> by orchestrating the three
/// helpers below.
/// </summary>
/// <param name="text">Free-form text to analyse.</param>
/// <param name="topN">Number of top-frequency words to include.</param>
/// <returns>A multi-line string suitable for console output.</returns>
public static string CountWords(string text, int topN)
{ … }
/// <summary>
/// Splits <paramref name="text"/> on whitespace, lower-cases every token,
/// and strips surrounding punctuation. Empty tokens are dropped.
/// </summary>
public static List<string> Tokenize(string text)
{ … }
/// <summary>
/// Groups <paramref name="words"/>, counts occurrences, and returns the
/// top <paramref name="topN"/> ranked by frequency descending then
/// alphabetically.
/// </summary>
public static List<KeyValuePair<string, int>> Tally(List<string> words, int topN)
{ … }
/// <summary>
/// Renders <paramref name="ranked"/> as a header line plus one
/// column-aligned row per entry.
/// </summary>
public static string Format(List<KeyValuePair<string, int>> ranked)
{ … }
/// <summary>
/// Same output as <see cref="Format"/>, but rents its
/// <see cref="StringBuilder"/> from <see cref="StringBuilderPool"/>
/// instead of allocating a fresh one each call. Exists to pair with
/// <see cref="Format"/> inside an <c>xmldocid-diff</c> fence so the
/// delta is small and focused on one mechanical change.
/// </summary>
public static string FormatV2(List<KeyValuePair<string, int>> ranked)
{ … }
}
,signatures works on a single member too, rendering only its signature over an elided body. It targets brace-delimited languages (C#, Java, TypeScript, Go, Rust); Python and Ruby suites collapse to a best-effort …. As the inverse of ,bodyonly, the two don't combine — ,signatures wins when both are set.
Show a delta with symbol-diff
When the article's point is that one version replaces another — a small refactor, a migration, a perf tweak — fence both versions with symbol-diff. The preprocessor emits a unified diff so the reader sees the two or three lines that moved rather than comparing two fences by eye. The form works best when the delta is small; whole-method rewrites render every line as changed and bury the point.
ModularWordCounter.FormatV2 is deliberately a one-change variant of Format. It rents its StringBuilder from a pool instead of constructing a fresh one, and returns the builder at the end. Everything else is identical, so the diff collapses to those lines.
```csharp:symbol-diff,bodyonly
examples/FocusedCodeSamplesExample/ModularWordCounter.cs > ModularWordCounter.Format
examples/FocusedCodeSamplesExample/ModularWordCounter.cs > ModularWordCounter.FormatV2
```
Which renders as:
var sb = new StringBuilder();
var sb = StringBuilderPool.Get();
sb.AppendLine($"Top {ranked.Count} words:");
foreach (var kv in ranked)
{
sb.Append(kv.Key.PadRight(12));
sb.Append(' ');
sb.AppendLine(kv.Value.ToString());
}
return sb.ToString();
var result = sb.ToString();
StringBuilderPool.Return(sb);
return result;
The fence body must hold exactly two references, one per line, in before → after order. ,bodyonly applies to both sides, so the diff compares implementations without declaration boilerplate drowning out the change.
Embed a whole file with a bare path
Top-level-statement Program.cs files, .razor components, markdown or YAML fixtures, and JSON / TOML / config files have no member to scope to. A bare <file> reference with no > member embeds the entire file:
```csharp:symbol
examples/FocusedCodeSamplesExample/Program.cs
```
Which renders as:
using FocusedCodeSamplesExample;
const string sample = """
the quick brown fox jumps over the lazy dog
the fox was quick and the dog was lazy
""";
Console.WriteLine("=== MonolithWordCounter ===");
Console.WriteLine(MonolithWordCounter.CountWords(sample, topN: 3));
Console.WriteLine("=== ModularWordCounter ===");
Console.WriteLine(ModularWordCounter.CountWords(sample, topN: 3));
Verify
- Rebuild the site with
dotnet run --project docs/Pennington.Docs -- buildand reload the page — each fence renders at the scope its info string declares, with no carry-over of enclosing-type members. A member-scoped fence (> Type.Member) shows only that member; a,bodyonlyfence drops the signature; the whole-file fence shows every line ofProgram.cs. - Rename
TokenizetoSplitinexamples/FocusedCodeSamplesExample/ModularWordCounter.csand rebuild — the build report surfaces an unresolvedModularWordCounter.Tokenizereference rather than silently rendering nothing.
Related
- Reference: Markdown extensions catalog — the full fence grammar including
symbol,symbol,bodyonly, andsymbol-diff. - Reference: Code-block argument reference — info-string parser details and the full list of suffix forms.
- How-to: Annotate code blocks — per-line
[!code highlight]/[!code ++]directives that compose with the fence forms on this page.