Skip to content

Commit

Permalink
fix: toolbar and copy-to-clipboard plugin not work
Browse files Browse the repository at this point in the history
fix #20
  • Loading branch information
Val-istar-Guo committed Mar 29, 2023
1 parent e56b1f2 commit 30356e7
Show file tree
Hide file tree
Showing 15 changed files with 285 additions and 50 deletions.
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
30 changes: 16 additions & 14 deletions src/parse-code-vistor.ts → src/create-parse-code-vistor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ElementContent, Parent> {
return node => {
if (!isElementNode(node) || node.tagName !== 'pre') return
export function createParseCodeVisitor(options?: RehypePrismOptions): Visitor<ElementContent, Parent> {
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
Expand All @@ -38,13 +41,12 @@ export function parseCodeVisitor(options?: RehypePrismOptions): Visitor<ElementC

codeElement.children = [...tree.children]

if (options?.plugins) {
applyPlugin(options.plugins, {
preElement,
codeElement,
raw,
lang,
})
}
applyPlugin({
parentNode,
index,
preElement,
raw,
lang,
})
}
}
31 changes: 31 additions & 0 deletions src/create-plugin-applier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { PluginOptions } from '@/interface/plugin-options'
import { RehypePrismPlugin } from '@/interface/rehype-prism-plugin'
import { PluginContext } from './interface/plugin-context.js'
import { createCopyToClipboardPlugin } from './plugins/copy-to-clipboard.js'
import { createLineNumberPlugin } from './plugins/line-numbers.js'
import { createToolbarPlugin } from './plugins/toolbar.js'

type Applier = (options: PluginOptions) => 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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
8 changes: 4 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'


Expand All @@ -21,8 +21,8 @@ const rehypePrism: unifiedTypes.Plugin<[RehypePrismOptions?], Element> = (option

return tree => visit<Element, Test>(
tree,
preElementSelector(),
parseCodeVisitor(options),
createPreElementSelector(),
createParseCodeVisitor(options),
)
}

Expand Down
9 changes: 9 additions & 0 deletions src/interface/plugin-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Element } from 'hast'
import { PluginOptions } from './plugin-options'


export interface PluginContext {
toolbar: {
buttons: ((options: PluginOptions) => Element)[]
}
}
5 changes: 3 additions & 2 deletions src/interface/plugin-options.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
10 changes: 0 additions & 10 deletions src/plugins/apply-plugin.ts

This file was deleted.

108 changes: 108 additions & 0 deletions src/plugins/copy-to-clipboard.ts
Original file line number Diff line number Diff line change
@@ -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
})
}

34 changes: 20 additions & 14 deletions src/plugins/line-numbers.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
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 {
const match = str.match(/\n(?!$)/g)
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)
}
}
37 changes: 37 additions & 0 deletions src/plugins/toolbar.ts
Original file line number Diff line number Diff line change
@@ -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)],
))
}
}
}
9 changes: 9 additions & 0 deletions src/utils/select-code-element.ts
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 30356e7

Please sign in to comment.