Skip to content

Commit f67aba4

Browse files
patrickerclaude
andcommitted
feat!: hardening pass — dedent, security, simplified API, unclosed detection
Breaking changes (pre-1.0, zero users): - strip option replaces stripPatterns + replaceDefaultStrip + keepAsserts (strip: RegExp[] | false | undefined) - ?noStrip replaces ?keepAsserts as per-block override - attribute option removed (hardcoded to "reference") New features: - Auto-dedent: common leading whitespace stripped from extracted regions - Security boundary: allowOutsideRoot defaults to false, preventing references from escaping rootDir - Unclosed region detection: build fails if region opened but never closed - file.fail() for errors with markdown source position context - Proper ?query flag parsing (split on ?, not includes/replace) Build: - CJS generated from ESM via esbuild (eliminates manual duplication) - npm run build generates index.cjs from index.mjs + lib/*.mjs - prepublishOnly runs build + test README rewrite: - Documented new strip API with PRESET_STRIP composition - Added auto-dedent section with before/after example - Added MDX compatibility section - Added VitePress incompatibility note - Added false-positive caveat for strip patterns - Added incremental migration note - Updated comparison table (auto-dedent and security boundary parity) - Updated options table and exports table 61 tests, all passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3064bff commit f67aba4

10 files changed

Lines changed: 478 additions & 275 deletions

README.md

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ If the test breaks, CI fails. If the region moves, the build fails. **Stale docs
3737

3838
## Quick start
3939

40+
Code fences without `reference` are untouched -- migrate one block at a time.
41+
4042
Install:
4143

4244
```bash
@@ -197,10 +199,12 @@ These patterns are removed from injected code by default:
197199
| `go` | `if err != nil { t.Fatal` | Go |
198200
| `markers` | Lines ending with `// test-only` or `# test-only` | Any |
199201

200-
To keep assertions visible in a specific block, append `?keepAsserts`:
202+
The `assert` and `expect()` patterns match lines starting with these keywords. If your library uses these as API calls (not test assertions), pass a custom `strip` list using `PRESET_STRIP` to include only the languages you need.
203+
204+
To keep assertions visible in a specific block, append `?noStrip`:
201205

202206
````markdown
203-
```python reference="tests/test_users.py#create_user?keepAsserts"
207+
```python reference="tests/test_users.py#create_user?noStrip"
204208
```
205209
````
206210

@@ -214,46 +218,72 @@ const { PRESET_STRIP } = codeRegion;
214218

215219
remarkPlugins: [[codeRegion, {
216220
// Only strip Python asserts, JS expects, and test-only markers
217-
stripPatterns: [...PRESET_STRIP.python, ...PRESET_STRIP.js, ...PRESET_STRIP.markers],
218-
replaceDefaultStrip: true,
221+
strip: [...PRESET_STRIP.python, ...PRESET_STRIP.js, ...PRESET_STRIP.markers],
219222
}]],
220223
```
221224

222225
Available presets: `python`, `rust`, `java`, `js`, `cpp`, `go`, `markers`.
223226

224-
You can also add custom patterns alongside the defaults (no `replaceDefaultStrip` needed):
227+
You can also add custom patterns alongside the defaults:
225228

226229
```js
230+
const codeRegion = require('remark-code-region');
231+
const { PRESET_STRIP } = codeRegion;
232+
227233
remarkPlugins: [[codeRegion, {
228-
// Add to defaults — strip lines matching your custom test helper
229-
stripPatterns: [/^\s*check\(/, /^\s*verify\(/],
234+
// Custom list — defaults plus your own test helpers
235+
strip: [
236+
...PRESET_STRIP.python,
237+
...PRESET_STRIP.js,
238+
...PRESET_STRIP.markers,
239+
/^\s*check\(/,
240+
/^\s*verify\(/,
241+
],
230242
}]],
231243
```
232244

233245
Or disable stripping entirely:
234246

235247
```js
236-
remarkPlugins: [[codeRegion, { keepAsserts: true }]],
248+
remarkPlugins: [[codeRegion, { strip: false }]],
249+
```
250+
251+
## Auto-dedent
252+
253+
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. No option needed -- this is always on.
254+
255+
For example, code indented inside a test function:
256+
257+
```python
258+
def test_create_user():
259+
# region: create_user
260+
from myapp import client
261+
user = client.create_user(name="Alice")
262+
# endregion: create_user
263+
```
264+
265+
...renders as:
266+
267+
```python
268+
from myapp import client
269+
user = client.create_user(name="Alice")
237270
```
238271

239272
## Options
240273

241274
| Option | Type | Default | Description |
242275
|---|---|---|---|
243276
| `rootDir` | `string` | `process.cwd()` | Base directory for resolving reference paths. |
277+
| `allowOutsideRoot` | `boolean` | `false` | Allow references to files outside `rootDir`. Disabled by default as a security boundary. |
244278
| `regionMarkers` | `{start, end}[]` | `DEFAULT_REGION_MARKERS` | Region marker pairs. Each `start`/`end` is a RegExp where group 1 captures the region name. |
245-
| `stripPatterns` | `RegExp[]` | `[]` | Patterns to strip. Merged with defaults unless `replaceDefaultStrip` is true. |
246-
| `replaceDefaultStrip` | `boolean` | `false` | If true, `stripPatterns` replaces the built-in defaults instead of merging. |
247-
| `keepAsserts` | `boolean` | `false` | Disable assertion stripping globally. |
248-
| `attribute` | `string` | `'reference'` | The code fence meta attribute name to look for. |
279+
| `strip` | `RegExp[]` \| `false` | `undefined` (defaults) | Patterns to strip from injected code. `undefined` uses the built-in defaults. `false` disables stripping. `RegExp[]` replaces the defaults with your custom list. |
249280

250281
**Exports** (for composing custom configurations):
251282

252283
| Export | What it is |
253284
|---|---|
254285
| `DEFAULT_REGION_MARKERS` | Default region marker pairs (`#` and `//` comments) |
255286
| `PRESET_MARKERS` | Additional markers: `.css`, `.html`, `.sql` |
256-
| `DEFAULT_STRIP_PATTERNS` | All built-in strip patterns (union of all presets) |
257287
| `PRESET_STRIP` | Strip patterns by language: `.python`, `.rust`, `.java`, `.js`, `.cpp`, `.go`, `.markers` |
258288

259289
```js
@@ -264,8 +294,7 @@ remarkPlugins: [[codeRegion, {
264294
codeRegion.PRESET_MARKERS.css,
265295
codeRegion.PRESET_MARKERS.sql,
266296
],
267-
stripPatterns: [...codeRegion.PRESET_STRIP.python, ...codeRegion.PRESET_STRIP.js],
268-
replaceDefaultStrip: true,
297+
strip: [...codeRegion.PRESET_STRIP.python, ...codeRegion.PRESET_STRIP.js],
269298
}]],
270299
```
271300

@@ -277,8 +306,18 @@ If a referenced file is missing or a region doesn't exist, **the build fails**:
277306
Error: remark-code-region: region 'create_user' not found in tests/test_users.py
278307
```
279308

309+
If a region is opened but never closed, **the build fails**:
310+
311+
```
312+
Error: remark-code-region: region 'create_user' in tests/test_users.py was opened but never closed
313+
```
314+
280315
This is intentional. Silent stale docs are worse than a build error.
281316

317+
## MDX compatibility
318+
319+
Works inside MDX components (`<Tabs>`, admonitions) -- the plugin runs at the remark AST level, before MDX processing. Any code fence with a `reference` attribute will be resolved, regardless of where it sits in the markdown tree.
320+
282321
## Framework support
283322

284323
### Docusaurus
@@ -324,19 +363,23 @@ const result = await remark()
324363
.process(markdown);
325364
```
326365

366+
VitePress uses markdown-it (not remark) and is not compatible.
367+
327368
## Why not remark-code-import?
328369

329370
[remark-code-import](https://github.com/kevin940726/remark-code-import) includes code from files using line ranges (`#L3-L6`). It's good for static inclusion, but:
330371

331372
| Feature | remark-code-import | remark-code-region |
332373
|---|---|---|
333374
| Include from file | `file=./path.js#L3-L6` | `reference="path.py#region_name"` |
334-
| **Named regions** | No | Yes stable across edits |
375+
| **Named regions** | No | Yes -- stable across edits |
335376
| **Strip test lines** | No | Auto-strips asserts, expects, test-only markers |
377+
| **Auto-dedent** | Yes | Yes |
378+
| **Security boundary** | Yes | Yes (`allowOutsideRoot` defaults to false) |
336379
| Fail on missing file | Yes | Yes |
337380
| Line ranges | Yes | No (regions don't shift when code is edited) |
338381

339-
Both plugins fail on missing files. The key difference is **how you target code**. Line ranges (`#L3-L6`) shift every time you add or remove a line above them. Named regions are anchored by markers in the source edit freely above or below, the region stays correct. And auto-stripping test assertions is what makes "code lives in test files" practical without it, you'd need separate display-only copies.
382+
Both plugins fail on missing files. The key difference is **how you target code**. Line ranges (`#L3-L6`) shift every time you add or remove a line above them. Named regions are anchored by markers in the source -- edit freely above or below, the region stays correct. And auto-stripping test assertions is what makes "code lives in test files" practical -- without it, you'd need separate display-only copies.
340383

341384
## License
342385

0 commit comments

Comments
 (0)