599 lines
14 KiB
Vue
599 lines
14 KiB
Vue
|
|
<template>
|
|||
|
|
<div class="compiler-demo">
|
|||
|
|
<div class="demo-header">
|
|||
|
|
<span class="icon">⚙️</span>
|
|||
|
|
<span class="title">编译器工作流程</span>
|
|||
|
|
<span class="subtitle">从源代码到机器码的旅程</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="demo-content">
|
|||
|
|
<div class="pipeline-visual">
|
|||
|
|
<div class="pipeline-title">编译流程</div>
|
|||
|
|
<div class="pipeline-stages">
|
|||
|
|
<div
|
|||
|
|
v-for="(stage, i) in stages"
|
|||
|
|
:key="i"
|
|||
|
|
:class="['stage', { active: activeStage === i }]"
|
|||
|
|
@click="activeStage = i"
|
|||
|
|
>
|
|||
|
|
<div class="stage-num">{{ i + 1 }}</div>
|
|||
|
|
<div class="stage-name">{{ stage.name }}</div>
|
|||
|
|
<div class="stage-output">{{ stage.output }}</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="stage-arrow" v-for="i in stages.length - 1" :key="'arrow-' + i">→</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="stage-detail" v-if="currentStage">
|
|||
|
|
<div class="detail-header">
|
|||
|
|
<span class="detail-name">{{ currentStage.name }}</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="detail-desc">{{ currentStage.desc }}</div>
|
|||
|
|
<div class="detail-tasks">
|
|||
|
|
<div class="task-title">主要任务</div>
|
|||
|
|
<ul>
|
|||
|
|
<li v-for="(task, j) in currentStage.tasks" :key="j">{{ task }}</li>
|
|||
|
|
</ul>
|
|||
|
|
</div>
|
|||
|
|
<div class="detail-example">
|
|||
|
|
<div class="example-title">示例</div>
|
|||
|
|
<pre><code>{{ currentStage.example }}</code></pre>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="interactive-demo">
|
|||
|
|
<div class="demo-title">词法分析演示</div>
|
|||
|
|
<div class="lexer-input">
|
|||
|
|
<label>输入代码:</label>
|
|||
|
|
<input v-model="sourceCode" type="text" placeholder="例如: int x = 10 + 5;" />
|
|||
|
|
</div>
|
|||
|
|
<div class="lexer-output">
|
|||
|
|
<div class="output-title">词法单元 (Tokens)</div>
|
|||
|
|
<div class="tokens">
|
|||
|
|
<div
|
|||
|
|
v-for="(token, i) in tokens"
|
|||
|
|
:key="i"
|
|||
|
|
:class="['token', token.type]"
|
|||
|
|
>
|
|||
|
|
<span class="token-value">{{ token.value }}</span>
|
|||
|
|
<span class="token-type">{{ token.type }}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="ast-demo">
|
|||
|
|
<div class="demo-title">语法树 (AST) 可视化</div>
|
|||
|
|
<div class="ast-input">
|
|||
|
|
<label>表达式:</label>
|
|||
|
|
<input v-model="expression" type="text" placeholder="例如: 1 + 2 * 3" />
|
|||
|
|
</div>
|
|||
|
|
<div class="ast-visual">
|
|||
|
|
<div class="ast-node root" v-if="ast">
|
|||
|
|
<div class="node-value">{{ ast.value }}</div>
|
|||
|
|
<div class="node-children" v-if="ast.left || ast.right">
|
|||
|
|
<div class="ast-node left" v-if="ast.left">
|
|||
|
|
<div class="node-value">{{ ast.left.value }}</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="ast-node right" v-if="ast.right">
|
|||
|
|
<div class="node-value">{{ ast.right.value }}</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="comparison-section">
|
|||
|
|
<div class="section-title">编译型 vs 解释型 vs JIT</div>
|
|||
|
|
<div class="comparison-grid">
|
|||
|
|
<div class="comparison-item" v-for="(item, i) in executionModels" :key="i">
|
|||
|
|
<div class="item-name">{{ item.name }}</div>
|
|||
|
|
<div class="item-flow">{{ item.flow }}</div>
|
|||
|
|
<div class="item-pros">{{ item.pros }}</div>
|
|||
|
|
<div class="item-cons">{{ item.cons }}</div>
|
|||
|
|
<div class="item-langs">{{ item.langs }}</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="info-box">
|
|||
|
|
<span class="icon">💡</span>
|
|||
|
|
<strong>核心思想:</strong>编译器将人类可读的源代码转换为机器可执行的指令。主要阶段包括词法分析、语法分析、语义分析、中间代码生成、优化和目标代码生成。
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
import { ref, computed, watch } from 'vue'
|
|||
|
|
|
|||
|
|
const activeStage = ref(0)
|
|||
|
|
|
|||
|
|
const stages = [
|
|||
|
|
{
|
|||
|
|
name: '词法分析',
|
|||
|
|
output: 'Token 流',
|
|||
|
|
desc: '将源代码分解为一个个词法单元(Token)',
|
|||
|
|
tasks: ['识别关键字、标识符、字面量、运算符', '过滤空白和注释', '记录位置信息'],
|
|||
|
|
example: `源代码: int x = 10;
|
|||
|
|
Tokens: [int][x][=][10][;]`
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: '语法分析',
|
|||
|
|
output: 'AST',
|
|||
|
|
desc: '根据语法规则,将 Token 流组织成语法树',
|
|||
|
|
tasks: ['构建抽象语法树 (AST)', '检查语法错误', '确定运算优先级'],
|
|||
|
|
example: `表达式: 1 + 2 * 3
|
|||
|
|
|
|||
|
|
AST:
|
|||
|
|
+
|
|||
|
|
/ \\
|
|||
|
|
1 *
|
|||
|
|
/ \\
|
|||
|
|
2 3`
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: '语义分析',
|
|||
|
|
output: '带类型的 AST',
|
|||
|
|
desc: '检查语义正确性,进行类型检查',
|
|||
|
|
tasks: ['类型检查', '作用域分析', '符号表构建', '类型推断'],
|
|||
|
|
example: `int x = "hello"; // 类型错误!
|
|||
|
|
int y = 10 + 5; // 正确`
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: '中间代码',
|
|||
|
|
output: 'IR',
|
|||
|
|
desc: '生成平台无关的中间表示',
|
|||
|
|
tasks: ['生成三地址码或 SSA', '便于优化', '支持多目标平台'],
|
|||
|
|
example: `x = 10 + 5
|
|||
|
|
// 三地址码:
|
|||
|
|
t1 = 10 + 5
|
|||
|
|
x = t1`
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: '优化',
|
|||
|
|
output: '优化后的 IR',
|
|||
|
|
desc: '对中间代码进行各种优化',
|
|||
|
|
tasks: ['常量折叠', '死代码消除', '循环优化', '内联展开'],
|
|||
|
|
example: `// 优化前
|
|||
|
|
x = 10 + 5
|
|||
|
|
y = x * 2
|
|||
|
|
|
|||
|
|
// 优化后(常量折叠)
|
|||
|
|
x = 15
|
|||
|
|
y = 30`
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: '代码生成',
|
|||
|
|
output: '机器码',
|
|||
|
|
desc: '生成目标机器的机器码',
|
|||
|
|
tasks: ['指令选择', '寄存器分配', '指令调度', '生成可执行文件'],
|
|||
|
|
example: `// x = 15 的汇编
|
|||
|
|
mov eax, 15
|
|||
|
|
mov [x], eax`
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
const currentStage = computed(() => stages[activeStage.value])
|
|||
|
|
|
|||
|
|
const sourceCode = ref('int x = 10 + 5;')
|
|||
|
|
|
|||
|
|
const keywords = ['int', 'float', 'if', 'else', 'while', 'for', 'return', 'void', 'class', 'public', 'private']
|
|||
|
|
const operators = ['+', '-', '*', '/', '=', '==', '!=', '<', '>', '<=', '>=']
|
|||
|
|
|
|||
|
|
const tokens = computed(() => {
|
|||
|
|
const code = sourceCode.value
|
|||
|
|
if (!code.trim()) return []
|
|||
|
|
|
|||
|
|
const result = []
|
|||
|
|
const words = code.split(/(\s+|[;,\(\)\{\}\[\]])/)
|
|||
|
|
|
|||
|
|
for (const word of words) {
|
|||
|
|
if (!word.trim()) continue
|
|||
|
|
|
|||
|
|
if (keywords.includes(word)) {
|
|||
|
|
result.push({ value: word, type: 'keyword' })
|
|||
|
|
} else if (operators.includes(word)) {
|
|||
|
|
result.push({ value: word, type: 'operator' })
|
|||
|
|
} else if (/^\d+$/.test(word)) {
|
|||
|
|
result.push({ value: word, type: 'number' })
|
|||
|
|
} else if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(word)) {
|
|||
|
|
result.push({ value: word, type: 'identifier' })
|
|||
|
|
} else if (word === ';') {
|
|||
|
|
result.push({ value: word, type: 'punctuation' })
|
|||
|
|
} else {
|
|||
|
|
result.push({ value: word, type: 'unknown' })
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return result
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const expression = ref('1 + 2 * 3')
|
|||
|
|
|
|||
|
|
const ast = computed(() => {
|
|||
|
|
const expr = expression.value.trim()
|
|||
|
|
if (!expr) return null
|
|||
|
|
|
|||
|
|
const parseExpression = (str) => {
|
|||
|
|
str = str.trim()
|
|||
|
|
|
|||
|
|
const addSubMatch = str.match(/^(.+?)\s*([+-])\s*(.+)$/)
|
|||
|
|
if (addSubMatch) {
|
|||
|
|
return {
|
|||
|
|
value: addSubMatch[2],
|
|||
|
|
left: parseExpression(addSubMatch[1]),
|
|||
|
|
right: parseExpression(addSubMatch[3])
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const mulDivMatch = str.match(/^(.+?)\s*([*/])\s*(.+)$/)
|
|||
|
|
if (mulDivMatch) {
|
|||
|
|
return {
|
|||
|
|
value: mulDivMatch[2],
|
|||
|
|
left: parseExpression(mulDivMatch[1]),
|
|||
|
|
right: parseExpression(mulDivMatch[3])
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (/^\d+$/.test(str)) {
|
|||
|
|
return { value: str }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return { value: str }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return parseExpression(expr)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const executionModels = [
|
|||
|
|
{
|
|||
|
|
name: '编译型',
|
|||
|
|
flow: '源码 → 编译 → 机器码 → 执行',
|
|||
|
|
pros: '执行快,编译期检查',
|
|||
|
|
cons: '编译慢,跨平台难',
|
|||
|
|
langs: 'C, C++, Rust, Go'
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: '解释型',
|
|||
|
|
flow: '源码 → 解释器 → 逐行执行',
|
|||
|
|
pros: '跨平台,开发快',
|
|||
|
|
cons: '执行慢,运行时检查',
|
|||
|
|
langs: 'Python, Ruby, PHP'
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: 'JIT',
|
|||
|
|
flow: '源码 → 字节码 → JIT编译 → 执行',
|
|||
|
|
pros: '兼顾性能和跨平台',
|
|||
|
|
cons: '启动慢,内存占用大',
|
|||
|
|
langs: 'Java, JavaScript(V8)'
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.compiler-demo {
|
|||
|
|
border: 1px solid var(--vp-c-divider);
|
|||
|
|
border-radius: 8px;
|
|||
|
|
background: var(--vp-c-bg-soft);
|
|||
|
|
padding: 1rem;
|
|||
|
|
margin: 1rem 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.demo-header {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 0.5rem;
|
|||
|
|
margin-bottom: 0.75rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.demo-header .icon { font-size: 1.25rem; }
|
|||
|
|
.demo-header .title { font-weight: bold; font-size: 1rem; }
|
|||
|
|
.demo-header .subtitle { color: var(--vp-c-text-2); font-size: 0.85rem; margin-left: 0.5rem; }
|
|||
|
|
|
|||
|
|
.pipeline-visual {
|
|||
|
|
background: var(--vp-c-bg);
|
|||
|
|
padding: 0.75rem;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
margin-bottom: 1rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.pipeline-title {
|
|||
|
|
font-weight: bold;
|
|||
|
|
font-size: 0.9rem;
|
|||
|
|
margin-bottom: 0.5rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.pipeline-stages {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 0.25rem;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
position: relative;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stage {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
align-items: center;
|
|||
|
|
padding: 0.4rem;
|
|||
|
|
background: var(--vp-c-bg-alt);
|
|||
|
|
border-radius: 6px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
min-width: 80px;
|
|||
|
|
border: 2px solid transparent;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stage:hover {
|
|||
|
|
background: var(--vp-c-bg-soft);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stage.active {
|
|||
|
|
border-color: var(--vp-c-brand);
|
|||
|
|
background: var(--vp-c-brand-soft);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stage-num {
|
|||
|
|
width: 20px;
|
|||
|
|
height: 20px;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
background: var(--vp-c-brand);
|
|||
|
|
color: white;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
font-size: 0.7rem;
|
|||
|
|
font-weight: bold;
|
|||
|
|
margin-bottom: 0.25rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stage-name {
|
|||
|
|
font-size: 0.75rem;
|
|||
|
|
font-weight: bold;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stage-output {
|
|||
|
|
font-size: 0.65rem;
|
|||
|
|
color: var(--vp-c-text-2);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stage-arrow {
|
|||
|
|
color: var(--vp-c-text-3);
|
|||
|
|
font-size: 0.8rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.stage-detail {
|
|||
|
|
background: var(--vp-c-bg);
|
|||
|
|
padding: 0.75rem;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
margin-bottom: 1rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.detail-header {
|
|||
|
|
margin-bottom: 0.5rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.detail-name {
|
|||
|
|
font-weight: bold;
|
|||
|
|
font-size: 1rem;
|
|||
|
|
color: var(--vp-c-brand);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.detail-desc {
|
|||
|
|
font-size: 0.85rem;
|
|||
|
|
color: var(--vp-c-text-2);
|
|||
|
|
margin-bottom: 0.5rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.detail-tasks {
|
|||
|
|
margin-bottom: 0.5rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.task-title, .example-title {
|
|||
|
|
font-size: 0.8rem;
|
|||
|
|
font-weight: bold;
|
|||
|
|
margin-bottom: 0.25rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.detail-tasks ul {
|
|||
|
|
margin: 0;
|
|||
|
|
padding-left: 1rem;
|
|||
|
|
font-size: 0.8rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.detail-example {
|
|||
|
|
background: var(--vp-c-bg-alt);
|
|||
|
|
padding: 0.5rem;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
pre {
|
|||
|
|
margin: 0;
|
|||
|
|
font-size: 0.75rem;
|
|||
|
|
white-space: pre-wrap;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
code {
|
|||
|
|
font-family: monospace;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.interactive-demo, .ast-demo {
|
|||
|
|
background: var(--vp-c-bg);
|
|||
|
|
padding: 0.75rem;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
margin-bottom: 1rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.demo-title {
|
|||
|
|
font-weight: bold;
|
|||
|
|
font-size: 0.9rem;
|
|||
|
|
margin-bottom: 0.5rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.lexer-input, .ast-input {
|
|||
|
|
margin-bottom: 0.5rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.lexer-input label, .ast-input label {
|
|||
|
|
display: block;
|
|||
|
|
font-size: 0.8rem;
|
|||
|
|
color: var(--vp-c-text-2);
|
|||
|
|
margin-bottom: 0.25rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.lexer-input input, .ast-input input {
|
|||
|
|
width: 100%;
|
|||
|
|
padding: 0.4rem;
|
|||
|
|
border: 1px solid var(--vp-c-divider);
|
|||
|
|
border-radius: 4px;
|
|||
|
|
background: var(--vp-c-bg-alt);
|
|||
|
|
font-family: monospace;
|
|||
|
|
font-size: 0.85rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.output-title {
|
|||
|
|
font-size: 0.8rem;
|
|||
|
|
color: var(--vp-c-text-2);
|
|||
|
|
margin-bottom: 0.25rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.tokens {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 0.25rem;
|
|||
|
|
flex-wrap: wrap;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.token {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
align-items: center;
|
|||
|
|
padding: 0.25rem 0.4rem;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
font-size: 0.75rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.token.keyword { background: #d4edda; }
|
|||
|
|
.token.operator { background: #fff3cd; }
|
|||
|
|
.token.number { background: #cce5ff; }
|
|||
|
|
.token.identifier { background: #e2e3e5; }
|
|||
|
|
.token.punctuation { background: #f8d7da; }
|
|||
|
|
|
|||
|
|
.token-value {
|
|||
|
|
font-family: monospace;
|
|||
|
|
font-weight: bold;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.token-type {
|
|||
|
|
font-size: 0.65rem;
|
|||
|
|
color: var(--vp-c-text-2);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.ast-visual {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: center;
|
|||
|
|
padding: 1rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.ast-node {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
align-items: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.node-value {
|
|||
|
|
padding: 0.4rem 0.6rem;
|
|||
|
|
background: var(--vp-c-brand);
|
|||
|
|
color: white;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
font-weight: bold;
|
|||
|
|
font-family: monospace;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.node-children {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 1rem;
|
|||
|
|
margin-top: 0.5rem;
|
|||
|
|
position: relative;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.node-children::before {
|
|||
|
|
content: '';
|
|||
|
|
position: absolute;
|
|||
|
|
top: -0.5rem;
|
|||
|
|
left: 50%;
|
|||
|
|
width: 1px;
|
|||
|
|
height: 0.5rem;
|
|||
|
|
background: var(--vp-c-divider);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.ast-node.left .node-value,
|
|||
|
|
.ast-node.right .node-value {
|
|||
|
|
background: var(--vp-c-brand-soft);
|
|||
|
|
color: var(--vp-c-brand);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.comparison-section {
|
|||
|
|
background: var(--vp-c-bg);
|
|||
|
|
padding: 0.75rem;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.section-title {
|
|||
|
|
font-weight: bold;
|
|||
|
|
font-size: 0.9rem;
|
|||
|
|
margin-bottom: 0.5rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.comparison-grid {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|||
|
|
gap: 0.5rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.comparison-item {
|
|||
|
|
padding: 0.5rem;
|
|||
|
|
background: var(--vp-c-bg-alt);
|
|||
|
|
border-radius: 6px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-name {
|
|||
|
|
font-weight: bold;
|
|||
|
|
font-size: 0.85rem;
|
|||
|
|
margin-bottom: 0.25rem;
|
|||
|
|
color: var(--vp-c-brand);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-flow {
|
|||
|
|
font-size: 0.75rem;
|
|||
|
|
font-family: monospace;
|
|||
|
|
margin-bottom: 0.25rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-pros, .item-cons {
|
|||
|
|
font-size: 0.75rem;
|
|||
|
|
color: var(--vp-c-text-2);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-pros::before { content: '✅ '; }
|
|||
|
|
.item-cons::before { content: '❌ '; }
|
|||
|
|
|
|||
|
|
.item-langs {
|
|||
|
|
font-size: 0.7rem;
|
|||
|
|
color: var(--vp-c-text-3);
|
|||
|
|
margin-top: 0.25rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.info-box {
|
|||
|
|
background: var(--vp-c-bg-alt);
|
|||
|
|
padding: 0.75rem;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
font-size: 0.85rem;
|
|||
|
|
color: var(--vp-c-text-2);
|
|||
|
|
margin-top: 0.75rem;
|
|||
|
|
display: flex;
|
|||
|
|
gap: 0.25rem;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.info-box .icon { flex-shrink: 0; }
|
|||
|
|
</style>
|