Skip to content

Commit 33ae1ed

Browse files
committed
feat (plugin): 增加代码高亮功能
1 parent 092d1b0 commit 33ae1ed

8 files changed

Lines changed: 506 additions & 14 deletions

File tree

src/App.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ const clearOutput = () => {
283283
showToast('输出已清空', 'info')
284284
}
285285
286-
window.addEventListener("contextmenu", (e) => e.preventDefault(), false);
286+
// window.addEventListener("contextmenu", (e) => e.preventDefault(), false);
287287
288288
onMounted(async () => {
289289
await getSupportedLanguages()

src/components/CodeEditor.vue

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,39 @@
11
<template>
2-
<div class="flex bg-white h-full">
2+
<div class="flex bg-white h-full relative">
33
<!-- 行号 -->
44
<div ref="lineNumbersRef"
5-
class="bg-gray-50 text-gray-400 text-sm font-mono py-4 px-3 select-none border-r border-gray-200 overflow-hidden">
5+
class="bg-gray-50 text-gray-400 text-sm font-mono py-4 px-3 select-none border-r border-gray-200 overflow-hidden flex-shrink-0 z-10">
66
<div v-for="(num, index) in lineNumbers" :key="index" class="h-6 leading-6 text-right">
77
{{ num }}
88
</div>
99
</div>
1010

11-
<!-- 代码输入框 -->
12-
<textarea ref="textareaRef"
13-
:value="modelValue"
14-
@input="handleInput"
15-
@keydown="handleKeyDown"
16-
@scroll="handleScroll"
17-
class="flex-1 p-4 font-mono text-sm leading-6 resize-none outline-none bg-white"
18-
placeholder="在此输入代码..."
19-
spellcheck="false">
20-
</textarea>
11+
<!-- 语法高亮容器 -->
12+
<div class="flex-1 relative">
13+
<!-- 高亮显示层 -->
14+
<pre ref="highlightRef"
15+
class="absolute inset-0 p-4 font-mono text-sm leading-6 bg-transparent pointer-events-none overflow-hidden whitespace-pre-wrap z-0"
16+
style="margin: 0; border: 0; word-break: break-word; white-space: pre-wrap;"
17+
v-html="highlightedCode"></pre>
18+
19+
<!-- 代码输入框 -->
20+
<textarea ref="textareaRef"
21+
:value="modelValue"
22+
@input="handleInput"
23+
@keydown="handleKeyDown"
24+
@scroll="handleScroll"
25+
class="absolute inset-0 p-4 font-mono text-sm leading-6 resize-none outline-none bg-transparent z-10"
26+
style="color: transparent; caret-color: #374151; margin: 0; border: 0; word-break: break-word; white-space: pre-wrap;"
27+
placeholder="在此输入代码..."
28+
spellcheck="false">
29+
</textarea>
30+
</div>
2131
</div>
2232
</template>
2333

2434
<script setup lang="ts">
2535
import { computed, nextTick, ref } from 'vue'
36+
import { highlightCode } from '../utils/highlighter'
2637
2738
const props = defineProps<{
2839
modelValue: string
@@ -35,12 +46,17 @@ const emit = defineEmits<{
3546
3647
const textareaRef = ref<HTMLTextAreaElement>()
3748
const lineNumbersRef = ref<HTMLElement>()
49+
const highlightRef = ref<HTMLPreElement>()
3850
3951
const lineNumbers = computed(() => {
4052
const lines = props.modelValue.split('\n')
4153
return lines.map((_, index) => String(index + 1))
4254
})
4355
56+
const highlightedCode = computed(() => {
57+
return highlightCode(props.modelValue, props.language || 'python3')
58+
})
59+
4460
const handleInput = (e: Event) => {
4561
const target = e.target as HTMLTextAreaElement
4662
emit('update:modelValue', target.value)
@@ -65,5 +81,9 @@ const handleScroll = (e: Event) => {
6581
if (lineNumbersRef.value) {
6682
lineNumbersRef.value.scrollTop = target.scrollTop
6783
}
84+
if (highlightRef.value) {
85+
highlightRef.value.scrollTop = target.scrollTop
86+
highlightRef.value.scrollLeft = target.scrollLeft
87+
}
6888
}
6989
</script>

src/components/OutputPanel.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
<pre :class="['whitespace-pre-wrap text-sm leading-relaxed', isSuccess ? 'text-green-300' : 'text-red-300']">{{ output }}</pre>
2222
</div>
2323

24-
<div v-else class="p-4 text-gray-500 flex flex-col items-center justify-center h-full space-y-2">
24+
<div v-else class="p-4 text-gray-500 flex flex-col items-center justify-center h-full space-y-2 select-none">
2525
<Terminal class="w-8 h-8"/>
2626
<p class="text-sm">没有输出</p>
2727
<p class="text-xs">可以尝试运行一些代码</p>

src/utils/highlighter/index.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
export * from './types'
2+
export * from './manager'
3+
export { Python2Highlighter } from './languages/python2'
4+
export { Python3Highlighter } from './languages/python3'
5+
6+
// 导入并重新导出管理器实例
7+
import { HighlightManager } from './manager'
8+
9+
// 创建单例实例
10+
export const highlightManager = new HighlightManager()
11+
12+
/**
13+
* 高亮代码的便捷函数
14+
* @param code 源代码
15+
* @param language 语言名称
16+
* @returns 高亮后的 HTML
17+
*/
18+
export function highlightCode(code: string, language: string): string
19+
{
20+
return highlightManager.highlight(code, language)
21+
}
22+
23+
/**
24+
* 获取支持的语言列表
25+
*/
26+
export function getSupportedLanguages()
27+
{
28+
return highlightManager.getSupportedLanguages()
29+
}
30+
31+
/**
32+
* 注册自定义高亮器
33+
*/
34+
export function registerHighlighter(highlighter: import('./types').LanguageHighlighter)
35+
{
36+
highlightManager.register(highlighter)
37+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import type { HighlightRule, LanguageHighlighter } from '../types'
2+
3+
export class Python2Highlighter
4+
implements LanguageHighlighter
5+
{
6+
getLanguageName(): string
7+
{
8+
return 'python2'
9+
}
10+
11+
getDisplayName(): string
12+
{
13+
return 'Python 2'
14+
}
15+
16+
getOrder(): number
17+
{
18+
return 2
19+
}
20+
21+
getRules(): HighlightRule[]
22+
{
23+
return [
24+
// 1. 注释 (最高优先级)
25+
{
26+
pattern: /#.*$/gm,
27+
className: 'text-gray-500 italic',
28+
priority: 1
29+
},
30+
31+
// 2. 字符串 (高优先级)
32+
{
33+
pattern: /"""[\s\S]*?"""/g,
34+
className: 'text-green-600',
35+
priority: 2
36+
},
37+
{
38+
pattern: /'''[\s\S]*?'''/g,
39+
className: 'text-green-600',
40+
priority: 2
41+
},
42+
{
43+
pattern: /"(?:[^"\\]|\\.)*"/g,
44+
className: 'text-green-600',
45+
priority: 2
46+
},
47+
{
48+
pattern: /'(?:[^'\\]|\\.)*'/g,
49+
className: 'text-green-600',
50+
priority: 2
51+
},
52+
53+
// 3. Python 2 关键字
54+
{
55+
pattern: /\b(def|class|if|elif|else|for|while|try|except|finally|with|as|import|from|return|yield|break|continue|pass|lambda|and|or|not|in|is|True|False|None|print|exec|raw_input)\b/g,
56+
className: 'text-purple-600 font-semibold',
57+
priority: 3
58+
},
59+
60+
// 4. 数字
61+
{
62+
pattern: /\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b/g,
63+
className: 'text-blue-600',
64+
priority: 4
65+
},
66+
67+
// 5. 函数调用
68+
{
69+
pattern: /\b([a-zA-Z_]\w*)(?=\s*\()/g,
70+
className: 'text-orange-600',
71+
priority: 5
72+
},
73+
74+
// 6. 类名定义
75+
{
76+
pattern: /\bclass\s+([A-Z]\w*)/g,
77+
className: 'text-yellow-600 font-semibold',
78+
priority: 6
79+
}
80+
]
81+
}
82+
83+
preProcess(code: string): string
84+
{
85+
// Python 2 特定的预处理
86+
return code
87+
}
88+
89+
postProcess(highlighted: string): string
90+
{
91+
// Python 2 特定的后处理
92+
return highlighted
93+
}
94+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import type { HighlightRule, LanguageHighlighter } from '../types'
2+
3+
export class Python3Highlighter
4+
implements LanguageHighlighter
5+
{
6+
getLanguageName(): string
7+
{
8+
return 'python3'
9+
}
10+
11+
getDisplayName(): string
12+
{
13+
return 'Python 3'
14+
}
15+
16+
getOrder(): number
17+
{
18+
return 1
19+
}
20+
21+
getRules(): HighlightRule[]
22+
{
23+
return [
24+
// 1. 注释 (最高优先级)
25+
{
26+
pattern: /#.*$/gm,
27+
className: 'text-gray-500 italic',
28+
priority: 1
29+
},
30+
31+
// 2. 字符串 (高优先级)
32+
{
33+
pattern: /"""[\s\S]*?"""/g,
34+
className: 'text-green-600',
35+
priority: 2
36+
},
37+
{
38+
pattern: /'''[\s\S]*?'''/g,
39+
className: 'text-green-600',
40+
priority: 2
41+
},
42+
{
43+
pattern: /f"(?:[^"\\]|\\.)*"/g,
44+
className: 'text-emerald-600',
45+
priority: 2
46+
}, // f-strings
47+
{
48+
pattern: /f'(?:[^'\\]|\\.)*'/g,
49+
className: 'text-emerald-600',
50+
priority: 2
51+
}, // f-strings
52+
{
53+
pattern: /"(?:[^"\\]|\\.)*"/g,
54+
className: 'text-green-600',
55+
priority: 2
56+
},
57+
{
58+
pattern: /'(?:[^'\\]|\\.)*'/g,
59+
className: 'text-green-600',
60+
priority: 2
61+
},
62+
63+
// 3. Python 3 关键字
64+
{
65+
pattern: /\b(def|class|if|elif|else|for|while|try|except|finally|with|as|import|from|return|yield|break|continue|pass|lambda|and|or|not|in|is|True|False|None|async|await|nonlocal)\b/g,
66+
className: 'text-purple-600 font-semibold',
67+
priority: 3
68+
},
69+
70+
// 4. 数字
71+
{
72+
pattern: /\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b/g,
73+
className: 'text-blue-600',
74+
priority: 4
75+
},
76+
77+
// 5. 函数调用
78+
{
79+
pattern: /\b([a-zA-Z_]\w*)(?=\s*\()/g,
80+
className: 'text-orange-600',
81+
priority: 5
82+
},
83+
84+
// 6. 装饰器
85+
{
86+
pattern: /@[a-zA-Z_]\w*/g,
87+
className: 'text-yellow-600',
88+
priority: 6
89+
},
90+
91+
// 7. 类名定义
92+
{
93+
pattern: /\bclass\s+([A-Z]\w*)/g,
94+
className: 'text-yellow-600 font-semibold',
95+
priority: 7
96+
}
97+
]
98+
}
99+
100+
preProcess(code: string): string
101+
{
102+
// Python 3 特定的预处理
103+
return code
104+
}
105+
106+
postProcess(highlighted: string): string
107+
{
108+
// Python 3 特定的后处理
109+
return highlighted
110+
}
111+
}

0 commit comments

Comments
 (0)