generated from coulomb/repo-seed
refactor: complete migration to template-v2 architecture
- Remove legacy template.svg files from example/ and my-project/ - Simplify generator.js by removing generateHardcoded method (326→210 lines, -36%) - Add strict template validation with clear error messages - Remove all fallback mechanisms - template-v2.svg format now required - Clean up tests: remove hardcoded generation tests, keep template-based tests - Add comprehensive e2e tests (large datasets, edge cases, error handling) - Update documentation: mark REFACTORING_PLAN.md complete, add TEMPLATE_V2_GUIDE.md - All 56 tests passing (16 engine + 25 generator + 15 integration) BREAKING CHANGE: Old template.svg format no longer supported. Must use template-v2.svg with <g id="*-template"> elements in defs section. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
200
generator.js
200
generator.js
@@ -1,5 +1,13 @@
|
||||
window.timelineGenerator = {
|
||||
generate(items, cfg, template) {
|
||||
// Validate template is provided
|
||||
if (!template) {
|
||||
throw new Error('Template is required. Please provide a template-v2.svg file with template elements.');
|
||||
}
|
||||
|
||||
// Validate template has required elements
|
||||
this.validateTemplate(template);
|
||||
|
||||
const monthsRange = (cfg.settings && cfg.settings.timelineMonths) || 18;
|
||||
|
||||
// Determine time window from earliest due date
|
||||
@@ -36,42 +44,41 @@ window.timelineGenerator = {
|
||||
const laneHeight = 80;
|
||||
const laneGap = 16;
|
||||
|
||||
// Check if template uses new template-based approach
|
||||
if (template && this.hasTemplateElements(template)) {
|
||||
return this.generateFromTemplates(items, cfg, template, {
|
||||
months, start, laneMap, laneNames,
|
||||
left, top, monthWidth, laneHeight, laneGap
|
||||
});
|
||||
}
|
||||
|
||||
// Use hardcoded generation for backward compatibility
|
||||
return this.generateHardcoded(items, cfg, template, {
|
||||
return this.generateFromTemplates(items, cfg, template, {
|
||||
months, start, laneMap, laneNames,
|
||||
left, top, monthWidth, laneHeight, laneGap
|
||||
});
|
||||
},
|
||||
|
||||
escapeXml(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
},
|
||||
// Validate template has required elements
|
||||
validateTemplate(template) {
|
||||
const hasMonthTemplate = template.includes('id="month-template"');
|
||||
const hasLaneTemplate = template.includes('id="lane-template"');
|
||||
const hasTaskTemplate = template.includes('id="task-template"');
|
||||
|
||||
// Check if template has template elements
|
||||
hasTemplateElements(template) {
|
||||
return template.includes('id="month-template"') &&
|
||||
template.includes('id="lane-template"') &&
|
||||
template.includes('id="task-template"');
|
||||
if (!hasMonthTemplate || !hasLaneTemplate || !hasTaskTemplate) {
|
||||
const missing = [];
|
||||
if (!hasMonthTemplate) missing.push('month-template');
|
||||
if (!hasLaneTemplate) missing.push('lane-template');
|
||||
if (!hasTaskTemplate) missing.push('task-template');
|
||||
|
||||
throw new Error(
|
||||
`Template is missing required elements: ${missing.join(', ')}. ` +
|
||||
`Please use a template-v2.svg file with proper template elements in the <defs> section.`
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
// Extract a template element from SVG
|
||||
extractTemplate(template, id) {
|
||||
const regex = new RegExp(`<g id="${id}"[^>]*>([\\s\\S]*?)</g>`, 'i');
|
||||
const match = template.match(regex);
|
||||
return match ? match[0] : null;
|
||||
|
||||
if (!match) {
|
||||
throw new Error(`Failed to extract template element: ${id}`);
|
||||
}
|
||||
|
||||
return match[0];
|
||||
},
|
||||
|
||||
// Replace placeholders in template string
|
||||
@@ -87,16 +94,11 @@ window.timelineGenerator = {
|
||||
generateFromTemplates(items, cfg, template, layout) {
|
||||
const { months, start, laneMap, laneNames, left, top, monthWidth, laneHeight, laneGap } = layout;
|
||||
|
||||
// Extract template elements
|
||||
// Extract template elements (will throw if not found)
|
||||
const monthTemplate = this.extractTemplate(template, 'month-template');
|
||||
const laneTemplate = this.extractTemplate(template, 'lane-template');
|
||||
const taskTemplate = this.extractTemplate(template, 'task-template');
|
||||
|
||||
if (!monthTemplate || !laneTemplate || !taskTemplate) {
|
||||
console.warn('Template elements not found, falling back to hardcoded generation');
|
||||
return this.generateHardcoded(items, cfg, template, layout);
|
||||
}
|
||||
|
||||
const monthLabelY = 90;
|
||||
const gridTop = top - 20;
|
||||
const gridBottom = top + laneNames.length * (laneHeight + laneGap) + 40;
|
||||
@@ -187,6 +189,12 @@ window.timelineGenerator = {
|
||||
.replace("{{MONTHS}}", monthGraphics)
|
||||
.replace("{{LANES}}", laneBlocks);
|
||||
|
||||
// Remove template elements from defs (they should not appear in final output)
|
||||
processedTemplate = processedTemplate
|
||||
.replace(/<g id="month-template"[^>]*>[\s\S]*?<\/g>/, '')
|
||||
.replace(/<g id="lane-template"[^>]*>[\s\S]*?<\/g>/, '')
|
||||
.replace(/<g id="task-template"[^>]*>[\s\S]*?<\/g>/, '');
|
||||
|
||||
// Add width and height attributes to the SVG element
|
||||
processedTemplate = processedTemplate.replace(
|
||||
/<svg([^>]*?)>/,
|
||||
@@ -196,130 +204,12 @@ window.timelineGenerator = {
|
||||
return processedTemplate;
|
||||
},
|
||||
|
||||
// Original hardcoded generation (for backward compatibility)
|
||||
generateHardcoded(items, cfg, template, layout) {
|
||||
const { months, start, laneMap, laneNames, left, top, monthWidth, laneHeight, laneGap } = layout;
|
||||
|
||||
// This is the original hardcoded generation that was in the generate() method
|
||||
// Month grid (labels + vertical lines)
|
||||
let monthGraphics = "";
|
||||
const monthLabelY = 90;
|
||||
const gridTop = top - 20;
|
||||
const gridBottom = top + laneNames.length * (laneHeight + laneGap) + 40;
|
||||
|
||||
months.forEach((m, i) => {
|
||||
const x = left + i * monthWidth;
|
||||
const label = m.toLocaleString("de-DE", { month: "short", year: "2-digit" });
|
||||
|
||||
// Enhanced styling when using external template
|
||||
if (template && template.includes('Enhanced')) {
|
||||
// Determine color scheme based on template content
|
||||
const isBlueTheme = template.includes('My Project');
|
||||
const primaryColor = isBlueTheme ? '#3b82f6' : '#2d8659';
|
||||
const secondaryColor = isBlueTheme ? '#60a5fa' : '#4a9b6b';
|
||||
|
||||
// More prominent month indicators for external template
|
||||
monthGraphics += `<line x1="${x}" y1="${gridTop}" x2="${x}" y2="${gridBottom}" stroke="${secondaryColor}" stroke-width="2" opacity="0.6" />`;
|
||||
monthGraphics += `<rect x="${x-30}" y="${monthLabelY-20}" width="60" height="25" fill="${primaryColor}" opacity="0.1" rx="4" />`;
|
||||
monthGraphics += `<text x="${x + 4}" y="${monthLabelY}" fill="${primaryColor}" font-size="13" font-weight="600">${label}</text>`;
|
||||
// Add month separator
|
||||
if (i > 0) {
|
||||
monthGraphics += `<rect x="${x-1}" y="${gridTop}" width="2" height="${gridBottom-gridTop}" fill="${secondaryColor}" opacity="0.3" />`;
|
||||
}
|
||||
} else {
|
||||
// Default styling
|
||||
monthGraphics += `<line x1="${x}" y1="${gridTop}" x2="${x}" y2="${gridBottom}" stroke="#E3E8EF" />`;
|
||||
monthGraphics += `<text x="${x + 4}" y="${monthLabelY}" fill="#5C6B7A" font-size="12">${label}</text>`;
|
||||
}
|
||||
});
|
||||
|
||||
// Helper to compute month index
|
||||
function monthIndexForDate(d) {
|
||||
return (d.getFullYear() - start.getFullYear()) * 12
|
||||
+ (d.getMonth() - start.getMonth());
|
||||
}
|
||||
|
||||
// Lane blocks
|
||||
let laneBlocks = "";
|
||||
laneNames.forEach((laneName, laneIdx) => {
|
||||
const laneY = top + laneIdx * (laneHeight + laneGap);
|
||||
|
||||
// Enhanced styling when using external template
|
||||
if (template && template.includes('Enhanced')) {
|
||||
// Determine color scheme based on template content
|
||||
const isBlueTheme = template.includes('My Project');
|
||||
const primaryColor = isBlueTheme ? '#3b82f6' : '#2d8659';
|
||||
const secondaryColor = isBlueTheme ? '#60a5fa' : '#4a9b6b';
|
||||
|
||||
// More subtle lane borders for enhanced template
|
||||
laneBlocks += `<rect x="40" y="${laneY - 24}" width="${left + months.length * monthWidth}" height="${laneHeight}" fill="rgba(255,255,255,0.7)" stroke="${secondaryColor}" stroke-width="1" opacity="0.5" rx="8" />`;
|
||||
// Enhanced lane label
|
||||
laneBlocks += `<text x="56" y="${laneY - 4}" fill="${primaryColor}" font-size="14" font-weight="700">${this.escapeXml(laneName)}</text>`;
|
||||
} else {
|
||||
// Default lane styling
|
||||
laneBlocks += `<rect x="40" y="${laneY - 24}" width="${left + months.length * monthWidth}" height="${laneHeight}" fill="#FFFFFF" stroke="#E3E8EF" rx="10" />`;
|
||||
// Default lane label
|
||||
laneBlocks += `<text x="56" y="${laneY - 4}" fill="#0B1F3B" font-size="14" font-weight="600">${this.escapeXml(laneName)}</text>`;
|
||||
}
|
||||
|
||||
const laneItems = laneMap.get(laneName).sort((a, b) => a.due - b.due);
|
||||
laneItems.forEach((it, idx) => {
|
||||
const mi = monthIndexForDate(it.due);
|
||||
const clampedMi = Math.max(0, Math.min(months.length - 1, mi));
|
||||
const cx = left + clampedMi * monthWidth + monthWidth * 0.5;
|
||||
const cy = laneY + 10 + idx * 18;
|
||||
|
||||
// Enhanced task item styling for external template
|
||||
if (template && template.includes('Enhanced')) {
|
||||
// Determine color scheme based on template content
|
||||
const isBlueTheme = template.includes('My Project');
|
||||
const primaryColor = isBlueTheme ? '#3b82f6' : '#2d8659';
|
||||
const secondaryColor = isBlueTheme ? '#60a5fa' : '#4a9b6b';
|
||||
const darkColor = isBlueTheme ? '#1e40af' : '#1e5a3d';
|
||||
|
||||
laneBlocks += `<circle cx="${cx}" cy="${cy}" r="6" fill="${primaryColor}" stroke="${secondaryColor}" stroke-width="2" />`;
|
||||
laneBlocks += `<text x="${cx + 12}" y="${cy + 4}" font-size="12" fill="${primaryColor}" font-weight="500">
|
||||
<tspan class="item-id">${this.escapeXml(it.id || "")}: </tspan>
|
||||
<tspan class="item-title" fill="${darkColor}">${this.escapeXml(it.title || "")}</tspan>
|
||||
</text>`;
|
||||
} else {
|
||||
// Default task item styling
|
||||
laneBlocks += `<circle cx="${cx}" cy="${cy}" r="5" fill="#0A4D8C" />`;
|
||||
laneBlocks += `<text x="${cx + 10}" y="${cy + 4}" font-size="12" fill="#0B1F3B">
|
||||
<tspan class="item-id">${this.escapeXml(it.id || "")}: </tspan>
|
||||
<tspan class="item-title">${this.escapeXml(it.title || "")}</tspan>
|
||||
</text>`;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (template && template.includes("{{MONTHS}}") && template.includes("{{LANES}}")) {
|
||||
// Calculate dimensions for template
|
||||
const height = top + laneNames.length * (laneHeight + laneGap) + 80;
|
||||
const width = left + months.length * monthWidth + 100;
|
||||
|
||||
// Replace placeholders and inject calculated dimensions
|
||||
let processedTemplate = template
|
||||
.replace("{{MONTHS}}", monthGraphics)
|
||||
.replace("{{LANES}}", laneBlocks);
|
||||
|
||||
// Add width and height attributes to the SVG element
|
||||
processedTemplate = processedTemplate.replace(
|
||||
/<svg([^>]*?)>/,
|
||||
`<svg$1 width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">`
|
||||
);
|
||||
|
||||
return processedTemplate;
|
||||
}
|
||||
|
||||
// Fallback: embed directly in simple SVG
|
||||
const height = top + laneNames.length * (laneHeight + laneGap) + 80;
|
||||
const width = left + months.length * monthWidth + 100;
|
||||
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
|
||||
<rect width="100%" height="100%" fill="#FFFFFF" />
|
||||
${monthGraphics}
|
||||
${laneBlocks}
|
||||
</svg>`;
|
||||
escapeXml(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user