| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393 |
- /**
- * @typedef {import("hast").Element} Element
- * @typedef {import("hast").Root} Root
- * @typedef Options options
- * Configuration.
- * @property {boolean} [showLineNumbers]
- * Set `showLineNumbers` to `true` to always display line number
- * @property {boolean} [ignoreMissing]
- * Set `ignoreMissing` to `true` to ignore unsupported languages and line highlighting when no language is specified
- * @property {string} [defaultLanguage]
- * Uses the specified language as the default if none is specified. Takes precedence over `ignoreMissing`.
- * Note: The language must be registered with refractor.
- */
- import { visit } from "unist-util-visit"
- import { toString } from "hast-util-to-string"
- import { filter } from "unist-util-filter"
- import rangeParser from "parse-numeric-range"
- const getLanguage = (node) => {
- const className = node.properties.className
- //@ts-ignore
- for (const classListItem of className) {
- if (classListItem.slice(0, 9) === "language-") {
- return classListItem.slice(9).toLowerCase()
- }
- }
- return null
- }
- /**
- * @param {import("refractor/lib/core").Refractor} refractor
- * @param {string} defaultLanguage
- * @return {void}
- */
- const checkIfLanguageIsRegistered = (refractor, defaultLanguage) => {
- if (defaultLanguage && !refractor.registered(defaultLanguage)) {
- throw new Error(`The default language "${defaultLanguage}" is not registered with refractor.`)
- }
- }
- /**
- * Create a closure that determines if we have to highlight the given index
- *
- * @param {string} meta
- * @return { (index:number) => boolean }
- */
- const calculateLinesToHighlight = (meta) => {
- const RE = /{([\d,-]+)}/
- // Remove space between {} e.g. {1, 3}
- const parsedMeta = meta
- .split(",")
- .map((str) => str.trim())
- .join()
- if (RE.test(parsedMeta)) {
- const strlineNumbers = RE.exec(parsedMeta)[1]
- const lineNumbers = rangeParser(strlineNumbers)
- return (index) => lineNumbers.includes(index + 1)
- } else {
- return () => false
- }
- }
- /**
- * Check if we want to start the line numbering from a given number or 1
- * showLineNumbers=5, will start the numbering from 5
- * @param {string} meta
- * @returns {number}
- */
- const calculateStartingLine = (meta) => {
- const RE = /showLineNumbers=(?<lines>\d+)/i
- // pick the line number after = using a named capturing group
- if (RE.test(meta)) {
- const {
- groups: { lines },
- } = RE.exec(meta)
- return Number(lines)
- }
- return 1
- }
- /**
- * Create container AST for node lines
- *
- * @param {number} number
- * @return {Element[]}
- */
- const createLineNodes = (number) => {
- const a = new Array(number)
- for (let i = 0; i < number; i++) {
- a[i] = {
- type: "element",
- tagName: "span",
- properties: { className: [] },
- children: [],
- }
- }
- return a
- }
- /**
- * Split multiline text nodes into individual nodes with positioning
- * Add a node start and end line position information for each text node
- *
- * @return { (ast:Element["children"]) => Element["children"] }
- *
- */
- const addNodePositionClosure = () => {
- let startLineNum = 1
- /**
- * @param {Element["children"]} ast
- * @return {Element["children"]}
- */
- const addNodePosition = (ast) => {
- return ast.reduce((result, node) => {
- if (node.type === "text") {
- const value = /** @type {string} */ (node.value)
- const numLines = (value.match(/\n/g) || "").length
- if (numLines === 0) {
- node.position = {
- // column: 1 is needed to avoid error with @next/mdx
- // https://github.com/timlrx/rehype-prism-plus/issues/44
- start: { line: startLineNum, column: 1 },
- end: { line: startLineNum, column: 1 },
- }
- result.push(node)
- } else {
- const lines = value.split("\n")
- for (const [i, line] of lines.entries()) {
- result.push({
- type: "text",
- value: i === lines.length - 1 ? line : line + "\n",
- position: {
- start: { line: startLineNum + i, column: 1 },
- end: { line: startLineNum + i, column: 1 },
- },
- })
- }
- }
- startLineNum = startLineNum + numLines
- return result
- }
- if (Object.prototype.hasOwnProperty.call(node, "children")) {
- const initialLineNum = startLineNum
- // @ts-ignore
- node.children = addNodePosition(node.children, startLineNum)
- result.push(node)
- node.position = {
- start: { line: initialLineNum, column: 1 },
- end: { line: startLineNum, column: 1 },
- }
- return result
- }
- result.push(node)
- return result
- }, [])
- }
- return addNodePosition
- }
- /**
- * @param {Element} parent
- * @param {string} meta
- */
- const createToolbarElement = (parent, meta) => {
- const toolbarShowRx = /toolbar-.*/
- if (!toolbarShowRx.test(meta)) {
- return
- }
- const toolbarTitleRx = /toolbar-title="(.*)"/
- const toolbarTitle = toolbarTitleRx.exec(meta)[1]
- parent.children.push({
- type: "element",
- tagName: "div",
- properties: { className: ["toolbar-container"] },
- children: [
- {
- type: "element",
- tagName: "div",
- properties: { className: ["toolbar"] },
- children: [
- {
- type: "element",
- tagName: "div",
- properties: { className: ["toolbar-title"] },
- children: [
- {
- type: "text",
- value: toolbarTitle,
- }
- ]
- },
- {
- type: "element",
- tagName: "div",
- properties: {
- className: ["toolbar-buttons"],
- },
- children: [
- {
- type: "element",
- tagName: "button",
- properties: {
- className: ["toolbar-download-code-button"],
- },
- children: [
- {
- type: "text",
- value: "DOWNLOAD"
- },
- ]
- },
- {
- type: "element",
- tagName: "button",
- properties: {
- className: ["toolbar-copy-code-button"],
- },
- children: [
- {
- type: "text",
- value: "COPY"
- },
- ]
- }
- ]
- }
- ],
- }
- ],
- })
- }
- /**
- * Rehype prism plugin generator that highlights code blocks with refractor (prismjs)
- *
- * Pass in your own refractor object with the required languages registered:
- * https://github.com/wooorm/refractor#refractorregistersyntax
- *
- * @param {import("refractor/lib/core").Refractor} refractor
- * @return {import("unified").Plugin<[Options?], Root>}
- */
- const rehypePrismGenerator = (refractor) => {
- return (options = {}) => {
- checkIfLanguageIsRegistered(refractor, options.defaultLanguage)
- return (tree) => {
- visit(tree, "element", visitor)
- }
- /**
- * @param {Element} node
- * @param {number} index
- * @param {Element} parent
- */
- function visitor(node, index, parent) {
- if (!parent || parent.tagName !== "pre" || node.tagName !== "code") {
- return
- }
- // @ts-ignore meta is a custom code block property
- let meta = /** @type {string} */ (node?.data?.meta || node?.properties?.metastring || "")
- // Coerce className to array
- if (node.properties.className) {
- if (typeof node.properties.className === "boolean") {
- node.properties.className = []
- } else if (!Array.isArray(node.properties.className)) {
- node.properties.className = [node.properties.className]
- }
- } else {
- node.properties.className = []
- }
- let lang = getLanguage(node)
- if (!lang && options.defaultLanguage) {
- lang = options.defaultLanguage
- node.properties.className.push(`language-${lang}`)
- }
- node.properties.className.push("code-highlight")
- /** @type {Element} */
- let refractorRoot
- // Syntax highlight
- if (lang) {
- try {
- let rootLang
- if (lang?.includes("diff-")) {
- rootLang = lang.split("-")[1]
- } else {
- rootLang = lang
- }
- // @ts-ignore
- refractorRoot = refractor.highlight(toString(node), rootLang)
- // @ts-ignore className is already an array
- parent.properties.className = (parent.properties.className || []).concat(
- "language-" + rootLang
- )
- } catch (err) {
- if (options.ignoreMissing && /Unknown language/.test(err.message)) {
- refractorRoot = node
- } else {
- throw err
- }
- }
- } else {
- refractorRoot = node
- }
- refractorRoot.children = addNodePositionClosure()(refractorRoot.children)
- // Add position info to root
- if (refractorRoot.children.length > 0) {
- refractorRoot.position = {
- start: { line: refractorRoot.children[0].position.start.line, column: 0 },
- end: {
- line: refractorRoot.children[refractorRoot.children.length - 1].position.end.line,
- column: 0,
- },
- }
- } else {
- refractorRoot.position = {
- start: { line: 0, column: 0 },
- end: { line: 0, column: 0 },
- }
- }
- const shouldHighlightLine = calculateLinesToHighlight(meta)
- const startingLineNumber = calculateStartingLine(meta)
- const codeLineArray = createLineNodes(refractorRoot.position.end.line)
- const falseShowLineNumbersStr = [
- "showlinenumbers=false",
- "showlinenumbers=\"false\"",
- "showlinenumbers={false}",
- ]
- const shouldShowLineNumbers = (meta.toLowerCase().includes("showLineNumbers".toLowerCase())
- || options.showLineNumbers) && !falseShowLineNumbersStr.some((str) => meta.toLowerCase().includes(str))
- createToolbarElement(parent, meta)
- for (const [i, line] of codeLineArray.entries()) {
- // Default class name for each line
- line.properties.className = ["code-line"]
- // Syntax highlight
- const treeExtract = filter(
- refractorRoot,
- (node) => node.position.start.line <= i + 1 && node.position.end.line >= i + 1
- )
- line.children = treeExtract.children
- if (shouldShowLineNumbers) {
- line.properties.line = [(i + startingLineNumber).toString()]
- line.properties.className.push("line-number")
- }
- if (shouldHighlightLine(i)) {
- line.properties.className.push("highlight-line")
- }
- // Diff classes
- if (
- (lang === "diff" || lang?.includes("diff-")) &&
- toString(line).substring(0, 1) === "-"
- ) {
- line.properties.className.push("deleted")
- } else if (
- (lang === "diff" || lang?.includes("diff-")) &&
- toString(line).substring(0, 1) === "+"
- ) {
- line.properties.className.push("inserted")
- }
- }
- // Remove possible trailing line when splitting by \n which results in empty array
- if (
- codeLineArray.length > 0 &&
- toString(codeLineArray[codeLineArray.length - 1]).trim() === ""
- ) {
- codeLineArray.pop()
- }
- node.children = codeLineArray
- }
- }
- }
- export default rehypePrismGenerator
|