| id | 232 |
|---|---|
| title | include heading-offset parameter |
| status | ✅ |
| summary | Add a `heading-offset` parameter to the `<?include?>` directive (MDS021) that shifts every heading in the embedded content by a signed integer. It complements the context-relative `heading-level: "absolute"` and works even when no heading precedes the marker. |
| model | opus |
| depends-on |
Give <?include?> a way to shift the headings in
an embedded file by a fixed amount. The content then nests
at the depth you pick. It works even with no heading before
the marker.
<?include?> already adjusts headings with
heading-level: "absolute". That mode shifts the embedded
headings so the shallowest one sits one level below the
nearest preceding heading. The math lives in
adjustHeadings.
The mode is context-relative: it reads the parent heading
level from the host document. When the marker sits at the
document root, there is no parent. adjustHeadings then
returns the content unchanged, so the embedded file keeps
its source levels.
That leaves a gap. Take a README whose visual title is a
logo rather than an H1. A file embedded at the top of it
cannot have its headings demoted: the source # Title
stays an H1 and outranks the host's own ## sections. The
author needs a way to say "shift these down by one" that
does not depend on a parent heading.
A signed integer. It shifts every heading in the embedded
content by that amount. The idea matches Pandoc's
--shift-heading-level-by and AsciiDoc's leveloffset.
<?include
file: features.md
heading-offset: "1"
?>
## Was An H1 In The Source
<?/include?>
| Value | Effect |
|---|---|
"1" |
every heading one level deeper, H1→H2 |
"-1" |
every heading one level shallower |
"0" |
no change |
Levels are clamped to the 1–6 range after the shift, the
same cap absolute already applies. The sign may carry an
explicit +, as in "+1".
heading-level: "absolute" is relative to the host: the
shift depends on the parent heading. heading-offset is
relative to the source: the shift is fixed and applies even
at the document root. They are two strategies for one goal,
so they are mutually exclusive. Setting both is a lint
error.
heading-offset shifts every heading by the same amount,
upward too for a negative value. So it reuses the existing
applyShift helper directly. It skips the
"no shift when the result is ≤ 0" guard that adjustHeadings
needs for parent-relative nesting.
- The value must parse as an integer from −6 to 6. Anything else is a lint error.
heading-offsetcannot pair withheading-level, since the two are rival strategies.heading-offsetcannot pair withextract:, which returns one scalar and has no headings. This mirrors the ruleheading-levelalready follows.
- An absolute base form that pins the shallowest heading to
level N. It overlaps
heading-offsetfor single-rooted content, so it waits for a real need. - Stacking
heading-offsetonabsolute. The two stay mutually exclusive to keep the model simple. - Using the new syntax in any tracked Markdown file. Per
the directive-syntax process, the pinned CI
baseline must ship the feature first, or
mdsmith-fixed-versionbreaks. This plan adds the capability only. Adoption is a later, separate change.
- Add
adjustHeadingsByOffset(content, offset)toheadings.go, reusingapplyShift. Unit tests for ATX, setext, code fences, clamping, and theoffset == 0no-op land inheadings_test.gofirst, red. - Apply
heading-offsetinprocessIncludedContentinrule.go: calladjustHeadingsByOffsetwhen the parameter is present. - Extend
validateIncludeDirective: reject a non-integer or out-of-range value, reject the pairing withheading-level, and reject the pairing withextract. A unit test pins each branch. - Add fixtures under MDS021-include: a
good/plus matchingfixed/pair that exercisesheading-offset, and abad/case with a stale body. - Update the rule README (MDS021-include): the parameter table, a heading-offset section, an example, and the new diagnostics rows.
- Update the directive guide
(generating content) and the built-in help doc
internal/directives/generating-content.mdto coverheading-offset. - Run
go test ./...,go tool golangci-lint run, the allocation-budget test, andmdsmith check ..
-
heading-offset: "1"demotes every embedded heading one level onmdsmith fix, even with no heading before the marker -
heading-offset: "-1"promotes every embedded heading one level, clamped at H1 - A stale
heading-offsetbody reports the MDS021 "generated section is out of date" diagnostic oncheck - Pairing
heading-offsetwithheading-levelorextractis a lint error with a clear message - A non-integer or out-of-range
heading-offsetis a lint error - No tracked Markdown uses the new syntax, so
mdsmith-fixed-versionstays green - All tests pass:
go test ./... -
go tool golangci-lint runreports no issues