diff --git a/package.json b/package.json index c77efb8..21bd12d 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "rehype-attr", "version": "2.0.3", "description": "New syntax to add attributes to Markdown.", + "homepage": "https://jaywcjlove.github.io/rehype-attr", "author": "Kenny Wong ", "license": "MIT", "sideEffects": false, @@ -36,25 +37,18 @@ "unified" ], "jest": { - "testMatch": [ - "/test/*.{ts,tsx}" - ], - "coverageReporters": [ - "lcov", - "json-summary" - ], - "collectCoverageFrom": [ - "/src/*.{tsx,ts}" - ], "transformIgnorePatterns": [ "/node_modules/?!(.*)" ] }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, "dependencies": { - "unified": "10.1.0" + "unified": "10.1.0", + "unist-util-visit": "4.1.0" }, "devDependencies": { - "@types/jest": "27.0.1", "rehype": "12.0.0", "rehype-raw": "6.1.0", "rehype-stringify": "9.0.2", diff --git a/src/index.ts b/src/index.ts index f6684b2..fadde67 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ -import { Plugin, Transformer } from 'unified'; -import { Parent, NodeData, Node } from 'unist'; -import visit from './visit'; +import { Plugin } from 'unified'; +import { Root, Element, Comment, Properties, Literal } from 'hast'; +import { visit } from 'unist-util-visit'; import { propertiesHandle, nextChild, prevChild, getCommentObject } from './utils'; export type RehypeAttrsOptions = { @@ -42,40 +42,41 @@ export type RehypeAttrsOptions = { *

text

* ``` */ - properties: 'data' | 'string' | 'attr' + properties: 'data' | 'string' | 'attr'; } const defaultOptions: RehypeAttrsOptions = { - properties: 'data' + properties: 'data', } -const rehypeAttrs: Plugin<[RehypeAttrsOptions?]> = (options): Transformer => { +const rehypeAttrs: Plugin<[RehypeAttrsOptions?], Root> = (options) => { const opts = { ...defaultOptions, ...options } - return transformer; - function transformer(tree: Node>): void { - // ????? any - visit(tree as any, 'element', (node: NodeData, index: number, parent: NodeData) => { - const codeNode = node && node.children && Array.isArray(node.children) && node.children[0] - if (node.tagName === 'pre' && codeNode && codeNode.tagName === 'code' && Array.isArray(parent.children) && parent.children.length > 1) { - const child = prevChild(parent.children, index) - if (child) { - const attr = getCommentObject(child) - if (Object.keys(attr).length > 0) { - node.properties = { ...(node.properties as any), ...{ 'data-type': 'rehyp' } } - codeNode.properties = propertiesHandle(codeNode.properties, attr, opts.properties) + return (tree) => { + visit(tree, 'element', (node, index, parent) => { + if (node.tagName === 'pre' && node && Array.isArray(node.children) && parent && Array.isArray(parent.children) && parent.children.length > 1) { + const firstChild = node.children[0] as Element; + if (firstChild && firstChild.tagName === 'code' && typeof index === 'number') { + const child = prevChild(parent.children as Literal[], index); + if (child) { + const attr = getCommentObject(child); + if (Object.keys(attr).length > 0) { + node.properties = { ...node.properties, ...{ 'data-type': 'rehyp' } } + firstChild.properties = propertiesHandle(firstChild.properties, attr, opts.properties) as Properties + } } } } - if (/^(em|strong|b|a|i|p|pre|kbd|blockquote|h(1|2|3|4|5|6)|code|table|img|del|ul|ol)$/.test(node.tagName as string) && Array.isArray(parent.children)) { + + if (/^(em|strong|b|a|i|p|pre|kbd|blockquote|h(1|2|3|4|5|6)|code|table|img|del|ul|ol)$/.test(node.tagName) && parent && Array.isArray(parent.children) && typeof index === 'number') { const child = nextChild(parent.children, index) if (child) { - const attr = getCommentObject(child) + const attr = getCommentObject(child as Comment) if (Object.keys(attr).length > 0) { - node.properties = propertiesHandle(node.properties as any, attr, opts.properties) + node.properties = propertiesHandle(node.properties, attr, opts.properties) as Properties } } } - }) + }); } } diff --git a/src/utils.ts b/src/utils.ts index 05865b9..71c14dd 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,48 +1,44 @@ -import { Parent, NodeData } from 'unist'; +import { Element, Comment, Literal, ElementContent, RootContent, Properties } from 'hast'; import { RehypeAttrsOptions } from './'; export const getURLParameters = (url: string): Record => (url.match(/([^?=&]+)(=([^&]*))/g) || []).reduce( (a: Record, v: string) => ( - // eslint-disable-next-line no-sequences (a[v.slice(0, v.indexOf('='))] = v.slice(v.indexOf('=') + 1)), a ), {}, ); -type CommentData = { - type?: 'comment', - value?: string, -} - -export const prevChild = (data: NodeData[] = [], index: number): CommentData | undefined => { +export const prevChild = (data: Literal[] = [], index: number): Comment | undefined => { let i = index; while (i > -1) { i--; if (!data[i]) return if ((data[i] && data[i].value && (data[i].value as string).replace(/(\n|\s)/g, '') !== '') || data[i].type !== 'text') { if (!/^rehype:/.test(data[i].value as string) || (data[i].type as string) !== 'comment') return; - return data[i] as unknown as CommentData; + return data[i] as unknown as Comment; } } return; } -export const nextChild = (data: NodeData[] = [], index: number, tagName?: string): CommentData | undefined => { +export const nextChild = (data: RootContent[] | ElementContent[] = [], index: number, tagName?: string): ElementContent | undefined => { let i = index; while (i < data.length) { i++; if (tagName) { - if (data[i] && data[i].value && (data[i].value as string).replace(/(\n|\s)/g, '') !== '' || data[i] && (data[i].type as string) === 'element') { - return data[i].tagName === tagName ? data[i] as unknown as CommentData : undefined + const element = data[i] as Literal & Element; + if (element && element.value && (element.value as string).replace(/(\n|\s)/g, '') !== '' || data[i] && (data[i].type as string) === 'element') { + return element.tagName === tagName ? element : undefined } } else { - if (!data[i] || (data[i].type !== 'text' && (data[i].type as string) !== 'comment') || (data[i].type == 'text' && (data[i].value as string).replace(/(\n|\s)/g, '') !== '')) return - if ((data[i].type as string) === 'comment') { - if (!/^rehype:/.test(data[i].value as string)) return; + const element = data[i] as ElementContent & Literal; + if (!element || (element.type !== 'text' && (element.type as string) !== 'comment') || (element.type === 'text' && (element.value as string).replace(/(\n|\s)/g, '') !== '')) return; + if ((element.type as string) === 'comment') { + if (!/^rehype:/.test(element.value as string)) return; const nextNode = nextChild(data, i, 'pre') if (nextNode) return; - return data[i] as unknown as CommentData; + return element; } } } @@ -55,7 +51,7 @@ export const nextChild = (data: NodeData[] = [], index: number, tagName? * @param index 当前数据所在的位置 * @returns 返回 当前参数数据 Object,`{}` */ -export const getCommentObject = ({ value = '' }: CommentData): Record => { +export const getCommentObject = ({ value = '' }: Comment): Properties => { const param = getURLParameters(value.replace(/^rehype:/, '')); Object.keys(param).forEach((keyName: string) => { if (param[keyName] === 'true') { @@ -71,7 +67,11 @@ export const getCommentObject = ({ value = '' }: CommentData): Record | null, attrs?: Record | null, type?: RehypeAttrsOptions['properties']) => { +export type DataConfig = { + 'data-config': Properties +} + +export const propertiesHandle = (defaultAttrs?: Properties | null, attrs?: Properties, type?: RehypeAttrsOptions['properties']): Properties | DataConfig => { if (type === 'string') { return { ...defaultAttrs, 'data-config': JSON.stringify({ ...attrs, rehyp: true })} } else if (type === 'attr') { diff --git a/src/visit.ts b/src/visit.ts deleted file mode 100644 index 5804d7f..0000000 --- a/src/visit.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Parent, NodeData } from 'unist'; - -export type VisitCallback = (node: NodeData, index: number, parent: NodeData) => void; -export default function visit(tree?: NodeData, element?: string, callback?: VisitCallback) { - if (!element || !tree || !callback || typeof callback !== 'function') { - return - } - if (tree.children && Array.isArray(tree.children)) { - handle(tree.children, element, tree, callback) - } -} - -function handle(tree: NodeData[], element: string, parent: NodeData, callback: VisitCallback) { - tree.forEach((item, index) => { - if (item.type === element) { - callback(item, index, parent) - if (Array.isArray(item.children)) { - handle(item.children, element, item, callback) - } - } - }) -} \ No newline at end of file diff --git a/test/index.test.ts b/test/index.test.ts index a8b26b6..577bb5b 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,4 +1,5 @@ -import { unified, Plugin } from 'unified' +import { unified, Plugin } from 'unified'; +import { Comment, Literal, ElementContent } from 'hast'; import { Parent, NodeData } from 'unist'; import { rehype } from 'rehype'; import gfm from 'remark-gfm'; @@ -8,7 +9,6 @@ import remarkParse from 'remark-parse'; import stringify from 'rehype-stringify'; import rehypeAttrs from '../src'; import * as utils from '../src/utils'; -import visit from '../src/visit'; const mrkStr = "\n```js\nconsole.log('')\n```" @@ -30,40 +30,25 @@ describe('rehype-attr function test case', () => { ], "data": { "quirksMode": false } } - visit(node, 'element', (childNode, index, parent) => { - expect(/(del|p)/.test((childNode as any).tagName)).toBeTruthy() - expect(typeof childNode).toEqual('object') - expect(typeof index).toEqual('number') - expect(typeof parent).toEqual('object') - }) - expect(visit(node)).toBeUndefined() - expect(visit(node, 'element')).toBeUndefined() - expect(visit(node, 'element', () => {})).toBeUndefined() - expect(visit({ type: 'root' }, 'element', () => {})).toBeUndefined() - expect(visit({ type: 'root', children: [ { type: 'element' }] }, 'element', () => {})).toBeUndefined() - expect(visit()).toBeUndefined() - expect(visit(undefined)).toBeUndefined() - expect(visit(undefined, undefined)).toBeUndefined() - expect(visit(undefined, undefined, undefined)).toBeUndefined() }); it('getCommentObject', async () => { - expect(utils.getCommentObject({})).toEqual({ }); - expect(utils.getCommentObject({ value: 'rehype:title=Rehype Attrs' })).toEqual({ title: 'Rehype Attrs' }); + expect(utils.getCommentObject({} as Comment)).toEqual({ }); + expect(utils.getCommentObject({ value: 'rehype:title=Rehype Attrs' } as Comment)).toEqual({ title: 'Rehype Attrs' }); }); it('prevChild', async () => { expect(utils.prevChild(undefined, 0)).toBeUndefined() expect(utils.prevChild(undefined, -1)).toBeUndefined() expect(utils.prevChild([ { type: 'comment', value: 'rehype:title=Rehype Attrs' }, { type: 'text', value: '\n' } ], 1)).toEqual({ type: "comment", value: "rehype:title=Rehype Attrs" }) - expect(utils.prevChild([ { type: 'comment', value: 'rehype:title=Rehype Attrs' }, { type: 'text' } ], 1)).toEqual({ type: "comment", value: "rehype:title=Rehype Attrs" }) + expect(utils.prevChild([ { type: 'comment', value: 'rehype:title=Rehype Attrs' }, { type: 'text' } ] as Literal[], 1)).toEqual({ type: "comment", value: "rehype:title=Rehype Attrs" }) expect(utils.prevChild([ { type: 'text', value: '\n' }, { type: 'comment', value: 'rehype:title=Rehype Attrs' } ], 2)).toEqual({ type: "comment", value: "rehype:title=Rehype Attrs" }) }); it('nextChild', async () => { expect(utils.nextChild(undefined, 0)).toBeUndefined() expect(utils.nextChild(undefined, -1)).toBeUndefined() - expect(utils.nextChild([ { type: 'elment', value: 'rehype:title=Rehype Attrs' } ], 0)).toBeUndefined() - expect(utils.nextChild([ { type: 'text' }, { type: 'comment', value: 'rehype:title=Rehype Attrs' } ], 0)).toEqual({ type: "comment", value: "rehype:title=Rehype Attrs" }) + expect(utils.nextChild([ { type: 'elment', value: 'rehype:title=Rehype Attrs' } ] as unknown as ElementContent[], 0)).toBeUndefined() + expect(utils.nextChild([ { type: 'text' }, { type: 'comment', value: 'rehype:title=Rehype Attrs' } ] as ElementContent[], 0)).toEqual({ type: "comment", value: "rehype:title=Rehype Attrs" }) expect(utils.nextChild([ { type: 'text', value: '\n' }, { type: 'comment', value: 'rehype:title=Rehype Attrs' } ], 0)).toEqual({ type: "comment", value: "rehype:title=Rehype Attrs" }) - expect(utils.nextChild([ { type: 'text', value: '\n' }, { type: 'text', value: '' }, { type: 'element', tagName: 'pre' } ], 0, 'pre')).toEqual({ type: 'element', tagName: 'pre' }) + expect(utils.nextChild([ { type: 'text', value: '\n' }, { type: 'text', value: '' }, { type: 'element', tagName: 'pre' } ] as ElementContent[], 0, 'pre')).toEqual({ type: 'element', tagName: 'pre' }) }); it('propertiesHandle', async () => { expect(utils.propertiesHandle({}, {})).toEqual({ @@ -394,10 +379,10 @@ describe('rehype-attr test case', () => { const pluginWithoutOptions: Plugin = (options) => { // expectType(options) } - + const htmlStr = rehype() .data('settings', { fragment: true }) - .use(rehypeAttrs as any, { properties: 'attr' }) + .use(rehypeAttrs, { properties: 'attr' }) .processSync(data.markdown) .toString() expect(htmlStr).toEqual(data.expected);