|
|
@@ -1,6 +1,6 @@
|
|
|
/**
|
|
|
- * @typedef {import('hast').Element} Element
|
|
|
- * @typedef {import('hast').Root} Root
|
|
|
+ * @typedef {import("hast").Element} Element
|
|
|
+ * @typedef {import("hast").Root} Root
|
|
|
* @typedef Options options
|
|
|
* Configuration.
|
|
|
* @property {boolean} [showLineNumbers]
|
|
|
@@ -12,16 +12,16 @@
|
|
|
* 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'
|
|
|
+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-') {
|
|
|
+ if (classListItem.slice(0, 9) === "language-") {
|
|
|
return classListItem.slice(9).toLowerCase()
|
|
|
}
|
|
|
}
|
|
|
@@ -29,7 +29,7 @@ const getLanguage = (node) => {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * @param {import('refractor/lib/core').Refractor} refractor
|
|
|
+ * @param {import("refractor/lib/core").Refractor} refractor
|
|
|
* @param {string} defaultLanguage
|
|
|
* @return {void}
|
|
|
*/
|
|
|
@@ -49,7 +49,7 @@ const calculateLinesToHighlight = (meta) => {
|
|
|
const RE = /{([\d,-]+)}/
|
|
|
// Remove space between {} e.g. {1, 3}
|
|
|
const parsedMeta = meta
|
|
|
- .split(',')
|
|
|
+ .split(",")
|
|
|
.map((str) => str.trim())
|
|
|
.join()
|
|
|
if (RE.test(parsedMeta)) {
|
|
|
@@ -89,8 +89,8 @@ const createLineNodes = (number) => {
|
|
|
const a = new Array(number)
|
|
|
for (let i = 0; i < number; i++) {
|
|
|
a[i] = {
|
|
|
- type: 'element',
|
|
|
- tagName: 'span',
|
|
|
+ type: "element",
|
|
|
+ tagName: "span",
|
|
|
properties: { className: [] },
|
|
|
children: [],
|
|
|
}
|
|
|
@@ -102,20 +102,20 @@ const createLineNodes = (number) => {
|
|
|
* 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'] }
|
|
|
+ * @return { (ast:Element["children"]) => Element["children"] }
|
|
|
*
|
|
|
*/
|
|
|
const addNodePositionClosure = () => {
|
|
|
let startLineNum = 1
|
|
|
/**
|
|
|
- * @param {Element['children']} ast
|
|
|
- * @return {Element['children']}
|
|
|
+ * @param {Element["children"]} ast
|
|
|
+ * @return {Element["children"]}
|
|
|
*/
|
|
|
const addNodePosition = (ast) => {
|
|
|
return ast.reduce((result, node) => {
|
|
|
- if (node.type === 'text') {
|
|
|
+ if (node.type === "text") {
|
|
|
const value = /** @type {string} */ (node.value)
|
|
|
- const numLines = (value.match(/\n/g) || '').length
|
|
|
+ const numLines = (value.match(/\n/g) || "").length
|
|
|
if (numLines === 0) {
|
|
|
node.position = {
|
|
|
// column: 1 is needed to avoid error with @next/mdx
|
|
|
@@ -125,11 +125,11 @@ const addNodePositionClosure = () => {
|
|
|
}
|
|
|
result.push(node)
|
|
|
} else {
|
|
|
- const lines = value.split('\n')
|
|
|
+ const lines = value.split("\n")
|
|
|
for (const [i, line] of lines.entries()) {
|
|
|
result.push({
|
|
|
- type: 'text',
|
|
|
- value: i === lines.length - 1 ? line : line + '\n',
|
|
|
+ type: "text",
|
|
|
+ value: i === lines.length - 1 ? line : line + "\n",
|
|
|
position: {
|
|
|
start: { line: startLineNum + i, column: 1 },
|
|
|
end: { line: startLineNum + i, column: 1 },
|
|
|
@@ -142,7 +142,7 @@ const addNodePositionClosure = () => {
|
|
|
return result
|
|
|
}
|
|
|
|
|
|
- if (Object.prototype.hasOwnProperty.call(node, 'children')) {
|
|
|
+ if (Object.prototype.hasOwnProperty.call(node, "children")) {
|
|
|
const initialLineNum = startLineNum
|
|
|
// @ts-ignore
|
|
|
node.children = addNodePosition(node.children, startLineNum)
|
|
|
@@ -161,20 +161,95 @@ const addNodePositionClosure = () => {
|
|
|
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>}
|
|
|
+ * @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)
|
|
|
+ visit(tree, "element", visitor)
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -183,15 +258,15 @@ const rehypePrismGenerator = (refractor) => {
|
|
|
* @param {Element} parent
|
|
|
*/
|
|
|
function visitor(node, index, parent) {
|
|
|
- if (!parent || parent.tagName !== 'pre' || node.tagName !== 'code') {
|
|
|
+ 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 || '')
|
|
|
+ let meta = /** @type {string} */ (node?.data?.meta || node?.properties?.metastring || "")
|
|
|
// Coerce className to array
|
|
|
if (node.properties.className) {
|
|
|
- if (typeof node.properties.className === 'boolean') {
|
|
|
+ if (typeof node.properties.className === "boolean") {
|
|
|
node.properties.className = []
|
|
|
} else if (!Array.isArray(node.properties.className)) {
|
|
|
node.properties.className = [node.properties.className]
|
|
|
@@ -201,12 +276,11 @@ const rehypePrismGenerator = (refractor) => {
|
|
|
}
|
|
|
|
|
|
let lang = getLanguage(node)
|
|
|
- // If no language is set on the code block, use defaultLanguage if specified
|
|
|
if (!lang && options.defaultLanguage) {
|
|
|
lang = options.defaultLanguage
|
|
|
node.properties.className.push(`language-${lang}`)
|
|
|
}
|
|
|
- node.properties.className.push('code-highlight')
|
|
|
+ node.properties.className.push("code-highlight")
|
|
|
|
|
|
/** @type {Element} */
|
|
|
let refractorRoot
|
|
|
@@ -215,8 +289,8 @@ const rehypePrismGenerator = (refractor) => {
|
|
|
if (lang) {
|
|
|
try {
|
|
|
let rootLang
|
|
|
- if (lang?.includes('diff-')) {
|
|
|
- rootLang = lang.split('-')[1]
|
|
|
+ if (lang?.includes("diff-")) {
|
|
|
+ rootLang = lang.split("-")[1]
|
|
|
} else {
|
|
|
rootLang = lang
|
|
|
}
|
|
|
@@ -224,7 +298,7 @@ const rehypePrismGenerator = (refractor) => {
|
|
|
refractorRoot = refractor.highlight(toString(node), rootLang)
|
|
|
// @ts-ignore className is already an array
|
|
|
parent.properties.className = (parent.properties.className || []).concat(
|
|
|
- 'language-' + rootLang
|
|
|
+ "language-" + rootLang
|
|
|
)
|
|
|
} catch (err) {
|
|
|
if (options.ignoreMissing && /Unknown language/.test(err.message)) {
|
|
|
@@ -260,13 +334,18 @@ const rehypePrismGenerator = (refractor) => {
|
|
|
const codeLineArray = createLineNodes(refractorRoot.position.end.line)
|
|
|
|
|
|
const falseShowLineNumbersStr = [
|
|
|
- 'showlinenumbers=false',
|
|
|
- 'showlinenumbers="false"',
|
|
|
- 'showlinenumbers={false}',
|
|
|
+ "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']
|
|
|
+ line.properties.className = ["code-line"]
|
|
|
|
|
|
// Syntax highlight
|
|
|
const treeExtract = filter(
|
|
|
@@ -275,39 +354,33 @@ const rehypePrismGenerator = (refractor) => {
|
|
|
)
|
|
|
line.children = treeExtract.children
|
|
|
|
|
|
- // Line number
|
|
|
- if (
|
|
|
- (meta.toLowerCase().includes('showLineNumbers'.toLowerCase()) ||
|
|
|
- options.showLineNumbers) &&
|
|
|
- !falseShowLineNumbersStr.some((str) => meta.toLowerCase().includes(str))
|
|
|
- ) {
|
|
|
+ if (shouldShowLineNumbers) {
|
|
|
line.properties.line = [(i + startingLineNumber).toString()]
|
|
|
- line.properties.className.push('line-number')
|
|
|
+ line.properties.className.push("line-number")
|
|
|
}
|
|
|
|
|
|
- // Line highlight
|
|
|
if (shouldHighlightLine(i)) {
|
|
|
- line.properties.className.push('highlight-line')
|
|
|
+ line.properties.className.push("highlight-line")
|
|
|
}
|
|
|
|
|
|
// Diff classes
|
|
|
if (
|
|
|
- (lang === 'diff' || lang?.includes('diff-')) &&
|
|
|
- toString(line).substring(0, 1) === '-'
|
|
|
+ (lang === "diff" || lang?.includes("diff-")) &&
|
|
|
+ toString(line).substring(0, 1) === "-"
|
|
|
) {
|
|
|
- line.properties.className.push('deleted')
|
|
|
+ line.properties.className.push("deleted")
|
|
|
} else if (
|
|
|
- (lang === 'diff' || lang?.includes('diff-')) &&
|
|
|
- toString(line).substring(0, 1) === '+'
|
|
|
+ (lang === "diff" || lang?.includes("diff-")) &&
|
|
|
+ toString(line).substring(0, 1) === "+"
|
|
|
) {
|
|
|
- line.properties.className.push('inserted')
|
|
|
+ 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() === ''
|
|
|
+ toString(codeLineArray[codeLineArray.length - 1]).trim() === ""
|
|
|
) {
|
|
|
codeLineArray.pop()
|
|
|
}
|