New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

rehype-highlight-code-lines

Package Overview
Dependencies
Maintainers
0
Versions
19
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

rehype-highlight-code-lines - npm Package Compare versions

Comparing version

to
1.0.7

5

dist/esm/index.d.ts
import type { Plugin } from "unified";
import type { Root } from "hast";
declare module "hast" {
interface Data {
meta?: string | undefined;
}
}
export type HighlightLinesOptions = {

@@ -4,0 +9,0 @@ showLineNumbers?: boolean;

188

dist/esm/index.js

@@ -11,2 +11,20 @@ import { visit } from "unist-util-visit";

}
// check if it is string array
function isStringArray(value) {
return (
// type-coverage:ignore-next-line
Array.isArray(value) && value.every((item) => typeof item === "string"));
}
// check if it is Element which first child is text
function isElementWithTextNode(node) {
return (node?.type === "element" && node.children[0]?.type === "text" && "value" in node.children[0]);
}
function hasClassName(node, className) {
return (node?.type === "element" &&
isStringArray(node.properties.className) &&
node.properties.className.some((cls) => cls.includes(className)));
}
// match all common types of line breaks
const REGEX_LINE_BREAKS = /\r?\n|\r/g;
const REGEX_LINE_BREAKS_IN_THE_BEGINNING = /^(\r?\n|\r)/;
/**

@@ -24,8 +42,8 @@ *

*/
function checkCodeTreeForFlatteningNeed(code) {
function hasFlatteningNeed(code) {
const elementContents = code.children;
// type ElementContent = Comment | Element | Text
for (const elemenContent of elementContents) {
if (elemenContent.type === "element")
if (elemenContent.children.length >= 1 && elemenContent.children[0].type === "element")
if (elemenContent.type === "element" && Boolean(elemenContent.children.length))
if (elemenContent.children.some((ec) => ec.type === "element"))
return true;

@@ -37,3 +55,4 @@ }

*
* flatten code element children, recursively
* flatten deeper nodes into first level <span> and text, especially for languages like jsx, tsx
* mutates the code, recursively
* inspired from https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/src/highlight.js

@@ -71,10 +90,6 @@ *

*/
const createLine = (children, lineNumber, startingNumber, directiveShowLineNumbers, linesToBeHighlighted) => {
const createLine = (children, lineNumber, directiveShowLineNumbers, directiveHighlightLines) => {
const firstChild = children[0];
const isAddition = firstChild?.type === "element" &&
Array.isArray(firstChild.properties.className) &&
firstChild.properties.className.some((cls) => typeof cls === "string" && cls.includes("addition"));
const isDeletion = firstChild?.type === "element" &&
Array.isArray(firstChild.properties.className) &&
firstChild.properties.className.some((cls) => typeof cls === "string" && cls.includes("deletion"));
const isAddition = hasClassName(firstChild, "addition");
const isDeletion = hasClassName(firstChild, "deletion");
return {

@@ -88,14 +103,74 @@ type: "element",

directiveShowLineNumbers && "numbered-code-line",
linesToBeHighlighted.includes(lineNumber) && "highlighted-code-line",
directiveHighlightLines.includes(lineNumber) && "highlighted-code-line",
isAddition && "inserted",
isDeletion && "deleted",
]),
dataLineNumber: directiveShowLineNumbers ? startingNumber - 1 + lineNumber : undefined,
dataLineNumber: typeof directiveShowLineNumbers === "number"
? directiveShowLineNumbers - 1 + lineNumber
: directiveShowLineNumbers
? lineNumber
: undefined,
},
};
};
// match all common types of line breaks
const REGEX_LINE_BREAKS = /\r?\n|\r/g;
/**
*
* handle elements which is multi line comment in code
* mutates the code
*
*/
function handleMultiLineComments(code) {
for (let index = 0; index < code.children.length; index++) {
const replacement = [];
const child = code.children[index];
if (!isElementWithTextNode(child))
continue;
if (!hasClassName(child, "comment"))
continue;
const textNode = child.children[0];
if (!REGEX_LINE_BREAKS.test(textNode.value))
continue;
const comments = textNode.value.split(REGEX_LINE_BREAKS);
for (let i = 0; i < comments.length; i++) {
const newChild = structuredClone(child);
newChild.children[0].value = comments[i];
replacement.push(newChild);
if (i < comments.length - 1) {
replacement.push({ type: "text", value: "\n" }); // eol
}
}
code.children = [
...code.children.slice(0, index),
...replacement,
...code.children.slice(index + 1),
];
}
}
/**
*
* handle eol characters in the beginning of the only first element
* (because gutter function does not check the first element in the HAST whether contain eol at the beginning)
* mutates the code
*
*/
function handleFirstElementContent(code) {
const replacement = [];
const elementNode = code.children[0];
if (!isElementWithTextNode(elementNode))
return;
const textNode = elementNode.children[0];
if (!REGEX_LINE_BREAKS_IN_THE_BEGINNING.test(textNode.value))
return;
let match = REGEX_LINE_BREAKS_IN_THE_BEGINNING.exec(textNode.value);
while (match !== null) {
replacement.push({ type: "text", value: match[0] });
// Update the child value
textNode.value = textNode.value.slice(1);
// iterate the match
match = REGEX_LINE_BREAKS_IN_THE_BEGINNING.exec(textNode.value);
}
code.children.unshift(...replacement);
}
/**
*
* check the code line is empty or with value only spaces

@@ -108,10 +183,21 @@ *

}
function gutter(tree, directiveShowLineNumbers, startingNumber, linesToBeHighlighted) {
/**
*
* extract the lines from HAST of code element
* mutates the code
*
*/
function gutter(code, { directiveShowLineNumbers, directiveHighlightLines, }) {
hasFlatteningNeed(code) && flattenCodeTree(code); // mutates the code
handleMultiLineComments(code); // mutates the code
handleFirstElementContent(code); // mutates the code
const replacement = [];
let index = -1;
let start = 0;
let startTextRemainder = "";
let lineNumber = 0;
while (++index < tree.children.length) {
const child = tree.children[index];
for (let index = 0; index < code.children.length; index++) {
const child = code.children[index];
// if (index === 0 && /^[\n][\s]*$/.test(child.value)) {
// console.log(child.value, index);
// }
if (child.type !== "text")

@@ -123,3 +209,4 @@ continue;

// Nodes in this line. (current child is exclusive)
const line = tree.children.slice(start, index);
// Array.prototype.slice() start to end (end not included)
const line = code.children.slice(start, index);
// Prepend text from a partial matched earlier text.

@@ -135,8 +222,4 @@ if (startTextRemainder) {

}
if (!isEmptyLine(line)) {
lineNumber += 1;
replacement.push(createLine(line, lineNumber, startingNumber, directiveShowLineNumbers, linesToBeHighlighted));
}
// Add eol
replacement.push({ type: "text", value: match[0] });
lineNumber += 1;
replacement.push(createLine(line, lineNumber, directiveShowLineNumbers, directiveHighlightLines), { type: "text", value: match[0] });
start = index + 1;

@@ -152,3 +235,3 @@ textStart = match.index + match[0].length;

}
const line = tree.children.slice(start);
const line = code.children.slice(start);
// Prepend text from a partial matched earlier text.

@@ -159,10 +242,13 @@ if (startTextRemainder) {

}
if (!isEmptyLine(line)) {
if (line.length > 0) {
if (line.length > 0) {
if (isEmptyLine(line)) {
replacement.push(...line);
}
else {
lineNumber += 1;
replacement.push(createLine(line, lineNumber, startingNumber, directiveShowLineNumbers, linesToBeHighlighted));
replacement.push(createLine(line, lineNumber, directiveShowLineNumbers, directiveHighlightLines));
}
}
// Replace children with new array.
tree.children = replacement;
code.children = replacement;
}

@@ -196,4 +282,4 @@ /**

return (tree) => {
visit(tree, "element", function (node, index, parent) {
if (!parent || index === undefined || node.tagName !== "code") {
visit(tree, "element", function (code, index, parent) {
if (!parent || index === undefined || code.tagName !== "code") {
return;

@@ -204,9 +290,8 @@ }

}
const code = node;
const classNames = code.properties.className;
// only for type narrowing
/* v8 ignore next */
if (!Array.isArray(classNames) && classNames !== undefined)
if (!isStringArray(classNames) && classNames !== undefined)
return;
let meta = code.data?.meta?.toLowerCase().trim() || "";
let meta = code.data?.meta?.toLowerCase().trim() ?? "";
const language = getLanguage(classNames);

@@ -218,29 +303,28 @@ if (language?.startsWith("{") ||

meta = (language + " " + meta).trim();
// remove all classnames like hljs, lang-x, language-x, because of false positive
// correct the code's meta
code.data && (code.data.meta = meta);
// remove all classnames like hljs, lang-{1,3}, language-showLineNumbers, because of false positive
code.properties.className = undefined;
}
const directiveShowLineNumbers = meta.includes("nolinenumbers")
let directiveShowLineNumbers = meta.includes("nolinenumbers")
? false
: settings.showLineNumbers || meta.includes("showlinenumbers");
let startingNumber = 1;
// find the number where the line number starts, if exists
if (directiveShowLineNumbers) {
const REGEX1 = /showlinenumbers=(?<start>\d+)/i;
const start = REGEX1.exec(meta)?.groups?.start;
if (start && !isNaN(Number(start)))
startingNumber = Number(start);
}
const REGEX1 = /showlinenumbers=(?<start>\d+)/i;
const start = REGEX1.exec(meta)?.groups?.start;
if (start && !isNaN(Number(start)))
directiveShowLineNumbers = Number(start);
// find number range string within curly braces and parse it
const REGEX2 = /{(?<lines>[\d\s,-]+)}/g;
const strLineNumbers = REGEX2.exec(meta)?.groups?.lines?.replace(/\s/g, "");
const linesToBeHighlighted = strLineNumbers ? rangeParser(strLineNumbers) : [];
// if nothing to do for numbering and highlihting, just return
if (!directiveShowLineNumbers && linesToBeHighlighted.length === 0)
const directiveHighlightLines = strLineNumbers ? rangeParser(strLineNumbers) : [];
// if nothing to do for numbering, highlihting or trimming blank lines, just return;
if (directiveShowLineNumbers === false && directiveHighlightLines.length === 0) {
return;
// flatten deeper nodes into first level <span> and text, especially for languages like jsx, tsx
if (checkCodeTreeForFlatteningNeed(code)) {
flattenCodeTree(code);
}
// add container for each line mutating the code element
gutter(code, directiveShowLineNumbers, startingNumber, linesToBeHighlighted);
gutter(code, {
directiveShowLineNumbers,
directiveHighlightLines,
});
});

@@ -247,0 +331,0 @@ };

{
"name": "rehype-highlight-code-lines",
"version": "1.0.5",
"version": "1.0.7",
"description": "Rehype plugin to add line numbers to code blocks and allow highlighting of desired code lines",

@@ -16,3 +16,4 @@ "type": "module",

"test:watch": "vitest",
"test:file": "vitest test.html.spec.ts",
"test:file1": "vitest test.markdown.spec.ts",
"test:file2": "vitest test.cases.spec.ts",
"prepack": "npm run build",

@@ -55,12 +56,12 @@ "prepublishOnly": "npm run test && npm run format && npm run test-coverage",

"devDependencies": {
"@eslint/js": "^9.19.0",
"@eslint/js": "^9.20.0",
"@types/dedent": "^0.7.2",
"@types/node": "^22.13.1",
"@vitest/coverage-v8": "^3.0.5",
"@vitest/eslint-plugin": "^1.1.25",
"@vitest/eslint-plugin": "^1.1.27",
"dedent": "^1.5.3",
"eslint": "^9.19.0",
"eslint": "^9.20.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.3",
"prettier": "^3.4.2",
"prettier": "^3.5.0",
"rehype": "^13.0.2",

@@ -67,0 +68,0 @@ "rehype-highlight": "^7.0.2",

import type { Plugin } from "unified";
import type { Root, Element, ElementContent, ElementData } from "hast";
import type { Root, Element, Text, ElementContent } from "hast";
import { visit, type VisitorResult } from "unist-util-visit";

@@ -10,2 +10,12 @@ import rangeParser from "parse-numeric-range";

declare module "hast" {
interface Data {
meta?: string | undefined;
}
}
type ElementWithTextNode = Element & {
children: [Text, ...ElementContent[]];
};
export type HighlightLinesOptions = {

@@ -29,6 +39,2 @@ showLineNumbers?: boolean;

type CodeData = ElementData & {
meta?: string;
};
// a simple util for our use case, like clsx package

@@ -39,2 +45,29 @@ export function clsx(arr: (string | false | null | undefined | 0)[]): string[] {

// check if it is string array
function isStringArray(value: unknown): value is string[] {
return (
// type-coverage:ignore-next-line
Array.isArray(value) && value.every((item) => typeof item === "string")
);
}
// check if it is Element which first child is text
function isElementWithTextNode(node: ElementContent | undefined): node is ElementWithTextNode {
return (
node?.type === "element" && node.children[0]?.type === "text" && "value" in node.children[0]
);
}
function hasClassName(node: ElementContent | undefined, className: string): boolean {
return (
node?.type === "element" &&
isStringArray(node.properties.className) &&
node.properties.className.some((cls) => cls.includes(className))
);
}
// match all common types of line breaks
const REGEX_LINE_BREAKS = /\r?\n|\r/g;
const REGEX_LINE_BREAKS_IN_THE_BEGINNING = /^(\r?\n|\r)/;
/**

@@ -57,3 +90,3 @@ *

*/
function checkCodeTreeForFlatteningNeed(code: Element): boolean {
function hasFlatteningNeed(code: Element): boolean {
const elementContents = code.children;

@@ -63,5 +96,4 @@

for (const elemenContent of elementContents) {
if (elemenContent.type === "element")
if (elemenContent.children.length >= 1 && elemenContent.children[0].type === "element")
return true;
if (elemenContent.type === "element" && Boolean(elemenContent.children.length))
if (elemenContent.children.some((ec) => ec.type === "element")) return true;
}

@@ -74,3 +106,4 @@

*
* flatten code element children, recursively
* flatten deeper nodes into first level <span> and text, especially for languages like jsx, tsx
* mutates the code, recursively
* inspired from https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/src/highlight.js

@@ -115,22 +148,9 @@ *

lineNumber: number,
startingNumber: number,
directiveShowLineNumbers: boolean,
linesToBeHighlighted: number[],
directiveShowLineNumbers: boolean | number,
directiveHighlightLines: number[],
): Element => {
const firstChild = children[0];
const isAddition = hasClassName(firstChild, "addition");
const isDeletion = hasClassName(firstChild, "deletion");
const isAddition =
firstChild?.type === "element" &&
Array.isArray(firstChild.properties.className) &&
firstChild.properties.className.some(
(cls) => typeof cls === "string" && cls.includes("addition"),
);
const isDeletion =
firstChild?.type === "element" &&
Array.isArray(firstChild.properties.className) &&
firstChild.properties.className.some(
(cls) => typeof cls === "string" && cls.includes("deletion"),
);
return {

@@ -144,7 +164,12 @@ type: "element",

directiveShowLineNumbers && "numbered-code-line",
linesToBeHighlighted.includes(lineNumber) && "highlighted-code-line",
directiveHighlightLines.includes(lineNumber) && "highlighted-code-line",
isAddition && "inserted",
isDeletion && "deleted",
]),
dataLineNumber: directiveShowLineNumbers ? startingNumber - 1 + lineNumber : undefined,
dataLineNumber:
typeof directiveShowLineNumbers === "number"
? directiveShowLineNumbers - 1 + lineNumber
: directiveShowLineNumbers
? lineNumber
: undefined,
},

@@ -154,7 +179,72 @@ };

// match all common types of line breaks
const REGEX_LINE_BREAKS = /\r?\n|\r/g;
/**
*
* handle elements which is multi line comment in code
* mutates the code
*
*/
function handleMultiLineComments(code: Element): undefined {
for (let index = 0; index < code.children.length; index++) {
const replacement: ElementContent[] = [];
const child = code.children[index];
if (!isElementWithTextNode(child)) continue;
if (!hasClassName(child, "comment")) continue;
const textNode = child.children[0];
if (!REGEX_LINE_BREAKS.test(textNode.value)) continue;
const comments = textNode.value.split(REGEX_LINE_BREAKS);
for (let i = 0; i < comments.length; i++) {
const newChild = structuredClone(child);
newChild.children[0].value = comments[i];
replacement.push(newChild);
if (i < comments.length - 1) {
replacement.push({ type: "text", value: "\n" }); // eol
}
}
code.children = [
...code.children.slice(0, index),
...replacement,
...code.children.slice(index + 1),
];
}
}
/**
*
* handle eol characters in the beginning of the only first element
* (because gutter function does not check the first element in the HAST whether contain eol at the beginning)
* mutates the code
*
*/
function handleFirstElementContent(code: Element): undefined {
const replacement: ElementContent[] = [];
const elementNode = code.children[0];
if (!isElementWithTextNode(elementNode)) return;
const textNode = elementNode.children[0];
if (!REGEX_LINE_BREAKS_IN_THE_BEGINNING.test(textNode.value)) return;
let match = REGEX_LINE_BREAKS_IN_THE_BEGINNING.exec(textNode.value);
while (match !== null) {
replacement.push({ type: "text", value: match[0] });
// Update the child value
textNode.value = textNode.value.slice(1);
// iterate the match
match = REGEX_LINE_BREAKS_IN_THE_BEGINNING.exec(textNode.value);
}
code.children.unshift(...replacement);
}
/**
*
* check the code line is empty or with value only spaces

@@ -170,11 +260,26 @@ *

/**
*
* extract the lines from HAST of code element
* mutates the code
*
*/
function gutter(
tree: Element,
directiveShowLineNumbers: boolean,
startingNumber: number,
linesToBeHighlighted: number[],
code: Element,
{
directiveShowLineNumbers,
directiveHighlightLines,
}: {
directiveShowLineNumbers: boolean | number;
directiveHighlightLines: number[];
},
) {
hasFlatteningNeed(code) && flattenCodeTree(code); // mutates the code
handleMultiLineComments(code); // mutates the code
handleFirstElementContent(code); // mutates the code
const replacement: ElementContent[] = [];
let index = -1;
let start = 0;

@@ -184,5 +289,9 @@ let startTextRemainder = "";

while (++index < tree.children.length) {
const child = tree.children[index];
for (let index = 0; index < code.children.length; index++) {
const child = code.children[index];
// if (index === 0 && /^[\n][\s]*$/.test(child.value)) {
// console.log(child.value, index);
// }
if (child.type !== "text") continue;

@@ -195,3 +304,4 @@

// Nodes in this line. (current child is exclusive)
const line = tree.children.slice(start, index);
// Array.prototype.slice() start to end (end not included)
const line = code.children.slice(start, index);

@@ -210,18 +320,8 @@ // Prepend text from a partial matched earlier text.

if (!isEmptyLine(line)) {
lineNumber += 1;
replacement.push(
createLine(
line,
lineNumber,
startingNumber,
directiveShowLineNumbers,
linesToBeHighlighted,
),
);
}
lineNumber += 1;
replacement.push(
createLine(line, lineNumber, directiveShowLineNumbers, directiveHighlightLines),
{ type: "text", value: match[0] }, // eol
);
// Add eol
replacement.push({ type: "text", value: match[0] });
start = index + 1;

@@ -240,3 +340,3 @@ textStart = match.index + match[0].length;

const line = tree.children.slice(start);
const line = code.children.slice(start);

@@ -249,13 +349,9 @@ // Prepend text from a partial matched earlier text.

if (!isEmptyLine(line)) {
if (line.length > 0) {
if (line.length > 0) {
if (isEmptyLine(line)) {
replacement.push(...line);
} else {
lineNumber += 1;
replacement.push(
createLine(
line,
lineNumber,
startingNumber,
directiveShowLineNumbers,
linesToBeHighlighted,
),
createLine(line, lineNumber, directiveShowLineNumbers, directiveHighlightLines),
);

@@ -266,3 +362,3 @@ }

// Replace children with new array.
tree.children = replacement;
code.children = replacement;
}

@@ -302,4 +398,4 @@

return (tree: Root): undefined => {
visit(tree, "element", function (node, index, parent): VisitorResult {
if (!parent || index === undefined || node.tagName !== "code") {
visit(tree, "element", function (code, index, parent): VisitorResult {
if (!parent || index === undefined || code.tagName !== "code") {
return;

@@ -312,4 +408,2 @@ }

const code = node;
const classNames = code.properties.className;

@@ -319,5 +413,5 @@

/* v8 ignore next */
if (!Array.isArray(classNames) && classNames !== undefined) return;
if (!isStringArray(classNames) && classNames !== undefined) return;
let meta = (code.data as CodeData)?.meta?.toLowerCase().trim() || "";
let meta = code.data?.meta?.toLowerCase().trim() ?? "";

@@ -334,18 +428,17 @@ const language = getLanguage(classNames);

// remove all classnames like hljs, lang-x, language-x, because of false positive
// correct the code's meta
code.data && (code.data.meta = meta);
// remove all classnames like hljs, lang-{1,3}, language-showLineNumbers, because of false positive
code.properties.className = undefined;
}
const directiveShowLineNumbers = meta.includes("nolinenumbers")
let directiveShowLineNumbers: boolean | number = meta.includes("nolinenumbers")
? false
: settings.showLineNumbers || meta.includes("showlinenumbers");
let startingNumber = 1;
// find the number where the line number starts, if exists
if (directiveShowLineNumbers) {
const REGEX1 = /showlinenumbers=(?<start>\d+)/i;
const start = REGEX1.exec(meta)?.groups?.start;
if (start && !isNaN(Number(start))) startingNumber = Number(start);
}
const REGEX1 = /showlinenumbers=(?<start>\d+)/i;
const start = REGEX1.exec(meta)?.groups?.start;
if (start && !isNaN(Number(start))) directiveShowLineNumbers = Number(start);

@@ -355,14 +448,14 @@ // find number range string within curly braces and parse it

const strLineNumbers = REGEX2.exec(meta)?.groups?.lines?.replace(/\s/g, "");
const linesToBeHighlighted = strLineNumbers ? rangeParser(strLineNumbers) : [];
const directiveHighlightLines = strLineNumbers ? rangeParser(strLineNumbers) : [];
// if nothing to do for numbering and highlihting, just return
if (!directiveShowLineNumbers && linesToBeHighlighted.length === 0) return;
// flatten deeper nodes into first level <span> and text, especially for languages like jsx, tsx
if (checkCodeTreeForFlatteningNeed(code)) {
flattenCodeTree(code);
// if nothing to do for numbering, highlihting or trimming blank lines, just return;
if (directiveShowLineNumbers === false && directiveHighlightLines.length === 0) {
return;
}
// add container for each line mutating the code element
gutter(code, directiveShowLineNumbers, startingNumber, linesToBeHighlighted);
gutter(code, {
directiveShowLineNumbers,
directiveHighlightLines,
});
});

@@ -369,0 +462,0 @@ };

Sorry, the diff of this file is not supported yet