Files
test-repo/docs/.vitepress/theme/components/appendix/terminal-intro/EscapeParserDemo.vue
T
sanbuphy 73f4788d7e feat: comprehensive documentation and demo updates
- Update READMEs and docs across multiple languages
- Enhance interactive demos for Agent, LLM, VLM, Audio, Image Gen, Terminal, and Web Basics
- Add new appendix sections for Database and IDE intros
- Update VitePress config, theme, and utility scripts
- Clean up unused assets and components
2026-01-16 19:10:51 +08:00

509 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="parser-demo">
<div class="demo-header">
<div class="title">转义序列解析原理 (Parser Mechanism)</div>
<div class="controls">
<button @click="reset" :disabled="isPlaying">Reset</button>
<button @click="togglePlay" class="play-btn">
{{
isPlaying ? '⏸ Pause' : isFinished ? '↺ Replay' : '▶ Play Animation'
}}
</button>
</div>
</div>
<!-- 1. 字节流传送带 -->
<div class="stream-container">
<div class="label">Input Byte Stream / 输入字节流</div>
<div class="stream-track">
<div class="stream-window-mask">
<div
class="stream-content"
:style="{ transform: `translateX(-${currentIndex * 40}px)` }"
>
<div
v-for="(char, index) in charStream"
:key="index"
class="char-box"
:class="{
active: index === currentIndex,
processed: index < currentIndex,
special: char.isSpecial,
arg: char.isArg
}"
>
<span class="char-val">{{ char.display }}</span>
<span class="char-code">{{ char.hex }}</span>
</div>
</div>
</div>
<!-- 指针 -->
<div class="pointer">
<div class="arrow"></div>
<div class="pointer-label">Current Byte</div>
</div>
</div>
</div>
<!-- 2. 解析器状态机 -->
<div class="parser-state-machine">
<div class="state-box" :class="{ active: parserState === 'NORMAL' }">
<div class="state-name">NORMAL</div>
<div class="state-desc">Print Characters</div>
</div>
<div class="arrow-right"></div>
<div
class="state-box warning"
:class="{ active: parserState === 'ESCAPE' }"
>
<div class="state-name">ESCAPE MODE</div>
<div class="state-desc">Buffer Command...</div>
</div>
<!-- 指令说明框 -->
<div class="action-log" v-if="lastAction">
<span class="action-icon"></span>
{{ lastAction }}
</div>
</div>
<!-- 3. 终端屏幕 -->
<div class="terminal-screen">
<div class="label">Terminal Screen / 屏幕显示</div>
<div class="screen-content">
<span
v-for="(char, index) in outputBuffer"
:key="index"
:style="char.style"
>{{ char.val }}</span
><span class="cursor">_</span>
</div>
</div>
<div class="explanation">
<p>
<span class="badge normal">Normal</span> 模式下字符直接上屏
<span class="badge escape">Escape</span> 模式下遇到
<code>ESC</code>
终端<strong>停止输出</strong>开始收集字符作为指令直到指令结束
<code>m</code>并执行
</p>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
// 原始字符串: Hello [RED]World[RESET]!
// \x1B [ 3 1 m
const RAW_DATA = [
{ val: 'H', display: 'H', hex: '48' },
{ val: 'i', display: 'i', hex: '69' },
{ val: ' ', display: ' ', hex: '20' },
{ val: '\x1B', display: 'ESC', hex: '1B', isSpecial: true },
{ val: '[', display: '[', hex: '5B', isSpecial: true },
{ val: '3', display: '3', hex: '33', isArg: true },
{ val: '1', display: '1', hex: '31', isArg: true },
{ val: 'm', display: 'm', hex: '6D', isSpecial: true }, // End of seq
{ val: 'V', display: 'V', hex: '56' },
{ val: 'i', display: 'i', hex: '69' },
{ val: 'b', display: 'b', hex: '62' },
{ val: 'e', display: 'e', hex: '65' },
{ val: '\x1B', display: 'ESC', hex: '1B', isSpecial: true },
{ val: '[', display: '[', hex: '5B', isSpecial: true },
{ val: '0', display: '0', hex: '30', isArg: true },
{ val: 'm', display: 'm', hex: '6D', isSpecial: true },
{ val: '!', display: '!', hex: '21' }
]
const charStream = ref(RAW_DATA)
const currentIndex = ref(0)
const outputBuffer = ref([])
const parserState = ref('NORMAL') // NORMAL, ESCAPE
const currentStyle = ref({})
const isPlaying = ref(false)
const isFinished = ref(false)
const lastAction = ref('')
const reset = () => {
isPlaying.value = false // Stop first
currentIndex.value = 0
outputBuffer.value = []
parserState.value = 'NORMAL'
currentStyle.value = {}
isFinished.value = false
lastAction.value = ''
}
const togglePlay = () => {
if (isPlaying.value) {
isPlaying.value = false
} else {
play()
}
}
const play = async () => {
if (isPlaying.value) return
isPlaying.value = true
// If finished, reset first
if (isFinished.value) {
reset()
isPlaying.value = true
}
while (currentIndex.value < charStream.value.length) {
if (!isPlaying.value) break
const char = charStream.value[currentIndex.value]
// Processing Logic
if (parserState.value === 'NORMAL') {
if (char.val === '\x1B') {
parserState.value = 'ESCAPE'
lastAction.value = 'Start Sequence'
} else {
outputBuffer.value.push({
val: char.val,
style: { ...currentStyle.value }
})
lastAction.value = 'Print Char'
}
} else if (parserState.value === 'ESCAPE') {
// 简单模拟:遇到 'm' 结束
if (char.val === 'm') {
// 解析指令 (Hardcoded for demo)
const prevChar = charStream.value[currentIndex.value - 1]
if (prevChar.val === '1') {
currentStyle.value = { color: '#ff5f56', fontWeight: 'bold' }
lastAction.value = 'Execute: Set Color Red'
} else if (prevChar.val === '0') {
currentStyle.value = {}
lastAction.value = 'Execute: Reset Style'
}
// Small delay to show "Executing" state
await new Promise((r) => setTimeout(r, 200))
parserState.value = 'NORMAL'
} else {
lastAction.value = 'Buffering...'
}
}
await new Promise((r) => setTimeout(r, 600)) // Animation speed
// Check playing again after wait
if (!isPlaying.value) break
currentIndex.value++
}
if (currentIndex.value >= charStream.value.length) {
isPlaying.value = false
isFinished.value = true
lastAction.value = 'Done'
}
}
</script>
<style scoped>
.parser-demo {
background: #1e1e1e;
border-radius: 8px;
padding: 20px;
color: #fff;
font-family: 'Menlo', monospace;
margin: 20px 0;
border: 1px solid #333;
}
.demo-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.title {
font-weight: bold;
color: #ccc;
}
.controls button {
background: #333;
border: 1px solid #555;
color: white;
padding: 5px 12px;
border-radius: 4px;
cursor: pointer;
margin-left: 10px;
font-size: 12px;
}
.controls button.play-btn {
background: #0dbc79;
border-color: #0dbc79;
color: #000;
font-weight: bold;
}
.controls button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Stream Track */
.stream-container {
background: #111;
padding: 15px;
border-radius: 6px;
margin-bottom: 20px;
position: relative;
overflow: hidden;
}
.label {
font-size: 10px;
color: #666;
text-transform: uppercase;
margin-bottom: 8px;
display: block;
}
.stream-track {
position: relative;
height: 60px;
/* Use a fixed height to contain the items */
}
.stream-window-mask {
width: 100%;
overflow: hidden;
position: relative;
height: 100%;
/* Mask gradient to fade edges */
mask-image: linear-gradient(
to right,
transparent,
black 40%,
black 60%,
transparent
);
-webkit-mask-image: linear-gradient(
to right,
transparent,
black 40%,
black 60%,
transparent
);
}
.stream-content {
display: flex;
gap: 4px;
position: absolute;
left: 50%; /* Center the container start */
/*
Correct centering logic:
- Item width: 36px
- Gap: 4px
- Total unit: 40px
- We want Item[0] center to be at left:0 (relative to left:50%)
- Item[0] center is at: 18px (half width)
- So we need to shift left by 18px initially.
*/
margin-left: -18px;
transition: transform 0.5s cubic-bezier(0.25, 1, 0.5, 1);
}
.char-box {
width: 36px;
height: 48px;
background: #2d2d2d;
border: 1px solid #444;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 4px;
flex-shrink: 0;
transition: all 0.3s;
}
.char-box.active {
background: #fff;
color: #000;
transform: scale(1.1);
box-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
z-index: 10;
border-color: #fff;
}
.char-box.processed {
opacity: 0.3;
}
.char-box.special {
border-color: #e5e510;
color: #e5e510;
}
.char-box.active.special {
background: #e5e510;
color: #000;
}
.char-box.arg {
border-color: #11a8cd;
color: #11a8cd;
}
.char-box.active.arg {
background: #11a8cd;
color: #fff;
}
.char-val {
font-size: 14px;
font-weight: bold;
}
.char-code {
font-size: 9px;
opacity: 0.7;
margin-top: 2px;
}
.pointer {
position: absolute;
bottom: -5px;
left: 50%;
transform: translateX(-50%);
text-align: center;
color: #0dbc79;
}
.arrow {
font-size: 20px;
line-height: 1;
}
.pointer-label {
font-size: 10px;
white-space: nowrap;
}
/* State Machine */
.parser-state-machine {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
margin-bottom: 20px;
background: #252526;
padding: 10px;
border-radius: 6px;
height: 60px;
}
.state-box {
padding: 8px 16px;
border-radius: 4px;
background: #333;
opacity: 0.3;
text-align: center;
transition: all 0.3s;
min-width: 100px;
}
.state-box.active {
opacity: 1;
background: #0dbc79;
color: #000;
box-shadow: 0 0 15px rgba(13, 188, 121, 0.2);
}
.state-box.warning.active {
background: #e5e510;
color: #000;
}
.state-name {
font-weight: bold;
font-size: 12px;
}
.state-desc {
font-size: 10px;
opacity: 0.8;
}
.arrow-right {
color: #555;
font-size: 18px;
}
.action-log {
margin-left: 20px;
padding: 4px 12px;
background: #000;
border-radius: 4px;
border: 1px solid #444;
font-size: 12px;
color: #fff;
display: flex;
align-items: center;
gap: 6px;
animation: flash 0.5s;
}
/* Screen */
.terminal-screen {
background: #000;
border: 1px solid #333;
border-radius: 6px;
padding: 15px;
min-height: 80px;
}
.screen-content {
font-size: 16px;
line-height: 1.5;
}
.cursor {
animation: blink 1s infinite;
color: #0dbc79;
}
.explanation {
margin-top: 15px;
font-size: 13px;
color: #999;
line-height: 1.5;
}
.badge {
padding: 2px 6px;
border-radius: 3px;
font-size: 11px;
color: #000;
}
.badge.normal {
background: #0dbc79;
}
.badge.escape {
background: #e5e510;
}
@keyframes blink {
50% {
opacity: 0;
}
}
@keyframes flash {
0% {
background: #333;
}
100% {
background: #000;
}
}
</style>