Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fair-ads-begin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"rehype-pretty-code": patch
---

fix: detect `showLineNumbers` wherever it appears in the meta string, not only before the highlight annotations, while ignoring the word when it is part of a character highlight such as `/foo showLineNumbers bar/` (#204)
31 changes: 18 additions & 13 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import rangeParser from 'parse-numeric-range';
import { unified, type Transformer } from 'unified';
import rehypeParse from 'rehype-parse';
import { charsHighlighter } from './chars/charsHighlighter';
import { reverseString } from './chars/utils';
import {
isElement,
isText,
Expand All @@ -23,6 +22,7 @@ import {
getThemeNames,
replaceLineClass,
getLineId,
getShowLineNumbers,
} from './utils';
export type { Options, LineElement, CharsElement, Theme } from './types';

Expand Down Expand Up @@ -427,24 +427,29 @@ export function rehypePrettyCode(
counterMap: new Map<string, number>(),
};

// Detect `showLineNumbers` (optionally `showLineNumbers{N}`) as a
// standalone meta token, wherever it appears in the meta string.
// Occurrences inside a character-highlight annotation, which may
// legitimately span whitespace (`/foo showLineNumbers bar/`), are
// highlight text rather than an option, so the spans those
// annotations already claim are excluded.
const showLineNumbers = getShowLineNumbers(
meta,
(charsMatches ?? []).map((match) => {
const start = match.index ?? 0;
return [start, start + match[0].length];
}),
);

// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: <explanation>
visit(codeTree, 'element', (element) => {
if (
element.tagName === 'code' &&
/srebmuNeniLwohs(?!(.*)(\/))/.test(reverseString(meta))
) {
if (element.tagName === 'code' && showLineNumbers) {
if (element.properties) {
element.properties['data-line-numbers'] = '';
}

const lineNumbersStartAtMatch = reverseString(meta).match(
/(?:\}(\d+){)?srebmuNeniLwohs(?!(.*)(\/))/,
);
const startNumberString = lineNumbersStartAtMatch?.[1];
if (startNumberString) {
const startAt = startNumberString
? Number(reverseString(startNumberString)) - 1
: 0;
if (showLineNumbers.startAt !== null) {
const startAt = showLineNumbers.startAt - 1;
lineNumbersMaxDigits = startAt;
if (element.properties) {
element.properties.style = `counter-set: line ${startAt};`;
Expand Down
37 changes: 37 additions & 0 deletions packages/core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,43 @@ export function replaceLineClass(element: Element) {
}
}

// `showLineNumbers`, optionally `showLineNumbers{N}`, bounded by
// whitespace or the ends of the meta string. Only lookahead is used, so
// this stays parseable on Safari < 16.4, which lacks lookbehind.
const showLineNumbersRegex = /(?:^|\s)(showLineNumbers(?:\{(\d+)\})?)(?=\s|$)/g;

/**
* Finds the `showLineNumbers` option in a block meta string.
*
* Whitespace boundaries alone do not prove the token is an option: it can
* also be part of a delimited character-highlight annotation, as in
* `/foo showLineNumbers bar/`, where it is highlight text. `takenSpans`
* holds the `[start, end)` offsets already claimed by those annotations,
* and any match overlapping one of them is skipped.
*
* Returns `null` when the option is absent, otherwise `startAt` — the
* line number given by `showLineNumbers{N}`, or `null` for a bare token.
*/
export function getShowLineNumbers(
meta: string,
takenSpans: Array<[number, number]>,
) {
for (const match of meta.matchAll(showLineNumbersRegex)) {
const token = match[1];
if (!token) continue;

// `match[0]` may carry a leading whitespace boundary character.
const start = (match.index ?? 0) + (match[0].length - token.length);
const end = start + token.length;

if (takenSpans.some(([from, to]) => start < to && end > from)) continue;

return { startAt: match[2] ? Number(match[2]) : null };
}

return null;
}

export function getLineId(lineNumber: number, meta: string) {
const segments = meta.match(/\{[^}]+\}#[a-zA-Z0-9]+/g);
if (!segments) return null;
Expand Down
33 changes: 33 additions & 0 deletions packages/core/test/fixtures/showLineNumbersAfterHighlight.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

105 changes: 105 additions & 0 deletions packages/core/test/results/showLineNumbersAfterHighlight.html

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading