feat: prevent `$` symbol selection in shell code (#5025)

pull/5026/head
Bjorn Lu 10 months ago committed by GitHub
parent 8da5e74948
commit bf2715ed67
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,6 +1,6 @@
import { inBrowser } from 'vitepress' import { inBrowser } from 'vitepress'
import { isShell } from '../../shared'
const shellRE = /language-(shellscript|shell|bash|sh|zsh)/
const ignoredNodes = ['.vp-copy-ignore', '.diff.remove'].join(', ') const ignoredNodes = ['.vp-copy-ignore', '.diff.remove'].join(', ')
export function useCopyCode() { export function useCopyCode() {
@ -15,8 +15,6 @@ export function useCopyCode() {
return return
} }
const isShell = shellRE.test(parent.className)
// Clone the node and remove the ignored nodes // Clone the node and remove the ignored nodes
const clone = sibling.cloneNode(true) as HTMLElement const clone = sibling.cloneNode(true) as HTMLElement
clone.querySelectorAll(ignoredNodes).forEach((node) => node.remove()) clone.querySelectorAll(ignoredNodes).forEach((node) => node.remove())
@ -26,7 +24,10 @@ export function useCopyCode() {
let text = clone.textContent || '' let text = clone.textContent || ''
if (isShell) { // NOTE: Any changes to this the code here may also need to update
// `transformerDisableShellSymbolSelect` in `src/node/markdown/plugins/highlight.ts`
const lang = /language-(\w+)/.exec(parent.className)?.[1] || ''
if (isShell(lang)) {
text = text.replace(/^ *(\$|>) /gm, '').trim() text = text.replace(/^ *(\$|>) /gm, '').trim()
} }

@ -11,6 +11,7 @@ import c from 'picocolors'
import type { BundledLanguage, ShikiTransformer } from 'shiki' import type { BundledLanguage, ShikiTransformer } from 'shiki'
import { createHighlighter, guessEmbeddedLanguages, isSpecialLang } from 'shiki' import { createHighlighter, guessEmbeddedLanguages, isSpecialLang } from 'shiki'
import type { Logger } from 'vite' import type { Logger } from 'vite'
import { isShell } from '../../shared'
import type { MarkdownOptions, ThemeOptions } from '../markdown' import type { MarkdownOptions, ThemeOptions } from '../markdown'
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10) const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10)
@ -47,6 +48,40 @@ function attrsToLines(attrs: string): TransformerCompactLineOption[] {
})) }))
} }
/**
* Prevents the leading '$' symbol etc from being selectable/copyable. Also
* normalizes its syntax so there's no leading spaces, and only a single
* trailing space.
*
* NOTE: Any changes to this function may also need to update
* `src/client/app/composables/copyCode.ts`
*/
function transformerDisableShellSymbolSelect(): ShikiTransformer {
return {
name: 'vitepress:disable-shell-symbol-select',
tokens(tokensByLine) {
if (!isShell(this.options.lang)) return
for (const tokens of tokensByLine) {
if (tokens.length < 2) continue
// The first token should only be a symbol token
const firstTokenText = tokens[0].content.trim()
if (firstTokenText !== '$' && firstTokenText !== '>') continue
// The second token must have a leading space (separates the symbol)
if (tokens[1].content[0] !== ' ') continue
tokens[0].content = firstTokenText + ' '
tokens[0].htmlStyle ??= {}
tokens[0].htmlStyle['user-select'] = 'none'
tokens[0].htmlStyle['-webkit-user-select'] = 'none'
tokens[1].content = tokens[1].content.slice(1)
}
}
}
}
export async function highlight( export async function highlight(
theme: ThemeOptions, theme: ThemeOptions,
options: MarkdownOptions, options: MarkdownOptions,
@ -83,6 +118,7 @@ export async function highlight(
}), }),
transformerNotationHighlight(), transformerNotationHighlight(),
transformerNotationErrorLevel(), transformerNotationErrorLevel(),
transformerDisableShellSymbolSelect(),
{ {
name: 'vitepress:add-dir', name: 'vitepress:add-dir',
pre(node) { pre(node) {

@ -353,3 +353,8 @@ type ObjectType = Record<PropertyKey, any>
export function isObject(value: unknown): value is ObjectType { export function isObject(value: unknown): value is ObjectType {
return Object.prototype.toString.call(value) === '[object Object]' return Object.prototype.toString.call(value) === '[object Object]'
} }
const shellLangs = ['shellscript', 'shell', 'bash', 'sh', 'zsh']
export function isShell(lang: string): boolean {
return shellLangs.includes(lang)
}

Loading…
Cancel
Save