From 09b49fbb8ff73ace2f3d56abf8ced2548e6dc9f5 Mon Sep 17 00:00:00 2001 From: "val.istar.guo" Date: Thu, 12 Jan 2023 14:26:23 +0800 Subject: [PATCH] feat: build-in line-numbers module --- package.json | 5 ++- src/append-class-name.ts | 13 +++++++ src/checker.ts | 14 +++++++ src/get-lang.ts | 7 ++++ src/get-line-number.ts | 4 ++ src/index.ts | 74 ++++++++----------------------------- src/parse-code-vistor.ts | 57 ++++++++++++++++++++++++++++ src/pre-element-selector.ts | 7 ++++ src/rehype-prism-options.ts | 14 +++++++ 9 files changed, 135 insertions(+), 60 deletions(-) create mode 100644 src/append-class-name.ts create mode 100644 src/checker.ts create mode 100644 src/get-lang.ts create mode 100644 src/get-line-number.ts create mode 100644 src/parse-code-vistor.ts create mode 100644 src/pre-element-selector.ts create mode 100644 src/rehype-prism-options.ts diff --git a/package.json b/package.json index eab3808..f464e5e 100644 --- a/package.json +++ b/package.json @@ -90,11 +90,12 @@ "@types/node": ">12", "@types/prismjs": "^1.16.6", "@types/unist": "*", + "hastscript": "^7.2.0", "prismjs": "^1.24.1", "rehype-parse": "^7 || ^ 8", "unist-util-is": "^4 || ^5", - "unist-util-select": "^4", - "unist-util-visit": "^3 || ^4" + "unist-util-select": "^4.0.2", + "unist-util-visit": "^4.1.1" }, "peerDependencies": { "unified": "^10" diff --git a/src/append-class-name.ts b/src/append-class-name.ts new file mode 100644 index 0000000..d3cecb5 --- /dev/null +++ b/src/append-class-name.ts @@ -0,0 +1,13 @@ +import { Element } from 'hast' + + +export function appendClassName(node: Element, className: string): void { + if (!node.properties) node.properties = {} + if (!node.properties.className) node.properties.className = [] + + if (typeof node.properties.className === 'string') node.properties.className = [node.properties.className] + if (typeof node.properties.className === 'number') node.properties.className = [node.properties.className] + if (typeof node.properties.className === 'boolean') node.properties.className = [] + + node.properties.className.push(className) +} diff --git a/src/checker.ts b/src/checker.ts new file mode 100644 index 0000000..947db74 --- /dev/null +++ b/src/checker.ts @@ -0,0 +1,14 @@ +import { Node } from 'unist' +import { Element } from 'hast' +import { Text } from 'mdast' + + +export class Checker { + public static isElement(node?: Node | null): node is Element { + return !!node && node.type === 'element' && 'tagName' in node + } + + public static isText(node: Node | null): node is Text { + return !!node && node.type === 'text' + } +} diff --git a/src/get-lang.ts b/src/get-lang.ts new file mode 100644 index 0000000..7ac421d --- /dev/null +++ b/src/get-lang.ts @@ -0,0 +1,7 @@ +import { Element } from 'hast' + + +export function getLang(node: Element): string { + const lang: string = node.properties?.className?.[0] || '' + return lang.replace(/^language-/, '') +} diff --git a/src/get-line-number.ts b/src/get-line-number.ts new file mode 100644 index 0000000..f16e62f --- /dev/null +++ b/src/get-line-number.ts @@ -0,0 +1,4 @@ +export function getLineNumber(str: string): number { + const match = str.match(/\n(?!$)/g) + return match ? match.length + 1 : 1 +} diff --git a/src/index.ts b/src/index.ts index b788255..6dcc02c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,69 +1,27 @@ -import Prism from 'prismjs' -import parse from 'rehype-parse' -import unifiedTypes, { unified } from 'unified' -import * as mdast from 'mdast' -import * as hast from 'hast' -import { Node } from 'unist' +import 'prismjs' +import unifiedTypes from 'unified' +import { Element } from 'hast' import { visit } from 'unist-util-visit' -import { select } from 'unist-util-select' +import { preElementSelector } from './pre-element-selector' +import { RehypePrismOptions } from './rehype-prism-options' +import { parseCodeVisitor } from './parse-code-vistor' +import { Test } from 'unist-util-is' -const getLang = (node: hast.Element): string => { - const lang: string = node.properties?.className?.[0] || '' - return lang.replace(/^language-/, '') -} - -const visitor = (preNode: hast.Element): void => { - const codeNode = select('[tagName=code]', preNode) as (hast.Element | null) - - if (!codeNode) return - const lang = getLang(codeNode) - - if (!lang || !Prism.languages[lang]) return - - const textNode = select('text', codeNode) as (mdast.Text | null) - if (!textNode) return - - const className = `language-${lang}` - const html = Prism.highlight(textNode.value, Prism.languages[lang], lang) - const tree = unified() - .use(parse, { fragment: true }) - .parse(html) as unknown as hast.Element - - if (!preNode.properties) preNode.properties = {} - if (!preNode.properties.className) preNode.properties.className = [] - if (typeof preNode.properties.className === 'string') preNode.properties.className = [preNode.properties.className] - if (typeof preNode.properties.className === 'number') preNode.properties.className = [preNode.properties.className] - if (typeof preNode.properties.className === 'boolean') preNode.properties.className = [] - preNode.properties.className = [...preNode.properties.className, className] - - codeNode.children = tree.children -} - -const selector = (node: hast.Element): boolean => node.tagName === 'pre' - - -export interface RehypePrismOptions { - plugins: ( - 'autolinker'| 'autoloader' | 'command-line' | 'copy-to-clipboard' | - 'custom-class' | 'data-uri-highlight' | 'diff-highlight' | - 'download-button' | 'file-highlight' | 'filter-highlight-all' | - 'highlight-keywords' | 'inline-color' | 'jsonp-highlight' | - 'keep-markup' | 'line-highlight' | 'line-numbers' | 'match-braces' | - 'normalize-whitespace' | 'previewers' | 'remove-initial-line-feed' | - 'show-invisibles' | 'show-language' | 'toolbar' | 'treeview' | - 'unescaped-markup' | 'wpd' - )[] -} - -const rehypePrism: unifiedTypes.Plugin<[RehypePrismOptions?]> = (options?: RehypePrismOptions) => { +const rehypePrism: unifiedTypes.Plugin<[RehypePrismOptions?], Element> = (options?: RehypePrismOptions) => { if (options && options.plugins) { - for (const plugin of options.plugins) { + const plugins = options.plugins.filter(plugin => plugin !== 'line-numbers') + + for (const plugin of plugins) { import(`prismjs/plugins/${plugin}/prism-${plugin}.js`) } } - return (tree: Node) => visit(tree, selector as any, visitor as any) + return tree => visit( + tree, + preElementSelector(), + parseCodeVisitor(options), + ) } export default rehypePrism diff --git a/src/parse-code-vistor.ts b/src/parse-code-vistor.ts new file mode 100644 index 0000000..0023f78 --- /dev/null +++ b/src/parse-code-vistor.ts @@ -0,0 +1,57 @@ +import Prism from 'prismjs' +import rehypeParse from 'rehype-parse' +import { Element, Parent, ElementContent } from 'hast' +import { Text } from 'mdast' +import { Visitor } from 'unist-util-visit/complex-types' +import { unified } from 'unified' +import { h } from 'hastscript' +import { select } from 'unist-util-select' +import { getLang } from './get-lang' +import { Checker } from './checker' +import { appendClassName } from './append-class-name' +import { getLineNumber } from './get-line-number' +import { RehypePrismOptions } from './rehype-prism-options' + + +const parser = unified() + .use(rehypeParse, { fragment: true }) + +export function parseCodeVisitor(options?: RehypePrismOptions): Visitor { + return node => { + if (!Checker.isElement(node) || node.tagName !== 'pre') return + const preNode = node + + const codeNode = select('[tagName=code]', preNode) + if (!Checker.isElement(codeNode)) return + + const lang = getLang(codeNode) + if (!lang || !Prism.languages[lang]) return + + const textNode = select('text', codeNode) as (Text | null) + if (!Checker.isText(textNode)) return + + const codeText = textNode.value + + const html = Prism.highlight(codeText, Prism.languages[lang], lang) + const tree = parser.parse(html) as unknown as Element + + appendClassName(preNode, `language-${lang}`) + + codeNode.children = [...tree.children] + + if (options?.plugins?.includes('line-numbers')) { + appendClassName(preNode, 'line-numbers') + + const lineNumber = getLineNumber(codeText) + const lineNumberColumn = h( + 'span', + { + 'aria-hidden': 'true', + className: ['line-numbers-rows'], + }, + new Array(lineNumber).fill(h('span')), + ) + codeNode.children.push(lineNumberColumn) + } + } +} diff --git a/src/pre-element-selector.ts b/src/pre-element-selector.ts new file mode 100644 index 0000000..7f51c03 --- /dev/null +++ b/src/pre-element-selector.ts @@ -0,0 +1,7 @@ +import { Test } from 'unist-util-is' +import { Checker } from './checker' + + +export function preElementSelector(): Test { + return node => Checker.isElement(node) && node.tagName === 'pre' +} diff --git a/src/rehype-prism-options.ts b/src/rehype-prism-options.ts new file mode 100644 index 0000000..15f9238 --- /dev/null +++ b/src/rehype-prism-options.ts @@ -0,0 +1,14 @@ +export type RehypePrismPlugin = ( + 'autolinker'| 'autoloader' | 'command-line' | 'copy-to-clipboard' | + 'custom-class' | 'data-uri-highlight' | 'diff-highlight' | + 'download-button' | 'file-highlight' | 'filter-highlight-all' | + 'highlight-keywords' | 'inline-color' | 'jsonp-highlight' | + 'keep-markup' | 'line-highlight' | 'line-numbers' | 'match-braces' | + 'normalize-whitespace' | 'previewers' | 'remove-initial-line-feed' | + 'show-invisibles' | 'show-language' | 'toolbar' | 'treeview' | + 'unescaped-markup' | 'wpd' +) + +export interface RehypePrismOptions { + plugins?: RehypePrismPlugin[] +}