refactor: extract CSV text cleaning to reusable method in manifest generator

This commit is contained in:
Davor Racić 2026-02-02 12:43:20 +01:00
parent 7c68f816b6
commit 039d1b2420
1 changed files with 24 additions and 20 deletions

View File

@ -22,6 +22,19 @@ class ManifestGenerator {
this.selectedIdes = []; this.selectedIdes = [];
} }
/**
* Clean text for CSV output by normalizing whitespace and escaping quotes
* @param {string} text - Text to clean
* @returns {string} Cleaned text safe for CSV
*/
cleanForCSV(text) {
if (!text) return '';
return text
.trim()
.replaceAll(/\s+/g, ' ') // Normalize all whitespace (including newlines) to single space
.replaceAll('"', '""'); // Escape quotes for CSV
}
/** /**
* Generate all manifests for the installation * Generate all manifests for the installation
* @param {string} bmadDir - _bmad * @param {string} bmadDir - _bmad
@ -202,7 +215,7 @@ class ManifestGenerator {
// Workflows with standalone: false are filtered out above // Workflows with standalone: false are filtered out above
workflows.push({ workflows.push({
name: workflow.name, name: workflow.name,
description: workflow.description.replaceAll('"', '""'), // Escape quotes for CSV description: this.cleanForCSV(workflow.description),
module: moduleName, module: moduleName,
path: installPath, path: installPath,
}); });
@ -320,24 +333,15 @@ class ManifestGenerator {
const agentName = entry.name.replace('.md', ''); const agentName = entry.name.replace('.md', '');
// Helper function to clean and escape CSV content
const cleanForCSV = (text) => {
if (!text) return '';
return text
.trim()
.replaceAll(/\s+/g, ' ') // Normalize whitespace
.replaceAll('"', '""'); // Escape quotes for CSV
};
agents.push({ agents.push({
name: agentName, name: agentName,
displayName: nameMatch ? nameMatch[1] : agentName, displayName: nameMatch ? nameMatch[1] : agentName,
title: titleMatch ? titleMatch[1] : '', title: titleMatch ? titleMatch[1] : '',
icon: iconMatch ? iconMatch[1] : '', icon: iconMatch ? iconMatch[1] : '',
role: roleMatch ? cleanForCSV(roleMatch[1]) : '', role: roleMatch ? this.cleanForCSV(roleMatch[1]) : '',
identity: identityMatch ? cleanForCSV(identityMatch[1]) : '', identity: identityMatch ? this.cleanForCSV(identityMatch[1]) : '',
communicationStyle: styleMatch ? cleanForCSV(styleMatch[1]) : '', communicationStyle: styleMatch ? this.cleanForCSV(styleMatch[1]) : '',
principles: principlesMatch ? cleanForCSV(principlesMatch[1]) : '', principles: principlesMatch ? this.cleanForCSV(principlesMatch[1]) : '',
module: moduleName, module: moduleName,
path: installPath, path: installPath,
}); });
@ -404,7 +408,7 @@ class ManifestGenerator {
const frontmatter = yaml.parse(frontmatterMatch[1]); const frontmatter = yaml.parse(frontmatterMatch[1]);
name = frontmatter.name || name; name = frontmatter.name || name;
displayName = frontmatter.displayName || frontmatter.name || name; displayName = frontmatter.displayName || frontmatter.name || name;
description = frontmatter.description || ''; description = this.cleanForCSV(frontmatter.description || '');
standalone = frontmatter.standalone === true || frontmatter.standalone === 'true'; standalone = frontmatter.standalone === true || frontmatter.standalone === 'true';
} catch { } catch {
// If YAML parsing fails, use defaults // If YAML parsing fails, use defaults
@ -417,7 +421,7 @@ class ManifestGenerator {
const descMatch = content.match(/description="([^"]+)"/); const descMatch = content.match(/description="([^"]+)"/);
const objMatch = content.match(/<objective>([^<]+)<\/objective>/); const objMatch = content.match(/<objective>([^<]+)<\/objective>/);
description = descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : ''; description = this.cleanForCSV(descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : '');
const standaloneMatch = content.match(/<task[^>]+standalone="true"/); const standaloneMatch = content.match(/<task[^>]+standalone="true"/);
standalone = !!standaloneMatch; standalone = !!standaloneMatch;
@ -430,7 +434,7 @@ class ManifestGenerator {
tasks.push({ tasks.push({
name: name, name: name,
displayName: displayName, displayName: displayName,
description: description.replaceAll('"', '""'), description: description,
module: moduleName, module: moduleName,
path: installPath, path: installPath,
standalone: standalone, standalone: standalone,
@ -498,7 +502,7 @@ class ManifestGenerator {
const frontmatter = yaml.parse(frontmatterMatch[1]); const frontmatter = yaml.parse(frontmatterMatch[1]);
name = frontmatter.name || name; name = frontmatter.name || name;
displayName = frontmatter.displayName || frontmatter.name || name; displayName = frontmatter.displayName || frontmatter.name || name;
description = frontmatter.description || ''; description = this.cleanForCSV(frontmatter.description || '');
standalone = frontmatter.standalone === true || frontmatter.standalone === 'true'; standalone = frontmatter.standalone === true || frontmatter.standalone === 'true';
} catch { } catch {
// If YAML parsing fails, use defaults // If YAML parsing fails, use defaults
@ -511,7 +515,7 @@ class ManifestGenerator {
const descMatch = content.match(/description="([^"]+)"/); const descMatch = content.match(/description="([^"]+)"/);
const objMatch = content.match(/<objective>([^<]+)<\/objective>/); const objMatch = content.match(/<objective>([^<]+)<\/objective>/);
description = descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : ''; description = this.cleanForCSV(descMatch ? descMatch[1] : objMatch ? objMatch[1].trim() : '');
const standaloneMatch = content.match(/<tool[^>]+standalone="true"/); const standaloneMatch = content.match(/<tool[^>]+standalone="true"/);
standalone = !!standaloneMatch; standalone = !!standaloneMatch;
@ -524,7 +528,7 @@ class ManifestGenerator {
tools.push({ tools.push({
name: name, name: name,
displayName: displayName, displayName: displayName,
description: description.replaceAll('"', '""'), description: description,
module: moduleName, module: moduleName,
path: installPath, path: installPath,
standalone: standalone, standalone: standalone,