fix(docs): pair backtick runs per line instead of counting parity

The skip used a running parity counter over every non-fenced backtick in the
file, so one unpaired backtick flipped the parity for everything after it and
the run still printed clean. `docker-compose.mdx` has 49 non-fenced backticks
and is already in that state, which means the tail of that file was
unreachable.

Backtick runs open and close within a line and pair by length, so that is what
the skip now computes. Three cases pin it: a stray backtick no longer blinds a
later placeholder, a run of two closes a run of two around an inner tick, and a
backtick at end of line does not open a span into the next.
This commit is contained in:
Félix Malfait committed 2026-08-22 07:28:09 +00:00
1 parent 1bccb736ae
commit 54dff6e4e5
2 files changed
+63 -11

No files matched your search

@@ -90,4 +90,28 @@ describe('findAngleBracketPlaceholders', () => {
'second-id',
]);
});
// A running parity counter would treat every later match as inline code.
it('is not blinded by an unpaired backtick earlier in the file', () => {
const violations = findAngleBracketPlaceholders(
'a stray ` backtick\n\nthen use <workspace-id> here',
);
expect(violations).toHaveLength(1);
expect(violations[0].name).toBe('workspace-id');
});
it('pairs backtick runs by length', () => {
expect(
findAngleBracketPlaceholders('``code with ` tick and <env>``'),
).toEqual([]);
});
it('does not treat a backtick on a previous line as opening a span', () => {
const violations = findAngleBracketPlaceholders(
'ends with a tick `\nnext line has <api-key>',
);
expect(violations).toHaveLength(1);
expect(violations[0].name).toBe('api-key');
});
});
+39 -11
View File
@@ -125,20 +125,47 @@ const getFencedCodeRanges = (text: string): Range[] => {
const isInsideRange = (position: number, ranges: Range[]) =>
ranges.some((range) => position >= range.start && position < range.end);
const isInsideInlineCode = (
position: number,
text: string,
fencedRanges: Range[],
) => {
let backtickCount = 0;
// Backtick runs are paired within a line, never across one. A running parity
// counter would let a single unpaired backtick silently suppress every finding
// in the rest of the file.
const getInlineCodeRanges = (text: string, fencedRanges: Range[]): Range[] => {
const ranges: Range[] = [];
let lineStart = 0;
for (let index = 0; index < position; index++) {
if (text[index] === '`' && !isInsideRange(index, fencedRanges)) {
backtickCount++;
for (const line of text.split('\n')) {
const runs: { start: number; length: number }[] = [];
for (const match of line.matchAll(/`+/g)) {
const start = lineStart + match.index;
if (!isInsideRange(start, fencedRanges)) {
runs.push({ start, length: match[0].length });
}
}
const unclosed: typeof runs = [];
for (const run of runs) {
const openerIndex = unclosed.findIndex(
(candidate) => candidate.length === run.length,
);
if (openerIndex === -1) {
unclosed.push(run);
continue;
}
ranges.push({
start: unclosed[openerIndex].start,
end: run.start + run.length,
});
unclosed.splice(0, openerIndex + 1);
}
lineStart += line.length + 1;
}
return backtickCount % 2 !== 0;
return ranges;
};
const getLineAndColumn = (text: string, position: number) => {
@@ -153,6 +180,7 @@ const getLineAndColumn = (text: string, position: number) => {
export const findAngleBracketPlaceholders = (text: string): MdxViolation[] => {
const fencedRanges = getFencedCodeRanges(text);
const inlineCodeRanges = getInlineCodeRanges(text, fencedRanges);
const violations: MdxViolation[] = [];
for (const match of text.matchAll(PLACEHOLDER_PATTERN)) {
@@ -169,7 +197,7 @@ export const findAngleBracketPlaceholders = (text: string): MdxViolation[] => {
if (
isInsideRange(position, fencedRanges) ||
isInsideInlineCode(position, text, fencedRanges)
isInsideRange(position, inlineCodeRanges)
) {
continue;
}