Files
test-repo/docs/.vitepress/theme/components/appendix/terminal-intro/EscapeParserDemo.vue
T

450 lines
11 KiB
Vue
Raw Normal View History

<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>