rehype-highlight-code-lines
Advanced tools
Comparing version
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; |
@@ -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", |
269
src/index.ts
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
53589
18.91%709
30.09%