Skip to content

Commit 2fbf13e

Browse files
patrickerclaude
andcommitted
feat: add diff regions, diff-step, and tab groups
- Diff between two regions: reference= + diff-reference= with unified, inline-annotations, and side-by-side output modes - Tutorial step diffs: diff-step= shows current step with change annotations relative to a previous step - Tab groups: consecutive tab="Label" fences are wrapped in tabGroup nodes; tab-group="id" provides cross-group sync (like Docusaurus groupId); companions transform into framework-specific tabs - Refactored visitor into resolveRef() helper for DRY reference resolution across normal, diff, and diff-step modes - Added negative lookbehinds to REF_REGEX/FILE_REGEX to prevent matching inside diff-reference=/diff-file= Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6ae74df commit 2fbf13e

10 files changed

Lines changed: 1454 additions & 113 deletions

File tree

README.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,86 @@ Inequality and negation are kept natural: `assert x != 0` becomes `# x != 0`.
298298

299299
Transmutation is opt-in and runs before strip. Transmuted lines become comments and skip stripping. Unmatched lines fall through to strip as usual. Use `?noTransmute` to disable per block.
300300

301+
## Diff between two regions
302+
303+
Show a highlighted diff between two tested code states -- for migration guides, changelogs, and "what changed in v2" docs.
304+
305+
````markdown
306+
```python diff reference="tests/test_api.py#v1_handler" diff-reference="#v2_handler"
307+
```
308+
````
309+
310+
The primary `reference=` is the "before", `diff-reference=` is the "after". Same-file shorthand (`#v2_handler`) inherits the file from the primary. Cross-file diffs work too.
311+
312+
Three output modes (set globally via `diffFormat` option or per-block with `?format=`):
313+
314+
**`unified` (default)** -- standard diff format, `lang` set to `"diff"`:
315+
316+
```diff
317+
def handler(request):
318+
- return Response(request.body)
319+
+ validate(request)
320+
+ return Response(request.body, status=200)
321+
```
322+
323+
**`inline-annotations`** -- Shiki `[!code ++]` / `[!code --]` markers, original language preserved:
324+
325+
```python
326+
return Response(request.body) // [!code --]
327+
validate(request) // [!code ++]
328+
return Response(request.body, status=200) // [!code ++]
329+
```
330+
331+
**`side-by-side`** -- emits two sibling code nodes with `data-diff-role="before"` / `"after"` for custom component rendering.
332+
333+
Both sides go through the full cleaning pipeline (strip, transmute, dedent) independently before diffing.
334+
335+
```js
336+
remarkPlugins: [[codeRegion, { diffFormat: 'inline-annotations' }]],
337+
```
338+
339+
Per-block override:
340+
341+
````markdown
342+
```python reference="file.py#v1?format=inline-annotations" diff-reference="#v2"
343+
```
344+
````
345+
346+
The `diff-file=` syntax works alongside `file=` for remark-code-import compatible paths.
347+
348+
## Step-by-step tutorial diffs
349+
350+
For tutorials where each step builds on the previous, use `diff-step` to show the current step's complete code with highlights on what changed:
351+
352+
````markdown
353+
First, create the basic app:
354+
355+
```python reference="tests/tutorial.py#step1"
356+
```
357+
358+
Now add configuration:
359+
360+
```python reference="tests/tutorial.py#step2" diff-step="step1"
361+
```
362+
363+
Finally, add authentication:
364+
365+
```python reference="tests/tutorial.py#step3" diff-step="step2"
366+
```
367+
````
368+
369+
**Step 1** renders as plain code. **Step 2** renders with `// [!code ++]` on lines added since step 1. **Step 3** shows what changed since step 2. Each block is a standalone code fence with its own prose context.
370+
371+
The reader sees complete, copyable code at every step -- with green/red highlights showing exactly what's new. Every step is a passing test.
372+
373+
`diff-step` defaults to `inline-annotations` (Shiki highlighting). Override with `?format=unified` for +/- diff output.
374+
375+
| | `diff-reference` | `diff-step` |
376+
|---|---|---|
377+
| Use case | Migration guide (before/after) | Tutorial (step-by-step) |
378+
| Output | Diff only | Full current code with change annotations |
379+
| Default format | `unified` (+/- prefixes) | `inline-annotations` (Shiki highlights) |
380+
301381
## Auto-dedent
302382

303383
Extracted regions are automatically dedented. Common leading whitespace is removed so that code nested inside test functions or classes renders flush-left in your docs.
@@ -361,6 +441,7 @@ remarkPlugins: [[codeRegion, { clean: false }]],
361441
| `clean` | `string[]` \| `false` | `['dedent', 'collapse', 'trim']` | Whitespace cleaning steps. `false` disables. `string[]` for custom list. |
362442
| `regionSeparator` | `string` | `'\n\n'` | String inserted between regions in multi-region concatenation. |
363443
| `preserveFileMeta` | `boolean` | `false` | Keep `file=` in meta after processing (matches remark-code-import behavior). |
444+
| `diffFormat` | `string` | `'unified'` | Output format for diff blocks: `'unified'`, `'inline-annotations'`, or `'side-by-side'`. |
364445

365446
**Exports** (for composing custom configurations):
366447

@@ -477,6 +558,8 @@ remark-code-import dropped `require()` support in v1.0.0. If your Docusaurus con
477558
| **Multi-region** | No | Yes (`#imports,create_user`) |
478559
| **Strip test lines** | No | Auto-strips asserts, expects, test-only markers |
479560
| **Transmute assertions** | No | Transforms assertions into `=> value` comments |
561+
| **Diff two regions** | No | `diff-reference=` with unified, inline-annotation, or side-by-side output |
562+
| **Tutorial step diffs** | No | `diff-step=` shows current code with change highlights from previous step |
480563
| **Auto-dedent** | Opt-in | On by default (configurable) |
481564
| **Composable clean pipeline** | No | `PRESET_CLEAN.default` / `.compat` / custom |
482565
| **Security boundary** | `allowImportingFromOutside` | `allowOutsideRoot` (default: false) |

0 commit comments

Comments
 (0)