@radarleaf/markdown-it-region-snippets
v1.1.1
Published
markdown-it plugin that extracts #region code snippets from source files and expands them in markdown
Maintainers
Readme
markdown-it-region-snippets
A markdown-it plugin that extracts #region code snippets from source files and expands snippet: name markers in markdown into fenced code blocks with syntax highlighting. Also supports direct file includes and inline snippet definitions.
Works with VitePress, VuePress, Docusaurus, or any markdown-it setup. Uses its own snippet: marker syntax — no conflict with VitePress's built-in <<< file includes.
Requirements
| Requirement | Version |
|-------------|---------|
| Node.js | >= 18.0.0 |
| Module system | ESM ("type": "module") |
| markdown-it | >= 13.0.0 (optional peer dependency) |
Zero runtime dependencies. Works with any markdown-it pipeline — VitePress, VuePress, Docusaurus, or standalone.
Installation
npm install markdown-it-region-snippetsQuick Start — VitePress
// docs/.vitepress/config.mts
import { defineConfig } from 'vitepress'
import { fileURLToPath, URL } from 'node:url'
import { regionSnippetPlugin } from 'markdown-it-region-snippets'
const rootDir = fileURLToPath(new URL('../..', import.meta.url))
export default defineConfig({
markdown: {
config(md) {
md.use(regionSnippetPlugin, {
rootDir,
syntax: 'snippet-colon',
})
},
},
})Quick Start — Plain markdown-it
import MarkdownIt from 'markdown-it'
import { regionSnippetPlugin } from 'markdown-it-region-snippets'
const md = new MarkdownIt()
md.use(regionSnippetPlugin, {
rootDir: '/path/to/project',
})
const html = md.render('snippet: my_snippet\n')How It Works
- Source files contain
#region name/#endregionmarkers around code - Markdown files reference them with
snippet: nameon its own line - At build time, the plugin scans source files, extracts snippet code, and replaces markers with fenced code blocks
- The fence language is auto-detected from the source file extension
Source file (e.g., src/Example.cs)
public class Example
{
#region sample_hello
public void Hello()
{
Console.WriteLine("Hello!");
}
#endregion
}Markdown file
## Example
snippet: sample_helloRendered output
```csharp
public void Hello()
{
Console.WriteLine("Hello!");
}
```Region Snippets vs File Includes vs Inline Snippets
This plugin supports three ways to embed source code in your docs: named region snippets, direct file includes, and inline snippets. Each has its strengths — use whichever fits the situation, or mix all three.
Named Region Snippets
Mark regions in your source code with #region / #endregion, then reference them by name in markdown.
#region sample_auth
public async Task<bool> Authenticate(string token) { ... }
#endregionsnippet: sample_authAdvantages:
- Decoupled from file structure — rename or move source files without updating any markdown. The region name is the only contract between code and docs.
- Reusable across pages — the same named snippet can be referenced from multiple markdown files. Define once, embed everywhere.
- Fast at parse time — all regions are pre-scanned at startup and stored in a Map. Each lookup is O(1) with no per-reference file I/O.
- Breakage is caught immediately — if a region is removed from source code, the build throws a clear error (
Missing snippet 'name'), preventing stale docs from shipping silently. - Namespace with filters — use the
includeoption (e.g./^sample_\w+$/) to restrict which regions are extracted, keeping documentation snippets separate from other regions in your codebase. - Nested regions — expose both coarse and fine-grained views of the same code. A
sample_classregion can contain asample_methodregion; both are independently addressable. - Auto-dedented — common leading whitespace is stripped, so indented regions render cleanly without extra indentation in docs.
Direct File Includes
Reference files by path with an @ prefix. No region markers needed in the source.
snippet: @/src/config.json
snippet: @/src/models/user.ts#sample_interface
snippet: @/src/Example.cs {1,3,5-8 :line-numbers} [Example]Advantages:
- Zero footprint in source code — no region markers to add or maintain. Good for files you don't own or don't want to annotate.
- Include entire files — ideal for config files, scripts, or small modules where the whole file is the example.
- VitePress-compatible modifiers — line highlights (
{1,3,5-8}), attributes (:line-numbers), titles ([Example]), and language overrides are all supported using VitePress syntax. - On-demand loading — files are read at parse time, so there's no startup cost for directories you never reference.
- Relative paths —
@./pathand@../pathresolve relative to the markdown file, making co-located docs intuitive.
Inline Snippets
Define snippet content directly in your markdown. When inlineSnippets: true, the configured marker syntax wrapped in an HTML comment (<!-- snippet: name -->, <!-- <<< #name -->, etc.) becomes an inline snippet opener — content follows the marker and is closed by <!-- /snippet --> or <!-- endSnippet -->. Both the opening and closing markers are HTML comments, so they are invisible in standard markdown renderers.
<!-- snippet: my_func {python} -->
def hello():
print("hello")
<!-- /snippet -->Advantages:
- Invisible markers — both opening and closing markers are HTML comments, so they render cleanly in plain markdown viewers.
- Self-contained docs — code examples live right in the markdown file. No external source files needed.
- VitePress-compatible modifiers — line highlights, attributes, titles, and language overrides work the same as file includes.
- Anchors — each inline snippet emits a named anchor (
<a id='snippet-name'>) for in-page linking. - Opt-in — disabled by default (
inlineSnippets: false), enable when needed. File includes still work normally.
When to Use Which
| Scenario | Recommended approach | |----------|---------------------| | Code example referenced from multiple doc pages | Named region snippet | | Source files may be renamed or reorganized | Named region snippet | | Including an entire config/script file | File include | | One-off reference where adding a region marker is overkill | File include | | Need line highlights, titles, or attributes | File include or inline snippet | | Want build-time validation that examples still exist | Named region snippet | | Code example written directly in docs, no source file | Inline snippet | | Docs that must render in plain markdown viewers too | Inline snippet (wrapped fence mode) |
All three approaches can be freely mixed in the same project.
Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| rootDir | string | (required) | Absolute path to project root |
| dirs | string[] | ['.'] | Directories to scan, relative to rootDir |
| extensions | string[] | 50+ common extensions | File extensions to scan (with leading dot) |
| include | RegExp \| null | null (all) | Only extract regions whose name matches |
| exclude | string[] | [] | Path substrings to skip when scanning |
| skipDirs | string[] | — | Additional dir names to skip (merged with defaults) |
| urlPrefix | string | '' (disabled) | Source link URL prefix |
| anchor | boolean | true | Emit <a id='...'> anchor before each code block. Requires urlPrefix |
| sourceLink | boolean | true | Emit <sup> source link after each code block. Requires urlPrefix |
| langMap | Record<string, string> | auto | Override extension→language mappings |
| syntax | string \| RegExp | 'snippet-colon' | Marker syntax — preset name, keyword, or custom RegExp |
| fileIncludes | boolean | true | Enable @/path file includes. Set false to let VitePress handle <<< file includes natively |
| inlineSnippets | boolean | false | Enable inline snippet mode. Marker syntax wrapped in HTML comments (<!-- snippet: name -->) becomes an inline opener; named snippet lookup is disabled. File includes still work |
| logPrefix | string | '[region-snippets]' | Console log prefix |
| silent | boolean | false | Suppress console output |
| strict | boolean | true | Throw on missing snippets/files. Set false to emit a warning fence instead |
Scanning Controls
Directories
md.use(regionSnippetPlugin, {
rootDir,
dirs: ['src', 'samples'], // only scan these directories
})File Extensions
md.use(regionSnippetPlugin, {
rootDir,
extensions: ['.cs', '.ts', '.js'], // only scan these extensions
})The default covers 50+ extensions across .NET, JVM, JS/TS, Web, Systems, Scripting, Shell, and Config files.
Path Exclusion
md.use(regionSnippetPlugin, {
rootDir,
exclude: ['Generated', 'obj/Debug'], // skip paths containing these substrings
})Skip Directories
md.use(regionSnippetPlugin, {
rootDir,
skipDirs: ['vendor', '.cache'], // merged with built-in skip dirs
})Built-in skip dirs include node_modules, bin, obj, .git, dist, build, out, target, and more.
Marker Syntax
The default syntax is snippet: followed by the region name:
snippet: my_region_nameBuilt-in Presets
| Preset | Markdown syntax | Example |
|--------|----------------|---------|
| 'snippet-colon' (default) | snippet: name | snippet: sample_hello |
| 'triple-chevron' | <<< #name / <<< @/path | <<< #sample_hello |
| 'at-snippet' | @snippet name | @snippet sample_hello |
// Use VitePress-compatible legacy syntax
md.use(regionSnippetPlugin, {
rootDir,
syntax: 'triple-chevron',
})VitePress Coexistence
The default snippet-colon syntax uses a completely different prefix (snippet:) so there is no overlap with VitePress's built-in <<< file includes. Both work side by side with zero conflict.
If you choose the triple-chevron preset, the plugin takes over all <<< handling — both named snippets (<<< #name) and file includes (<<< @/path). This effectively replaces VitePress's built-in <<< with the plugin's richer feature set (region extraction, highlights, attributes, titles). The plugin's block rule runs before the built-in fence rule, so matching <<< lines are intercepted first.
| Syntax | Mode | Example |
|--------|------|---------|
| <<< #name | Named snippet | <<< #sample_hello |
| <<< @/path | File include | <<< @/src/file.cs |
| <<< @/path#region | File include with region | <<< @/src/file.cs#my_region |
| <<< @/path {1,3 :line-numbers} [Title] | File include with modifiers | <<< @/src/file.cs {1,3} [Example] |
To use triple-chevron for named snippets only and let VitePress handle <<< @/path file includes natively, disable the plugin's file include feature:
md.use(regionSnippetPlugin, {
rootDir,
syntax: 'triple-chevron',
fileIncludes: false, // <<< @/path falls through to VitePress
})Custom Keyword
Any string that isn't a preset name is treated as a keyword — builds a keyword: name pattern:
md.use(regionSnippetPlugin, {
rootDir,
syntax: 'code', // now use `code: my_region_name` in markdown
})Custom RegExp
For full control, pass a RegExp with exactly one capture group for the snippet name:
md.use(regionSnippetPlugin, {
rootDir,
syntax: /^include\s+(\S+)$/, // matches `include my_name`
})File Includes
In addition to named snippet lookup, the plugin supports direct file includes using an @ prefix. This lets you include entire files or specific regions — without pre-scanning. The {...} block and [Title] use VitePress-compatible syntax, so highlights, attributes, and titles are passed through to the fenced code block for Shiki rendering.
Note: File includes use the
@prefix after the marker keyword. With the default syntax you writesnippet: @/path/to/file; withtriple-chevronyou write<<< @/path/to/file— the same syntax as VitePress's built-in file includes, but with additional support for region extraction and modifiers. See VitePress Coexistence for details.
Syntax
snippet: @/path/to/file.cs # entire file
snippet: @/path/to/file.cs#region_name # region from file
snippet: @/path/to/file.cs {c#} # language override
snippet: @/path/to/file.cs {1,3,5-8} # line highlighting
snippet: @/path/to/file.cs {:line-numbers} # attributes
snippet: @/path/to/file.cs [Title] # title
snippet: @/path/to/file.cs#region{1,3,5-8 c# :line-numbers} [My Title] # all combinedPath Resolution
@/path— relative torootDir@./pathor@../path— relative to the current markdown file's directory
{...} Block
The optional {...} block can contain space-separated tokens:
- Highlights:
1,3,5-8— digits, commas, and dashes → line highlighting (VitePress/Shiki) - Language:
c#,ts, etc. — override auto-detected fence language - Attributes:
:line-numbers,:line-numbers=5— passed through to fence info
The brace block can be space-separated from the path or adjacent to a #region name:
snippet: @/file.cs#my_region{1,3}
snippet: @/file.cs#my_region {1,3}[Title]
An optional [Title] at the end sets the code block title (rendered by VitePress/Shiki):
snippet: @/src/Example.cs [Example.cs]Fence Info Output
The plugin constructs the fence info string in VitePress format:
lang{highlights}[Title] :attrsFor example, snippet: @/file.cs {1,3 c# :line-numbers} [My File] produces:
```c#{1,3}[My File] :line-numbers
// ... file content ...
```Examples
Include an entire JavaScript file:
snippet: @/src/utils/helpers.jsInclude a specific region from a TypeScript file:
snippet: @/src/models/user.ts#sample_interfaceInclude a file with highlighted lines and line numbers:
snippet: @/src/Example.cs {1,3,5-8 :line-numbers}Include a region with a title:
snippet: @./examples/demo.cs#sample_setup [Setup Code]Inline Snippets
When inlineSnippets: true, the configured marker syntax wrapped in an HTML comment (<!-- snippet: name -->, <!-- <<< #name -->, etc.) becomes an inline snippet opener. Content follows the marker line and is closed by <!-- /snippet --> or <!-- endSnippet -->. Both opening and closing markers are HTML comments, making them invisible in standard markdown renderers. Named snippet lookup from pre-scanned files is disabled. File includes (@/path) still work normally.
md.use(regionSnippetPlugin, {
rootDir,
inlineSnippets: true,
})Syntax
Opening marker — the configured marker syntax wrapped in an HTML comment, with optional modifiers:
<!-- snippet: name -->
<!-- snippet: name {python} -->
<!-- snippet: name {python 1,3,5-8 :line-numbers} [My Title] -->With the triple-chevron preset:
<!-- <<< #name {python} -->Closing marker — two forms:
<!-- /snippet -->
<!-- endSnippet -->Content Modes
The plugin auto-detects which mode is in use.
Mode A — Raw content (language specified in opening marker):
<!-- snippet: my_func {python} -->
def hello():
print("hello")
<!-- /snippet -->Mode B — Wrapped fenced code block (degrades gracefully in standard renderers):
<!-- snippet: my_func -->
```python
def hello():
print("hello")
```
<!-- /snippet -->In Mode B, the plugin unwraps the fence and emits it as a proper token. The wrapped code block also renders normally in plain markdown viewers that don't run this plugin.
Language Priority
{lang}in the opening marker (highest)- Fence language from wrapped code block (Mode B)
- Empty string (lowest)
{...} Block
Same VitePress-compatible syntax as file includes:
- Language:
python,ts,c#— sets the fence language - Highlights:
1,3,5-8— line highlighting - Attributes:
:line-numbers— passed through to fence info
[Title]
An optional [Title] at the end sets the code block title:
<!-- snippet: setup {python} [Setup Code] -->Anchors
Each inline snippet emits an anchor tag for in-page linking, gated on the anchor option (default: true):
<a id='snippet-my_func'></a>No source link is emitted since there is no external source file.
Error Handling
A missing closing marker follows the same strict / lenient behavior as other snippet types:
strict: true(default) — throws an errorstrict: false— emits a warning fence (⚠ Inline snippet 'name' has no closing marker)
Supported Region Markers
| Language | Start | End |
|----------|-------|-----|
| C# | #region name | #endregion |
| JS/TS/Java/Go | // #region name | // #endregion |
| HTML/XML | <!-- #region name --> | <!-- #endregion --> |
| CSS/C | /* #region name */ | /* #endregion */ |
| Python/Ruby/Shell | # region name | # endregion |
Nested regions are handled correctly — inner region markers are stripped from the output.
Language Map
The fence language is auto-detected from the source file extension. Common mappings include:
| Extension | Language |
|-----------|----------|
| .cs | csharp |
| .ts, .mts | ts |
| .js, .mjs | js |
| .py | python |
| .rs | rust |
| .go | go |
| .html | html |
| .css | css |
Override with the langMap option:
md.use(regionSnippetPlugin, {
rootDir,
langMap: { '.cs': 'cs' }, // use 'cs' instead of 'csharp'
})Source Links
When urlPrefix is set, each code block is wrapped with an HTML anchor and a source link pointing to the original file with line numbers. Use anchor and sourceLink to control each part independently.
md.use(regionSnippetPlugin, {
rootDir,
urlPrefix: 'https://github.com/user/repo/blob/main',
})To emit only the source link (no anchor):
md.use(regionSnippetPlugin, {
rootDir,
urlPrefix: 'https://github.com/user/repo/blob/main',
anchor: false,
})To emit only the anchor (no source link):
md.use(regionSnippetPlugin, {
rootDir,
urlPrefix: 'https://github.com/user/repo/blob/main',
sourceLink: false,
})Named snippets
Each named snippet gets an anchor before the code block and a source link after it. The source link includes a back-link to the anchor for easy cross-referencing within the page.
<!-- Before the code block -->
<a id='snippet-sample_hello'></a>
<!-- After the code block -->
<sup><a href='https://github.com/user/repo/blob/main/src/Example.cs#L3-L8'>snippet source</a> | <a href='#snippet-sample_hello'>anchor</a></sup>File includes
File includes get the same anchor and source link with a back-link.
<!-- Before the code block -->
<a id='snippet-file-/src/Example.cs'></a>
<!-- After the code block -->
<sup><a href='https://github.com/user/repo/blob/main/src/Example.cs#L1-L20'>snippet source</a> | <a href='#snippet-file-/src/Example.cs'>anchor</a></sup>Error Handling
By default (strict: true), the plugin throws at build time when a snippet or file reference can't be resolved. This means broken documentation links are caught during the build rather than silently rendering empty or stale content.
| Condition | Error message |
|-----------|---------------|
| Named snippet not found | Missing snippet 'name' in docs/page.md |
| File not found (file include) | cannot read file '/path/to/file': ENOENT |
| Region not found in file | region 'name' not found in '/path/to/file' |
If a region is removed or renamed in source code, the next build fails immediately with a clear error pointing to the markdown file that references it. This makes region snippets a reliable contract between your code and your docs — stale examples can't ship unnoticed.
Lenient mode
Set strict: false for development workflows where you want to write docs before the referenced code exists. Instead of throwing, the plugin renders a fenced code block with a visible warning (e.g. ⚠ Snippet not found: name) and logs a console.warn (unless silent: true).
md.use(regionSnippetPlugin, {
rootDir,
strict: false, // render warnings instead of crashing
})Framework Compatibility
| Framework | Supported | Notes |
|-----------|-----------|-------|
| VitePress | Yes | Primary target; fence info is VitePress/Shiki-compatible |
| VuePress | Yes | Works with any markdown-it pipeline |
| Docusaurus | Yes | Via markdown-it plugin registration |
| Plain markdown-it | Yes | Direct md.use() |
TypeScript
Full type declarations ship with the package in index.d.ts, covering all public interfaces, function signatures, and option types. No @types/ package needed.
Advanced Usage
The plugin re-exports its internal utilities for custom workflows:
import {
extractSnippetsFromFile,
loadSnippets,
findRegion,
dedent,
MARKERS,
DEFAULT_LANG_MAP,
DEFAULT_EXTENSIONS,
DEFAULT_SKIP_DIRS,
SYNTAX_PRESETS,
DEFAULT_SYNTAX,
resolveSyntax,
isFileRef,
parseFileRef,
resolveFileInclude,
parseInlineSnippetOpen,
parseInlineValue,
isInlineSnippetClose,
detectWrappedFence,
} from 'markdown-it-region-snippets'License
MIT
