Skip to content

Commit 7ef3809

Browse files
committed
回传改为TCVB
1 parent ac74d6c commit 7ef3809

File tree

4 files changed

+376
-25
lines changed

4 files changed

+376
-25
lines changed

src/cvbManager.ts

Lines changed: 354 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const languageMapping: { [key: string]: string } = {
1818
'js': 'javascript'
1919
};
2020

21+
// ================== CVB 核心类 ==================
2122
export class Cvb {
2223
private content: string;
2324
private metadata: Record<string, string>;
@@ -34,6 +35,10 @@ export class Cvb {
3435
return this.metadata;
3536
}
3637

38+
setMetaData(key: string, metadata : string) {
39+
this.metadata[key] = metadata;
40+
}
41+
3742
getFiles(): Record<string, string> {
3843
return this.files;
3944
}
@@ -116,20 +121,357 @@ export class Cvb {
116121
files,
117122
};
118123
}
124+
125+
static getFormatDescription(): string {
126+
return `
127+
CVB 格式介绍:
128+
- 文件以 "## BEGIN_CVB" 开头,以 "## END_CVB" 结尾。
129+
- 元数据部分以 "## META" 开头,以 "## END_META" 结尾,包含用户需求和时间戳。
130+
- 每个文件以 "## FILE:文件路径" 开头,紧接着是 Markdown 格式的代码块,包含文件内容。
131+
- 多个文件按顺序拼接在一起。
132+
`;
133+
}
119134
}
120135

121-
/**
122-
* 返回 CVB 格式介绍的静态字符串
123-
* @returns CVB 格式介绍
124-
*/
125-
export function getCvbFormatDescription(): string {
126-
return `
127-
CVB 格式介绍:
128-
- 文件以 "## BEGIN_CVB" 开头,以 "## END_CVB" 结尾。
129-
- 元数据部分以 "## META" 开头,以 "## END_META" 结尾,包含用户需求和时间戳。
130-
- 每个文件以 "## FILE:文件路径" 开头,紧接着是 Markdown 格式的代码块,包含文件内容。
131-
- 多个文件按顺序拼接在一起。
132-
`;
136+
// ================== TCVB 差量格式 ==================
137+
abstract class CvbOperation {
138+
constructor(
139+
public readonly filePath: string,
140+
public readonly type: 'replace' | 'insert' | 'delete'
141+
) {}
142+
}
143+
144+
class ReplaceOperation extends CvbOperation {
145+
constructor(
146+
filePath: string,
147+
public readonly beforeAnchor: string,
148+
public readonly afterAnchor: string,
149+
public readonly oldContent: string,
150+
public readonly newContent: string
151+
) {
152+
super(filePath, 'replace');
153+
}
154+
}
155+
156+
class InsertOperation extends CvbOperation {
157+
constructor(
158+
filePath: string,
159+
public readonly beforeAnchor: string,
160+
public readonly afterAnchor: string,
161+
public readonly content: string
162+
) {
163+
super(filePath, 'insert');
164+
}
165+
}
166+
167+
class DeleteOperation extends CvbOperation {
168+
constructor(
169+
filePath: string,
170+
public readonly beforeAnchor: string,
171+
public readonly afterAnchor: string,
172+
public readonly oldContent: string
173+
) {
174+
super(filePath, 'delete');
175+
}
176+
}
177+
178+
export class TCVB {
179+
private operations: CvbOperation[] = [];
180+
181+
constructor(tcvbContent: string) {
182+
this.parse(tcvbContent);
183+
}
184+
185+
private parse(content: string) {
186+
const fileBlockRegex = /^## FILE:(.*?)\n([\s\S]*?)(?=^## FILE:|^## END_TCVB)/gm;
187+
let fileMatch: RegExpExecArray | null;
188+
189+
while ((fileMatch = fileBlockRegex.exec(content)) !== null) {
190+
const filePath = filePathNormalize(fileMatch[1]);
191+
const operationsBlock = fileMatch[2];
192+
193+
const operationRegex = /^## OPERATION:(\w+)(?:\s+FILE:(.*?))?\n([\s\S]*?)(?=^## OPERATION:|^## FILE:|^## END_TCVB)/gm;
194+
let opMatch: RegExpExecArray | null;
195+
196+
while ((opMatch = operationRegex.exec(operationsBlock)) !== null) {
197+
const type = opMatch[1].toLowerCase();
198+
const explicitFilePath = opMatch[2] ? filePathNormalize(opMatch[2]) : null;
199+
const operationContent = opMatch[3].trim();
200+
201+
const finalFilePath = explicitFilePath || filePath;
202+
this.parseOperation(finalFilePath, type, operationContent);
203+
}
204+
}
205+
}
206+
207+
private parseOperation(filePath: string, type: string, content: string) {
208+
try {
209+
switch (type) {
210+
case 'replace':
211+
this.parseReplace(filePath, content);
212+
break;
213+
case 'insert':
214+
this.parseInsert(filePath, content);
215+
break;
216+
case 'delete':
217+
this.parseDelete(filePath, content);
218+
break;
219+
default:
220+
throw new Error(`Unknown operation type: ${type}`);
221+
}
222+
} catch (e) {
223+
console.error(`Failed to parse ${type} operation for ${filePath}: ${e}`);
224+
}
225+
}
226+
227+
private parseReplace(filePath: string, content: string) {
228+
const sections = this.parseSections(content, ['BEFORE_ANCHOR', 'AFTER_ANCHOR', 'OLD_CONTENT', 'NEW_CONTENT']);
229+
this.operations.push(new ReplaceOperation(
230+
filePath,
231+
sections.BEFORE_ANCHOR,
232+
sections.AFTER_ANCHOR,
233+
sections.OLD_CONTENT,
234+
sections.NEW_CONTENT
235+
));
236+
}
237+
238+
private parseInsert(filePath: string, content: string) {
239+
const sections = this.parseSections(content, ['BEFORE_ANCHOR', 'AFTER_ANCHOR', 'INSERT_CONTENT']);
240+
this.operations.push(new InsertOperation(
241+
filePath,
242+
sections.BEFORE_ANCHOR,
243+
sections.AFTER_ANCHOR,
244+
sections.INSERT_CONTENT
245+
));
246+
}
247+
248+
private parseDelete(filePath: string, content: string) {
249+
const sections = this.parseSections(content, ['BEFORE_ANCHOR', 'AFTER_ANCHOR', 'DELETE_CONTENT']);
250+
this.operations.push(new DeleteOperation(
251+
filePath,
252+
sections.BEFORE_ANCHOR,
253+
sections.AFTER_ANCHOR,
254+
sections.DELETE_CONTENT
255+
));
256+
}
257+
258+
private parseSections(content: string, expectedSections: string[]): Record<string, string> {
259+
const result: Record<string, string> = {};
260+
let currentSection: string | null = null;
261+
let buffer: string[] = [];
262+
263+
for (const line of content.split('\n')) {
264+
const sectionMatch = line.match(/^## ([A-Z_]+)/);
265+
if (sectionMatch) {
266+
if (currentSection) {
267+
result[currentSection] = buffer.join('\n').trim();
268+
buffer = [];
269+
}
270+
currentSection = sectionMatch[1];
271+
if (!expectedSections.includes(currentSection)) {
272+
throw new Error(`Unexpected section: ${currentSection}`);
273+
}
274+
} else if (currentSection) {
275+
buffer.push(line);
276+
}
277+
}
278+
279+
if (currentSection) {
280+
result[currentSection] = buffer.join('\n').trim();
281+
}
282+
283+
// Validate required sections
284+
for (const section of expectedSections) {
285+
if (!(section in result)) {
286+
throw new Error(`Missing required section: ${section}`);
287+
}
288+
}
289+
290+
return result;
291+
}
292+
293+
getOperations(): CvbOperation[] {
294+
return [...this.operations];
295+
}
296+
297+
static getFormatDescription(): string {
298+
return `
299+
TCVB 格式规范(版本2.0):
300+
301+
## BEGIN_TCVB
302+
[文件块1]
303+
[文件块2]
304+
...
305+
## END_TCVB
306+
307+
文件块格式:
308+
## FILE:<文件路径>
309+
[操作1]
310+
[操作2]
311+
...
312+
313+
操作类型:
314+
1. 替换操作(REPLACE):
315+
## OPERATION:REPLACE
316+
## BEFORE_ANCHOR
317+
[前锚点内容]
318+
## AFTER_ANCHOR
319+
[后锚点内容]
320+
## OLD_CONTENT
321+
[被替换内容]
322+
## NEW_CONTENT
323+
[新内容]
324+
325+
2. 插入操作(INSERT):
326+
## OPERATION:INSERT
327+
## BEFORE_ANCHOR
328+
[插入位置前锚点]
329+
## AFTER_ANCHOR
330+
[插入位置后锚点]
331+
## INSERT_CONTENT
332+
[插入内容]
333+
334+
3. 删除操作(DELETE):
335+
## OPERATION:DELETE
336+
## BEFORE_ANCHOR
337+
[被删内容前锚点]
338+
## AFTER_ANCHOR
339+
[被删内容后锚点]
340+
## DELETE_CONTENT
341+
[被删除内容]
342+
343+
高级特性:
344+
1. 文件路径复用:同一文件下的多个操作共享FILE声明
345+
2. 混合操作:允许在文件块内任意顺序组合操作类型
346+
3. 精准锚点:使用至少3行唯一文本作为锚点
347+
4. 跨文件操作:可通过## OPERATION:TYPE FILE:path 临时指定其他文件
348+
349+
示例:
350+
## BEGIN_TCVB
351+
## FILE:src/app.js
352+
## OPERATION:REPLACE
353+
## BEFORE_ANCHOR
354+
function legacy() {
355+
console.log('old');
356+
## AFTER_ANCHOR
357+
}
358+
359+
## OLD_CONTENT
360+
return 100;
361+
## NEW_CONTENT
362+
return 200;
363+
364+
## OPERATION:INSERT
365+
## BEFORE_ANCHOR
366+
// == 配置开始 ==
367+
## AFTER_ANCHOR
368+
// == 配置结束 ==
369+
## INSERT_CONTENT
370+
timeout: 3000,
371+
372+
## FILE:README.md
373+
## OPERATION:DELETE
374+
## BEFORE_ANCHOR
375+
<!-- DEPRECATED SECTION -->
376+
## AFTER_ANCHOR
377+
<!-- END DEPRECATED -->
378+
## DELETE_CONTENT
379+
...旧内容...
380+
## END_TCVB
381+
`;
382+
}
383+
}
384+
385+
// ================== 合并函数 ==================
386+
export function mergeCvb(baseCvb: Cvb, tcvb: TCVB): Cvb {
387+
const mergedFiles = new Map<string, string>(Object.entries(baseCvb.getFiles()));
388+
389+
// 按文件分组操作
390+
const operationsByFile = new Map<string, CvbOperation[]>();
391+
for (const op of tcvb.getOperations()) {
392+
if (!operationsByFile.has(op.filePath)) {
393+
operationsByFile.set(op.filePath, []);
394+
}
395+
operationsByFile.get(op.filePath)!.push(op);
396+
}
397+
398+
// 处理每个文件的修改
399+
for (const [filePath, operations] of operationsByFile) {
400+
let content = mergedFiles.get(filePath) || '';
401+
402+
// 按操作顺序执行修改
403+
for (const op of operations) {
404+
if (op instanceof ReplaceOperation) {
405+
content = applyReplace(content, op);
406+
} else if (op instanceof InsertOperation) {
407+
content = applyInsert(content, op);
408+
} else if (op instanceof DeleteOperation) {
409+
content = applyDelete(content, op);
410+
}
411+
}
412+
413+
mergedFiles.set(filePath, content);
414+
}
415+
416+
// 重新生成CVB内容
417+
return rebuildCvb(baseCvb, mergedFiles);
418+
}
419+
420+
function applyReplace(content: string, op: ReplaceOperation): string {
421+
const pattern = buildPattern(op.beforeAnchor, op.oldContent, op.afterAnchor);
422+
const replacement = `${op.beforeAnchor}${op.newContent}${op.afterAnchor}`;
423+
return content.replace(pattern, replacement);
424+
}
425+
426+
function applyInsert(content: string, op: InsertOperation): string {
427+
const pattern = buildPattern(op.beforeAnchor, '', op.afterAnchor);
428+
const replacement = `${op.beforeAnchor}${op.content}${op.afterAnchor}`;
429+
return content.replace(pattern, replacement);
430+
}
431+
432+
function applyDelete(content: string, op: DeleteOperation): string {
433+
const pattern = buildPattern(op.beforeAnchor, op.oldContent, op.afterAnchor);
434+
return content.replace(pattern, `${op.beforeAnchor}${op.afterAnchor}`);
435+
}
436+
437+
function buildPattern(before: string, content: string, after: string): RegExp {
438+
return new RegExp(
439+
`${escapeRegExp(before)}${escapeRegExp(content)}${escapeRegExp(after)}`,
440+
'gs' // 使用dotall模式匹配换行
441+
);
442+
}
443+
444+
function rebuildCvb(baseCvb: Cvb, files: Map<string, string>): Cvb {
445+
let newContent = `## BEGIN_CVB\n## META\n`;
446+
447+
// 保留元数据
448+
const metadata = baseCvb.getMetadata();
449+
for (const [key, value] of Object.entries(metadata)) {
450+
newContent += `${key}: ${value}\n`;
451+
}
452+
newContent += `## END_META\n\n`;
453+
454+
// 重建文件内容
455+
for (const [filePath, content] of files) {
456+
const ext = path.extname(filePath).slice(1).toLowerCase();
457+
const lang = languageMapping[ext] || 'text';
458+
newContent += `## FILE:${filePath}\n\`\`\`${lang}\n${content}\n\`\`\`\n\n`;
459+
}
460+
461+
newContent += `## END_CVB`;
462+
const cvb = new Cvb(newContent);
463+
464+
cvb.setMetaData("时间戳", generateTimestamp());
465+
return cvb;
466+
}
467+
468+
// ================== 工具函数 ==================
469+
function escapeRegExp(str: string): string {
470+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
471+
}
472+
473+
function filePathNormalize(rawPath: string): string {
474+
return path.normalize(rawPath.replace(/^[\\/]+/, ''));
133475
}
134476

135477
/**

0 commit comments

Comments
 (0)