Add output preview phase and callout types specifications

- Implemented Phase 4: Output & Preview for text formatter, including saving formatted content, generating statistics, and providing HTML preview.
- Created callout types documentation with detection patterns and conversion rules for BBCode and HTML.
- Added element mapping specifications detailing detection patterns and conversion matrices for various Markdown elements.
- Established format conversion rules for BBCode and Markdown, emphasizing pixel-based sizing and supported tags.
- Developed BBCode template with structured document and callout templates for consistent formatting.
This commit is contained in:
catlog22
2026-01-13 16:53:25 +08:00
parent 549e6e70e4
commit 94ae9e264c
9 changed files with 2076 additions and 0 deletions

View File

@@ -0,0 +1,111 @@
# Phase 1: Input Collection
收集用户输入的文本内容。
## Objective
- 获取用户输入内容(直接粘贴或文件路径)
- 生成输入配置文件
**注意**: 输出格式固定为 BBCode + Markdown 混合格式(论坛优化),无需选择。
## Input
- 来源: 用户交互
- 配置: 无前置依赖
## Execution Steps
### Step 1: 询问输入方式
```javascript
const inputMethod = await AskUserQuestion({
questions: [
{
question: "请选择输入方式",
header: "输入方式",
multiSelect: false,
options: [
{ label: "直接粘贴文本", description: "在对话中粘贴要格式化的内容" },
{ label: "指定文件路径", description: "读取指定文件的内容" }
]
}
]
});
```
### Step 2: 获取内容
```javascript
let content = '';
if (inputMethod["输入方式"] === "直接粘贴文本") {
// 提示用户粘贴内容
const textInput = await AskUserQuestion({
questions: [
{
question: "请粘贴要格式化的文本内容(粘贴后选择确认)",
header: "文本内容",
multiSelect: false,
options: [
{ label: "已粘贴完成", description: "确认已在上方粘贴内容" }
]
}
]
});
// 从用户消息中提取文本内容
content = extractUserText();
} else {
// 询问文件路径
const filePath = await AskUserQuestion({
questions: [
{
question: "请输入文件路径",
header: "文件路径",
multiSelect: false,
options: [
{ label: "已输入路径", description: "确认路径已在上方输入" }
]
}
]
});
content = Read(extractedFilePath);
}
```
### Step 3: 保存配置
```javascript
const config = {
input_method: inputMethod["输入方式"],
target_format: "BBCode+MD", // 固定格式
original_content: content,
timestamp: new Date().toISOString()
};
Write(`${workDir}/input-config.json`, JSON.stringify(config, null, 2));
```
## Output
- **File**: `input-config.json`
- **Format**: JSON
```json
{
"input_method": "直接粘贴文本",
"target_format": "BBCode+MD",
"original_content": "...",
"timestamp": "2026-01-13T..."
}
```
## Quality Checklist
- [ ] 成功获取用户输入内容
- [ ] 内容非空且有效
- [ ] 配置文件已保存
## Next Phase
→ [Phase 2: Content Analysis](02-content-analysis.md)

View File

@@ -0,0 +1,248 @@
# Phase 2: Content Analysis
分析输入内容的结构和语义元素。
## Objective
- 识别内容结构(标题、段落、列表等)
- 检测特殊元素(代码块、表格、链接等)
- 生成结构化分析结果
## Input
- 依赖: `input-config.json`
- 配置: `{workDir}/input-config.json`
## Execution Steps
### Step 1: 加载输入
```javascript
const config = JSON.parse(Read(`${workDir}/input-config.json`));
const content = config.original_content;
```
### Step 2: 结构分析
```javascript
function analyzeStructure(text) {
const analysis = {
elements: [],
stats: {
headings: 0,
paragraphs: 0,
lists: 0,
code_blocks: 0,
tables: 0,
links: 0,
images: 0,
quotes: 0,
callouts: 0
}
};
// Callout 检测正则 (Obsidian 风格)
const CALLOUT_PATTERN = /^>\s*\[!(\w+)\](?:\s+(.+))?$/;
const lines = text.split('\n');
let currentElement = null;
let inCodeBlock = false;
let inList = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// 检测代码块
if (line.match(/^```/)) {
inCodeBlock = !inCodeBlock;
if (inCodeBlock) {
analysis.elements.push({
type: 'code_block',
start: i,
language: line.replace(/^```/, '').trim()
});
analysis.stats.code_blocks++;
}
continue;
}
if (inCodeBlock) continue;
// 检测标题 (Markdown 或纯文本模式)
if (line.match(/^#{1,6}\s/)) {
const level = line.match(/^(#+)/)[1].length;
analysis.elements.push({
type: 'heading',
level: level,
content: line.replace(/^#+\s*/, ''),
line: i
});
analysis.stats.headings++;
continue;
}
// 检测列表
if (line.match(/^[\s]*[-*+]\s/) || line.match(/^[\s]*\d+\.\s/)) {
if (!inList) {
analysis.elements.push({
type: 'list',
start: i,
ordered: line.match(/^\d+\./) !== null
});
analysis.stats.lists++;
inList = true;
}
continue;
} else {
inList = false;
}
// 检测 Callout (Obsidian 风格) - 优先于普通引用
const calloutMatch = line.match(CALLOUT_PATTERN);
if (calloutMatch) {
const calloutType = calloutMatch[1].toLowerCase();
const calloutTitle = calloutMatch[2] || null;
// 收集 Callout 内容行
const calloutContent = [];
let j = i + 1;
while (j < lines.length && lines[j].startsWith('>')) {
calloutContent.push(lines[j].replace(/^>\s*/, ''));
j++;
}
analysis.elements.push({
type: 'callout',
calloutType: calloutType,
title: calloutTitle,
content: calloutContent.join('\n'),
start: i,
end: j - 1
});
analysis.stats.callouts++;
i = j - 1; // 跳过已处理的行
continue;
}
// 检测普通引用
if (line.match(/^>\s/)) {
analysis.elements.push({
type: 'quote',
content: line.replace(/^>\s*/, ''),
line: i
});
analysis.stats.quotes++;
continue;
}
// 检测表格
if (line.match(/^\|.*\|$/)) {
analysis.elements.push({
type: 'table_row',
line: i
});
if (!analysis.elements.find(e => e.type === 'table')) {
analysis.stats.tables++;
}
continue;
}
// 检测链接
const links = line.match(/\[([^\]]+)\]\(([^)]+)\)/g);
if (links) {
analysis.stats.links += links.length;
}
// 检测图片
const images = line.match(/!\[([^\]]*)\]\(([^)]+)\)/g);
if (images) {
analysis.stats.images += images.length;
}
// 普通段落
if (line.trim() && !line.match(/^[-=]{3,}$/)) {
analysis.elements.push({
type: 'paragraph',
line: i,
preview: line.substring(0, 50)
});
analysis.stats.paragraphs++;
}
}
return analysis;
}
const analysis = analyzeStructure(content);
```
### Step 3: 语义增强
```javascript
// 识别特殊语义
function enhanceSemantics(text, analysis) {
const enhanced = { ...analysis };
// 检测关键词强调
const boldPatterns = text.match(/\*\*[^*]+\*\*/g) || [];
const italicPatterns = text.match(/\*[^*]+\*/g) || [];
enhanced.semantics = {
emphasis: {
bold: boldPatterns.length,
italic: italicPatterns.length
},
estimated_reading_time: Math.ceil(text.split(/\s+/).length / 200) // 200 words/min
};
return enhanced;
}
const enhancedAnalysis = enhanceSemantics(content, analysis);
```
### Step 4: 保存分析结果
```javascript
Write(`${workDir}/analysis.json`, JSON.stringify(enhancedAnalysis, null, 2));
```
## Output
- **File**: `analysis.json`
- **Format**: JSON
```json
{
"elements": [
{ "type": "heading", "level": 1, "content": "Title", "line": 0 },
{ "type": "paragraph", "line": 2, "preview": "..." },
{ "type": "callout", "calloutType": "warning", "title": "注意事项", "content": "...", "start": 4, "end": 6 },
{ "type": "code_block", "start": 8, "language": "javascript" }
],
"stats": {
"headings": 3,
"paragraphs": 10,
"lists": 2,
"code_blocks": 1,
"tables": 0,
"links": 5,
"images": 0,
"quotes": 1,
"callouts": 2
},
"semantics": {
"emphasis": { "bold": 5, "italic": 3 },
"estimated_reading_time": 2
}
}
```
## Quality Checklist
- [ ] 所有结构元素已识别
- [ ] 统计信息准确
- [ ] 语义增强完成
- [ ] 分析文件已保存
## Next Phase
→ [Phase 3: Format Transform](03-format-transform.md)

View File

@@ -0,0 +1,237 @@
# Phase 3: Format Transform
将内容转换为 BBCode + Markdown 混合格式(论坛优化)。
## Objective
- 根据分析结果转换内容
- 应用像素级字号规则
- 处理 Callout/标注语法
- 生成论坛兼容的输出
## Input
- 依赖: `input-config.json`, `analysis.json`
- 规范: `specs/format-rules.md`, `specs/element-mapping.md`
## Format Specification
### Size Hierarchy (Pixels)
| Element | Size | Color | Usage |
|---------|------|-------|-------|
| **H1** | 150 | #2196F3 | 文档主标题 |
| **H2** | 120 | #2196F3 | 章节标题 |
| **H3** | 100 | #333 | 子标题 |
| **H4+** | (默认) | - | 仅加粗 |
| **Notes** | 80 | gray | 备注/元数据 |
### Unsupported Tags (禁止使用)
| Tag | Reason | Alternative |
|-----|--------|-------------|
| `[align]` | 不渲染 | 删除,使用默认左对齐 |
| `[hr]` | 显示为文本 | 使用 Markdown `---` |
| `[table]` | 支持有限 | 转为列表或代码块 |
| HTML tags | 不支持 | 仅使用 BBCode |
## Execution Steps
### Step 1: 加载配置和分析
```javascript
const config = JSON.parse(Read(`${workDir}/input-config.json`));
const analysis = JSON.parse(Read(`${workDir}/analysis.json`));
const content = config.original_content;
```
### Step 2: Callout 配置
```javascript
// Callout 类型映射(像素级字号)
const CALLOUT_CONFIG = {
// 信息类
note: { icon: '📝', color: '#2196F3', label: '注意' },
info: { icon: '', color: '#2196F3', label: '信息' },
abstract: { icon: '📄', color: '#2196F3', label: '摘要' },
summary: { icon: '📄', color: '#2196F3', label: '摘要' },
tldr: { icon: '📄', color: '#2196F3', label: '摘要' },
// 成功/提示类
tip: { icon: '💡', color: '#4CAF50', label: '提示' },
hint: { icon: '💡', color: '#4CAF50', label: '提示' },
success: { icon: '✅', color: '#4CAF50', label: '成功' },
check: { icon: '✅', color: '#4CAF50', label: '完成' },
done: { icon: '✅', color: '#4CAF50', label: '完成' },
// 警告类
warning: { icon: '⚠️', color: '#FF9800', label: '警告' },
caution: { icon: '⚠️', color: '#FF9800', label: '注意' },
attention: { icon: '⚠️', color: '#FF9800', label: '注意' },
question: { icon: '❓', color: '#FF9800', label: '问题' },
help: { icon: '❓', color: '#FF9800', label: '帮助' },
faq: { icon: '❓', color: '#FF9800', label: 'FAQ' },
todo: { icon: '📋', color: '#FF9800', label: '待办' },
// 错误/危险类
danger: { icon: '❌', color: '#F44336', label: '危险' },
error: { icon: '❌', color: '#F44336', label: '错误' },
bug: { icon: '🐛', color: '#F44336', label: 'Bug' },
important: { icon: '⭐', color: '#F44336', label: '重要' },
// 其他
example: { icon: '📋', color: '#9C27B0', label: '示例' },
quote: { icon: '💬', color: 'gray', label: '引用' },
cite: { icon: '💬', color: 'gray', label: '引用' }
};
// Callout 检测正则 (支持 +/- 折叠标记)
const CALLOUT_PATTERN = /^>\s*\[!(\w+)\][+-]?(?:\s+(.+))?$/;
```
### Step 3: Callout 解析器
```javascript
function parseCallouts(text) {
const lines = text.split('\n');
const result = [];
let i = 0;
while (i < lines.length) {
const match = lines[i].match(CALLOUT_PATTERN);
if (match) {
const type = match[1].toLowerCase();
const title = match[2] || null;
const content = [];
i++;
// 收集 Callout 内容行
while (i < lines.length && lines[i].startsWith('>')) {
content.push(lines[i].replace(/^>\s*/, ''));
i++;
}
result.push({
isCallout: true,
type,
title,
content: content.join('\n')
});
} else {
result.push({ isCallout: false, line: lines[i] });
i++;
}
}
return result;
}
```
### Step 4: BBCode+MD 转换器
```javascript
function formatBBCodeMD(text) {
let result = text;
// ===== 标题转换 (像素级字号) =====
result = result.replace(/^######\s*(.+)$/gm, '[b]$1[/b]');
result = result.replace(/^#####\s*(.+)$/gm, '[b]$1[/b]');
result = result.replace(/^####\s*(.+)$/gm, '[b]$1[/b]');
result = result.replace(/^###\s*(.+)$/gm, '[size=100][color=#333][b]$1[/b][/color][/size]');
result = result.replace(/^##\s*(.+)$/gm, '[size=120][color=#2196F3][b]$1[/b][/color][/size]');
result = result.replace(/^#\s*(.+)$/gm, '[size=150][color=#2196F3][b]$1[/b][/color][/size]');
// ===== 文本样式 =====
result = result.replace(/\*\*\*(.+?)\*\*\*/g, '[b][i]$1[/i][/b]');
result = result.replace(/\*\*(.+?)\*\*/g, '[b]$1[/b]');
result = result.replace(/\*(.+?)\*/g, '[i]$1[/i]');
result = result.replace(/~~(.+?)~~/g, '[s]$1[/s]');
result = result.replace(/==(.+?)==/g, '[color=yellow]$1[/color]');
// ===== 代码 =====
result = result.replace(/```(\w*)\n([\s\S]*?)```/g, '[code]$2[/code]');
// 行内代码保持原样 (部分论坛不支持 font=monospace)
// ===== 链接和图片 =====
result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '[url=$2]$1[/url]');
result = result.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '[img]$2[/img]');
// ===== 引用 (非 Callout) =====
result = result.replace(/^>\s+(.+)$/gm, '[quote]$1[/quote]');
// ===== 列表 (使用 • 符号) =====
result = result.replace(/^[-*+]\s+(.+)$/gm, '• $1');
// ===== 分隔线 (保持 Markdown 语法) =====
// `---` 在混合格式中通常可用,不转换为 [hr]
return result.trim();
}
```
### Step 5: Callout 转换
```javascript
function convertCallouts(text) {
const parsed = parseCallouts(text);
return parsed.map(item => {
if (item.isCallout) {
const cfg = CALLOUT_CONFIG[item.type] || CALLOUT_CONFIG.note;
const displayTitle = item.title || cfg.label;
// 使用 [quote] 包裹,标题使用 size=100
return `[quote]
[size=100][color=${cfg.color}][b]${cfg.icon} ${displayTitle}[/b][/color][/size]
${item.content}
[/quote]`;
}
return item.line;
}).join('\n');
}
```
### Step 6: 执行转换
```javascript
// 1. 先处理 Callouts
let formattedContent = convertCallouts(content);
// 2. 再进行通用 BBCode+MD 转换
formattedContent = formatBBCodeMD(formattedContent);
// 3. 清理多余空行
formattedContent = formattedContent.replace(/\n{3,}/g, '\n\n');
```
### Step 7: 保存转换结果
```javascript
const outputFile = 'output.bbcode.txt';
Write(`${workDir}/${outputFile}`, formattedContent);
// 更新配置
config.output_file = outputFile;
config.formatted_content = formattedContent;
Write(`${workDir}/input-config.json`, JSON.stringify(config, null, 2));
```
## Output
- **File**: `output.bbcode.txt`
- **Format**: BBCode + Markdown 混合格式
## Quality Checklist
- [ ] 标题使用像素值 (150/120/100)
- [ ] 未使用 `[align]` 标签
- [ ] 未使用 `[hr]` 标签
- [ ] 分隔线使用 `---`
- [ ] Callout 正确转换为 [quote]
- [ ] 颜色值使用 hex 格式
- [ ] 内容完整无丢失
## Next Phase
→ [Phase 4: Output & Preview](04-output-preview.md)

View File

@@ -0,0 +1,183 @@
# Phase 4: Output & Preview
输出最终结果并提供预览。
## Objective
- 保存格式化后的内容到文件
- 提供预览功能
- 显示转换统计信息
## Input
- 依赖: `input-config.json`, `output.*`
- 配置: `{workDir}/input-config.json`
## Execution Steps
### Step 1: 加载结果
```javascript
const config = JSON.parse(Read(`${workDir}/input-config.json`));
const analysis = JSON.parse(Read(`${workDir}/analysis.json`));
const outputFile = `${workDir}/${config.output_file}`;
const formattedContent = Read(outputFile);
```
### Step 2: 生成统计摘要
```javascript
const summary = {
input: {
method: config.input_method,
original_length: config.original_content.length,
word_count: config.original_content.split(/\s+/).length
},
output: {
format: config.target_format,
file: outputFile,
length: formattedContent.length
},
elements: analysis.stats,
reading_time: analysis.semantics?.estimated_reading_time || 1
};
console.log(`
╔════════════════════════════════════════════════════════════════╗
║ Text Formatter Summary ║
╠════════════════════════════════════════════════════════════════╣
║ Input: ${summary.input.word_count} words (${summary.input.original_length} chars)
║ Output: ${summary.output.format}${summary.output.file}
║ Elements Converted:
║ • Headings: ${summary.elements.headings}
║ • Paragraphs: ${summary.elements.paragraphs}
║ • Lists: ${summary.elements.lists}
║ • Code Blocks: ${summary.elements.code_blocks}
║ • Links: ${summary.elements.links}
║ Estimated Reading Time: ${summary.reading_time} min
╚════════════════════════════════════════════════════════════════╝
`);
```
### Step 3: HTML 预览(如适用)
```javascript
if (config.target_format === 'HTML') {
// 生成完整 HTML 文件用于预览
const previewHtml = `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Text Formatter Preview</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
max-width: 800px;
margin: 0 auto;
padding: 2rem;
background: #f5f5f5;
}
.content {
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1, h2, h3, h4, h5, h6 { color: #333; margin-top: 1.5em; }
code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; }
pre { background: #282c34; color: #abb2bf; padding: 1rem; border-radius: 6px; overflow-x: auto; }
pre code { background: none; padding: 0; }
blockquote { border-left: 4px solid #ddd; margin: 0; padding-left: 1rem; color: #666; }
a { color: #0066cc; }
img { max-width: 100%; }
hr { border: none; border-top: 1px solid #ddd; margin: 2rem 0; }
</style>
</head>
<body>
<div class="content">
${formattedContent}
</div>
</body>
</html>`;
Write(`${workDir}/preview.html`, previewHtml);
// 可选:在浏览器中打开预览
// Bash(`start "${workDir}/preview.html"`); // Windows
// Bash(`open "${workDir}/preview.html"`); // macOS
}
```
### Step 4: 显示输出内容
```javascript
// 显示格式化后的内容
console.log('\n=== Formatted Content ===\n');
console.log(formattedContent);
console.log('\n=========================\n');
// 提示用户
console.log(`
📁 Output saved to: ${outputFile}
${config.target_format === 'HTML' ? '🌐 Preview available: ' + workDir + '/preview.html' : ''}
💡 Tips:
- Copy the content above for immediate use
- Or access the saved file at the path shown
`);
```
### Step 5: 询问后续操作
```javascript
const nextAction = await AskUserQuestion({
questions: [
{
question: "需要执行什么操作?",
header: "后续操作",
multiSelect: false,
options: [
{ label: "完成", description: "结束格式化流程" },
{ label: "转换为其他格式", description: "选择另一种输出格式" },
{ label: "重新编辑", description: "修改原始内容后重新格式化" }
]
}
]
});
if (nextAction["后续操作"] === "转换为其他格式") {
// 返回 Phase 1 选择新格式
console.log('请重新运行 /text-formatter 选择其他格式');
}
```
## Output
- **File**: `output.{ext}` (最终输出)
- **File**: `preview.html` (HTML 预览,仅 HTML 格式)
- **Console**: 统计摘要和格式化内容
## Final Output Structure
```
{workDir}/
├── input-config.json # 配置信息
├── analysis.json # 分析结果
├── output.md # Markdown 输出(如选择)
├── output.bbcode.txt # BBCode 输出(如选择)
├── output.html # HTML 输出(如选择)
└── preview.html # HTML 预览页面
```
## Quality Checklist
- [ ] 输出文件已保存
- [ ] 统计信息正确显示
- [ ] 预览功能可用HTML
- [ ] 用户可访问输出内容
## Completion
此为最终阶段,格式化流程完成。