From 30356e70c68ea79ac3563abe489b6b5dd4c912fc Mon Sep 17 00:00:00 2001 From: "val.istar.guo" Date: Wed, 29 Mar 2023 16:46:23 +0800 Subject: [PATCH] fix: toolbar and copy-to-clipboard plugin not work fix #20 --- README.md | 15 ++- ...-vistor.ts => create-parse-code-vistor.ts} | 30 ++--- src/create-plugin-applier.ts | 31 +++++ ...ctor.ts => create-pre-element-selector.ts} | 2 +- src/index.ts | 8 +- src/interface/plugin-context.ts | 9 ++ src/interface/plugin-options.ts | 5 +- src/plugins/apply-plugin.ts | 10 -- src/plugins/copy-to-clipboard.ts | 108 ++++++++++++++++++ src/plugins/line-numbers.ts | 34 +++--- src/plugins/toolbar.ts | 37 ++++++ src/utils/select-code-element.ts | 9 ++ tests/index.ts | 26 ++++- tests/snapshots/index.ts.md | 11 +- tests/snapshots/index.ts.snap | Bin 281 -> 299 bytes 15 files changed, 285 insertions(+), 50 deletions(-) rename src/{parse-code-vistor.ts => create-parse-code-vistor.ts} (63%) create mode 100644 src/create-plugin-applier.ts rename src/{pre-element-selector.ts => create-pre-element-selector.ts} (76%) create mode 100644 src/interface/plugin-context.ts delete mode 100644 src/plugins/apply-plugin.ts create mode 100644 src/plugins/copy-to-clipboard.ts create mode 100644 src/plugins/toolbar.ts create mode 100644 src/utils/select-code-element.ts diff --git a/README.md b/README.md index 596649d..132dbf0 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,20 @@ rehype() The names to use can be found [here](https://github.com/PrismJS/prism/tree/master/plugins). -> line-number plugin is reimplemented by rehype-prism +### Plugins Reimplemented By rehype-prism + +The table list plugins that cannot running on the server side. +Therefor it has been re-implemented by rehype-prism. + +| Plugin Name | +|:------------------| +| line-numbers | +| toolbar | +| copy-to-clipboard | + +> I haven't tested all prism plugins. +> If there are another plugins not work, +> submit issue on github. ## Load More Languages diff --git a/src/parse-code-vistor.ts b/src/create-parse-code-vistor.ts similarity index 63% rename from src/parse-code-vistor.ts rename to src/create-parse-code-vistor.ts index e77260f..27ded82 100644 --- a/src/parse-code-vistor.ts +++ b/src/create-parse-code-vistor.ts @@ -10,19 +10,22 @@ import { RehypePrismOptions } from './interface/rehype-prism-options.js' import { isElementNode } from './utils/is-element-node.js' import { isTextNode } from './utils/is-text-node.js' -import { applyPlugin } from './plugins/apply-plugin.js' +import { createPluginApplier } from './create-plugin-applier.js' +import { selectCodeElement } from './utils/select-code-element.js' const parser = unified() .use(rehypeParse, { fragment: true }) -export function parseCodeVisitor(options?: RehypePrismOptions): Visitor { - return node => { - if (!isElementNode(node) || node.tagName !== 'pre') return +export function createParseCodeVisitor(options?: RehypePrismOptions): Visitor { + const applyPlugin = createPluginApplier(options?.plugins || []) + + return (node, index, parentNode) => { + if (!isElementNode(node) || node.tagName !== 'pre' || index === null || parentNode === null) return const preElement = node - const codeElement = select('[tagName=code]', preElement) - if (!isElementNode(codeElement)) return + const codeElement = selectCodeElement(preElement) + if (!codeElement) return const lang = getLang(codeElement) if (!lang || !Prism.languages[lang]) return @@ -38,13 +41,12 @@ export function parseCodeVisitor(options?: RehypePrismOptions): Visitor void + +export function createPluginApplier(plugins: RehypePrismPlugin[]): Applier { + const context: PluginContext = { + toolbar: { + buttons: [], + }, + } + + const appliers = plugins + // uniq + .filter((item, index, arr) => arr.indexOf(item, 0) === index) + .map(plugin => { + if (plugin === 'line-numbers') return createLineNumberPlugin() + if (plugin === 'toolbar') return createToolbarPlugin(context) + if (plugin === 'copy-to-clipboard') return createCopyToClipboardPlugin(context) + }) + + return options => { + for (const applier of appliers) { + applier && applier(options) + } + } +} diff --git a/src/pre-element-selector.ts b/src/create-pre-element-selector.ts similarity index 76% rename from src/pre-element-selector.ts rename to src/create-pre-element-selector.ts index 8a1a6c2..26c6921 100644 --- a/src/pre-element-selector.ts +++ b/src/create-pre-element-selector.ts @@ -2,6 +2,6 @@ import { Test } from 'unist-util-is' import { isElementNode } from './utils/is-element-node.js' -export function preElementSelector(): Test { +export function createPreElementSelector(): Test { return node => isElementNode(node) && node.tagName === 'pre' } diff --git a/src/index.ts b/src/index.ts index 3d196af..1803f88 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,8 +4,8 @@ import { Element } from 'hast' import { visit } from 'unist-util-visit' import { Test } from 'unist-util-is' import { RehypePrismOptions } from './interface/rehype-prism-options.js' -import { preElementSelector } from './pre-element-selector.js' -import { parseCodeVisitor } from './parse-code-vistor.js' +import { createPreElementSelector } from './create-pre-element-selector.js' +import { createParseCodeVisitor } from './create-parse-code-vistor.js' import { internalPlugins } from './constant.js' @@ -21,8 +21,8 @@ const rehypePrism: unifiedTypes.Plugin<[RehypePrismOptions?], Element> = (option return tree => visit( tree, - preElementSelector(), - parseCodeVisitor(options), + createPreElementSelector(), + createParseCodeVisitor(options), ) } diff --git a/src/interface/plugin-context.ts b/src/interface/plugin-context.ts new file mode 100644 index 0000000..d474456 --- /dev/null +++ b/src/interface/plugin-context.ts @@ -0,0 +1,9 @@ +import { Element } from 'hast' +import { PluginOptions } from './plugin-options' + + +export interface PluginContext { + toolbar: { + buttons: ((options: PluginOptions) => Element)[] + } +} diff --git a/src/interface/plugin-options.ts b/src/interface/plugin-options.ts index b359464..2d838c3 100644 --- a/src/interface/plugin-options.ts +++ b/src/interface/plugin-options.ts @@ -1,9 +1,10 @@ -import { Element } from 'hast' +import { Element, Parent } from 'hast' export interface PluginOptions { preElement: Element - codeElement: Element + index: number + parentNode: Parent raw: string lang: string diff --git a/src/plugins/apply-plugin.ts b/src/plugins/apply-plugin.ts deleted file mode 100644 index 1d93312..0000000 --- a/src/plugins/apply-plugin.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { PluginOptions } from '@/interface/plugin-options' -import { RehypePrismPlugin } from '@/interface/rehype-prism-plugin' -import { applyLineNumberPlugin } from './line-numbers.js' - - -export function applyPlugin(plugins: RehypePrismPlugin[], options: PluginOptions): void { - for (const plugin of plugins) { - if (plugin === 'line-numbers') applyLineNumberPlugin(options) - } -} diff --git a/src/plugins/copy-to-clipboard.ts b/src/plugins/copy-to-clipboard.ts new file mode 100644 index 0000000..4e78b54 --- /dev/null +++ b/src/plugins/copy-to-clipboard.ts @@ -0,0 +1,108 @@ +import { h } from 'hastscript' +import { PluginContext } from '@/interface/plugin-context' + +interface Setting { + 'copy': 'Copy' + 'copy-error': 'Press Ctrl+C to copy' + 'copy-success': 'Copied!' + 'copy-timeout': 5000 +} + +function createClickCallback(str: string, setting: Setting): string { + return `(function(button){ + const setting = ${JSON.stringify(setting)} + const span = button.querySelector('span') + + function setState(state) { + span.textContent = setting[state]; + button.setAttribute('data-copy-state', state); + } + + function resetButtonText() { + setTimeout(function () { + setState('copy'); + }, setting['copy-timeout']); + } + + function onSuccess() { + setState('copy-success'); + resetButtonText(); + } + + function onError(error) { + error && console.error(error) + setState('copy-error'); + resetButtonText(); + } + + function fallbackCopyTextToClipboard(str) { + var textArea = document.createElement('textarea'); + textArea.value = str + + // Avoid scrolling to bottom + textArea.style.top = '0'; + textArea.style.left = '0'; + textArea.style.position = 'fixed'; + + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + + try { + var successful = document.execCommand('copy'); + setTimeout(function () { + if (successful) onSuccess() + else onError() + + }, 1); + } catch (err) { + setTimeout(function () { + onError(err) + }, 1); + } + + document.body.removeChild(textArea); + } + + function copyTextToClipboard(str) { + if (navigator.clipboard) { + navigator.clipboard.writeText(str) + .then(onSuccess, function () { + fallbackCopyTextToClipboard(str); + }); + } else { + fallbackCopyTextToClipboard(str); + } + } + + copyTextToClipboard(${JSON.stringify(str)}) + })(this)` +} + +export function createCopyToClipboardPlugin(context: PluginContext): void { + context.toolbar.buttons.push(({ raw }) => { + const span = h( + 'span', + {}, + ['Copy'], + ) + + const copyBtn = h( + 'button', + { + type: 'button', + className: ['copy-to-clipboard-button'], + onClick: createClickCallback(raw, { + copy: 'Copy', + 'copy-error': 'Press Ctrl+C to copy', + 'copy-success': 'Copied!', + 'copy-timeout': 5000, + }), + }, + [span], + ) + + return copyBtn + }) +} + diff --git a/src/plugins/line-numbers.ts b/src/plugins/line-numbers.ts index 9ec2e7a..c900e3b 100644 --- a/src/plugins/line-numbers.ts +++ b/src/plugins/line-numbers.ts @@ -1,6 +1,7 @@ import { h } from 'hastscript' -import { appendClassName } from '@/utils/append-class-name.js' import { Plugin } from '@/interface/plugin.js' +import { appendClassName } from '@/utils/append-class-name.js' +import { selectCodeElement } from '@/utils/select-code-element.js' function getLineNumber(str: string): number { @@ -8,19 +9,24 @@ function getLineNumber(str: string): number { return match ? match.length + 1 : 1 } -export const applyLineNumberPlugin: Plugin = options => { - const { preElement, codeElement, raw } = options +export function createLineNumberPlugin(): Plugin { + return options => { + const { preElement, raw } = options + + const codeElement = selectCodeElement(preElement) + if (!codeElement) return - appendClassName(preElement, 'line-numbers') + appendClassName(preElement, 'line-numbers') - const lineNumber = getLineNumber(raw) - const lineNumberColumn = h( - 'span', - { - 'aria-hidden': 'true', - className: ['line-numbers-rows'], - }, - new Array(lineNumber).fill(h('span')), - ) - codeElement.children.push(lineNumberColumn) + const lineNumber = getLineNumber(raw) + const lineNumberColumn = h( + 'span', + { + 'aria-hidden': 'true', + className: ['line-numbers-rows'], + }, + new Array(lineNumber).fill(h('span')), + ) + codeElement.children.push(lineNumberColumn) + } } diff --git a/src/plugins/toolbar.ts b/src/plugins/toolbar.ts new file mode 100644 index 0000000..2b18e1a --- /dev/null +++ b/src/plugins/toolbar.ts @@ -0,0 +1,37 @@ +import { h } from 'hastscript' +import { Plugin } from '@/interface/plugin.js' +import { PluginContext } from '@/interface/plugin-context.js' + + +export function createToolbarPlugin(context: PluginContext): Plugin { + return options => { + const { parentNode, index, preElement } = options + + const toolbar = h( + 'div', + { + className: ['toolbar'], + }, + ) + + const container = h( + 'div', + { + className: ['code-toolbar'], + }, + [preElement, toolbar], + ) + + parentNode.children.splice(index, 1, container) + + for (const button of context.toolbar.buttons) { + toolbar.children.push(h( + 'div', + { + className: ['toolbar-item'], + }, + [button(options)], + )) + } + } +} diff --git a/src/utils/select-code-element.ts b/src/utils/select-code-element.ts new file mode 100644 index 0000000..3f797aa --- /dev/null +++ b/src/utils/select-code-element.ts @@ -0,0 +1,9 @@ +import { Element } from 'hast' +import { select } from 'unist-util-select' +import { isElementNode } from './is-element-node.js' + + +export function selectCodeElement(parent: Element): Element | null { + const codeElement = select('[tagName=code]', parent) + return isElementNode(codeElement) ? codeElement : null +} diff --git a/tests/index.ts b/tests/index.ts index 99f4484..1272161 100644 --- a/tests/index.ts +++ b/tests/index.ts @@ -10,7 +10,7 @@ test('rehype prism with language', t => { const processor = unified() .use(remarkParse) .use(remark2rehype) - .use(rehypePrism, { plugins: ['toolbar', 'copy-to-clipboard'] }) + .use(rehypePrism) .use(html) t.snapshot(processor.processSync('```javascript\nconst a = 1\n```\n').value) @@ -20,8 +20,30 @@ test('rehype prism without language', t => { const processor = unified() .use(remarkParse) .use(remark2rehype) - .use(rehypePrism, { plugins: ['toolbar', 'copy-to-clipboard'] }) + .use(rehypePrism) .use(html) t.snapshot(processor.processSync('```\nconst a = 1\n```\n').value) }) + + +test('rehype prism with line-numbers plugin', t => { + const processor = unified() + .use(remarkParse) + .use(remark2rehype) + .use(rehypePrism, { plugins: ['line-numbers'] }) + .use(html) + + t.snapshot(processor.processSync('```javascript\nconst a = 1\n```\n').value) +}) + + +test('rehype prism with toolbar plugin', t => { + const processor = unified() + .use(remarkParse) + .use(remark2rehype) + .use(rehypePrism, { plugins: ['toolbar', 'copy-to-clipboard'] }) + .use(html) + + t.snapshot(processor.processSync('```javascript\nconst a = 1\n```\n').value) +}) diff --git a/tests/snapshots/index.ts.md b/tests/snapshots/index.ts.md index f2ae95c..ea1985b 100644 --- a/tests/snapshots/index.ts.md +++ b/tests/snapshots/index.ts.md @@ -18,9 +18,16 @@ Generated by [AVA](https://avajs.dev). `
const a = 1␊
     
` -## rehype prism +## rehype prism with line-numbers plugin > Snapshot 1 - `
const a = 1␊
+    `
const a = 1␊
+    
` + +## rehype prism with toolbar plugin + +> Snapshot 1 + + `
const a = 1␊
     
` diff --git a/tests/snapshots/index.ts.snap b/tests/snapshots/index.ts.snap index bcabc26ea4a52ce2cb80b8512a93dee79f121998..e9017ad802fed397854bb66b8272ddea14cfe1e9 100644 GIT binary patch literal 299 zcmV+`0o49MRzVDb0*Ga)l|-J{@( ztncC*_&~PQg0Mv|oAVd`Z@$A*<;iyZ{(<7ePs9DCMS{xdkfp+X@F}^TS-}V)Qb)82rs#;`XQn_3}rIhu<+) xcVmm`-pP9qF4bmv>V6@*3%H&{Sw-JBE}V2y9Zw38uOOXr?HhPPH?Us<008f{j#&Tz literal 281 zcmV+!0p|WeRzV*An!++3)Ce+)T|j>&~(>sr6X; zCHgP^fgfao4G0bT0iE+3=FQu^FFo0YfA%O&Jbuj**4v0GCdX^FaZ)P}Vb#J49S4Y9 zlkoShszBiFc%MCH{Y)OR>D?jIF`=>^n9F?Fdi{V;V4GZ&+-OVH4T2+tfe>mis0Ozi zN>y$w1r}ZOgi)YIP>Ps9DCMRcxDhI}>m_!~{FiysMek*F(fOO9#PgpL)XJ3@i^~|3 fm%hdM-?MioT&nr#DSvtCpHw~p$j)s`c>(|c)*gjX