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

pull/5026/head
Bjorn Lu 3 weeks 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 { isShell } from '../../shared'
const shellRE = /language-(shellscript|shell|bash|sh|zsh)/
const ignoredNodes = ['.vp-copy-ignore', '.diff.remove'].join(', ')
export function useCopyCode() {
@ -15,8 +15,6 @@ export function useCopyCode() {
return
}
const isShell = shellRE.test(parent.className)
// Clone the node and remove the ignored nodes
const clone = sibling.cloneNode(true) as HTMLElement
clone.querySelectorAll(ignoredNodes).forEach((node) => node.remove())
@ -26,7 +24,10 @@ export function useCopyCode() {
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()
}

@ -11,6 +11,7 @@ import c from 'picocolors'
import type { BundledLanguage, ShikiTransformer } from 'shiki'
import { createHighlighter, guessEmbeddedLanguages, isSpecialLang } from 'shiki'
import type { Logger } from 'vite'
import { isShell } from '../../shared'
import type { MarkdownOptions, ThemeOptions } from '../markdown'
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(
theme: ThemeOptions,
options: MarkdownOptions,
@ -83,6 +118,7 @@ export async function highlight(
}),
transformerNotationHighlight(),
transformerNotationErrorLevel(),
transformerDisableShellSymbolSelect(),
{
name: 'vitepress:add-dir',
pre(node) {

@ -353,3 +353,8 @@ type ObjectType = Record<PropertyKey, any>
export function isObject(value: unknown): value is ObjectType {
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