generated from coulomb/repo-seed
refactor: implement template-based SVG generation
Implements the refactoring plan to move SVG structure from hardcoded JavaScript to editable template files: - Add template-v2.svg files with template elements in defs - Create template elements for months, lanes, and tasks with placeholders - Refactor generator.js to extract and clone template elements - Add hasTemplateElements, extractTemplate, and replacePlaceholders methods - Add generateFromTemplates method for template-based rendering - Keep generateHardcoded method for backward compatibility - Add comprehensive tests for new template system - Support both v1 (hardcoded) and v2 (template-based) approaches Benefits: - Templates can now be edited in SVG tools (Inkscape, Adobe Illustrator) - Visual template design with actual layout visible while editing - Separation of presentation (SVG) and logic (JavaScript) - Easier customization without touching code 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
81
example/template-v2.svg
Normal file
81
example/template-v2.svg
Normal file
@@ -0,0 +1,81 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="background: linear-gradient(135deg, #f0f9f4 0%, #e6f7ea 100%);">
|
||||
<defs>
|
||||
<!-- Enhanced month indicator styling -->
|
||||
<linearGradient id="monthHeaderGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#2d8659;stop-opacity:0.15"/>
|
||||
<stop offset="100%" style="stop-color:#4a9b6b;stop-opacity:0.08"/>
|
||||
</linearGradient>
|
||||
|
||||
<!-- Drop shadow for month labels -->
|
||||
<filter id="textShadow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feDropShadow dx="1" dy="1" stdDeviation="1.5" flood-color="#2d8659" flood-opacity="0.4"/>
|
||||
</filter>
|
||||
|
||||
<!-- Subtle background grid pattern -->
|
||||
<pattern id="bgGrid" width="40" height="40" patternUnits="userSpaceOnUse">
|
||||
<rect width="40" height="40" fill="transparent"/>
|
||||
<circle cx="20" cy="20" r="1" fill="#4a9b6b" opacity="0.1"/>
|
||||
</pattern>
|
||||
|
||||
<!-- Template elements (hidden, will be cloned) -->
|
||||
<g id="month-template" style="display: none;">
|
||||
<line x1="{{MONTH_X}}" y1="{{GRID_TOP}}" x2="{{MONTH_X}}" y2="{{GRID_BOTTOM}}"
|
||||
stroke="#4a9b6b" stroke-width="2" opacity="0.6"/>
|
||||
<rect x="{{MONTH_X_OFFSET}}" y="{{MONTH_LABEL_Y_OFFSET}}" width="60" height="25"
|
||||
fill="#2d8659" opacity="0.1" rx="4"/>
|
||||
<text x="{{MONTH_TEXT_X}}" y="{{MONTH_LABEL_Y}}" fill="#2d8659"
|
||||
font-size="13" font-weight="600">{{MONTH_LABEL}}</text>
|
||||
<rect x="{{MONTH_SEP_X}}" y="{{GRID_TOP}}" width="2" height="{{GRID_HEIGHT}}"
|
||||
fill="#4a9b6b" opacity="0.3"/>
|
||||
</g>
|
||||
|
||||
<g id="lane-template" style="display: none;">
|
||||
<rect x="{{LANE_X}}" y="{{LANE_Y}}" width="{{LANE_WIDTH}}" height="{{LANE_HEIGHT}}"
|
||||
fill="rgba(255,255,255,0.7)" stroke="#4a9b6b" stroke-width="1" opacity="0.5" rx="8"/>
|
||||
<text x="{{LABEL_X}}" y="{{LABEL_Y}}" fill="#2d8659"
|
||||
font-size="14" font-weight="700">{{LANE_NAME}}</text>
|
||||
</g>
|
||||
|
||||
<g id="task-template" style="display: none;">
|
||||
<circle cx="{{TASK_X}}" cy="{{TASK_Y}}" r="6"
|
||||
fill="#2d8659" stroke="#4a9b6b" stroke-width="2"/>
|
||||
<text x="{{TEXT_X}}" y="{{TEXT_Y}}" font-size="12" fill="#2d8659" font-weight="500">
|
||||
<tspan class="item-id">{{TASK_ID}}: </tspan>
|
||||
<tspan class="item-title" fill="#1e5a3d">{{TASK_TITLE}}</tspan>
|
||||
</text>
|
||||
</g>
|
||||
</defs>
|
||||
|
||||
<!-- Background with subtle pattern -->
|
||||
<rect width="100%" height="100%" fill="url(#bgGrid)"/>
|
||||
|
||||
<!-- Enhanced month header background -->
|
||||
<rect x="0" y="0" width="100%" height="130" fill="url(#monthHeaderGrad)"
|
||||
stroke="#2d8659" stroke-width="1" opacity="0.6"/>
|
||||
|
||||
<!-- Title area with visual indicator -->
|
||||
<rect x="10" y="10" width="300" height="60" fill="#ffffff"
|
||||
stroke="#2d8659" stroke-width="2" rx="8" opacity="0.9"/>
|
||||
<text x="20" y="35" fill="#2d8659" font-size="16" font-weight="bold" filter="url(#textShadow)">
|
||||
📊 Template V2 Active
|
||||
</text>
|
||||
<text x="20" y="55" fill="#4a9b6b" font-size="11" font-weight="500">
|
||||
Template-based rendering with cloned elements ✨
|
||||
</text>
|
||||
|
||||
<!-- Month indicators with enhanced styling -->
|
||||
<g class="enhanced-months" transform="translate(0,0)">
|
||||
<rect x="0" y="75" width="100%" height="55" fill="rgba(45, 134, 89, 0.05)"
|
||||
stroke="#4a9b6b" stroke-width="1"/>
|
||||
{{MONTHS}}
|
||||
</g>
|
||||
|
||||
<!-- Lane content -->
|
||||
<g class="enhanced-lanes">
|
||||
{{LANES}}
|
||||
</g>
|
||||
|
||||
<!-- Decorative border -->
|
||||
<rect x="1" y="1" width="calc(100% - 2)" height="calc(100% - 2)"
|
||||
fill="none" stroke="#2d8659" stroke-width="2" rx="4" opacity="0.7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
174
generator.js
174
generator.js
@@ -36,6 +36,171 @@ 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, {
|
||||
months, start, laneMap, laneNames,
|
||||
left, top, monthWidth, laneHeight, laneGap
|
||||
});
|
||||
},
|
||||
|
||||
escapeXml(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
},
|
||||
|
||||
// Check if template has template elements
|
||||
hasTemplateElements(template) {
|
||||
return template.includes('id="month-template"') &&
|
||||
template.includes('id="lane-template"') &&
|
||||
template.includes('id="task-template"');
|
||||
},
|
||||
|
||||
// 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;
|
||||
},
|
||||
|
||||
// Replace placeholders in template string
|
||||
replacePlaceholders(template, values) {
|
||||
let result = template;
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
result = result.replace(new RegExp(`{{${key}}}`, 'g'), value);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
// Generate SVG using template-based approach
|
||||
generateFromTemplates(items, cfg, template, layout) {
|
||||
const { months, start, laneMap, laneNames, left, top, monthWidth, laneHeight, laneGap } = layout;
|
||||
|
||||
// Extract template elements
|
||||
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;
|
||||
|
||||
// Generate months using template
|
||||
let monthGraphics = "";
|
||||
months.forEach((m, i) => {
|
||||
const x = left + i * monthWidth;
|
||||
const label = m.toLocaleString("de-DE", { month: "short", year: "2-digit" });
|
||||
|
||||
const monthValues = {
|
||||
MONTH_X: x,
|
||||
GRID_TOP: gridTop,
|
||||
GRID_BOTTOM: gridBottom,
|
||||
MONTH_X_OFFSET: x - 30,
|
||||
MONTH_LABEL_Y_OFFSET: monthLabelY - 20,
|
||||
MONTH_TEXT_X: x + 4,
|
||||
MONTH_LABEL_Y: monthLabelY,
|
||||
MONTH_LABEL: label,
|
||||
MONTH_SEP_X: x - 1,
|
||||
GRID_HEIGHT: gridBottom - gridTop
|
||||
};
|
||||
|
||||
let monthElement = this.replacePlaceholders(monthTemplate, monthValues);
|
||||
// Remove display:none and id to make element visible
|
||||
monthElement = monthElement.replace(/style="display:\s*none;?"/, '');
|
||||
monthElement = monthElement.replace(/id="month-template"/, '');
|
||||
monthGraphics += monthElement;
|
||||
});
|
||||
|
||||
// Helper to compute month index
|
||||
function monthIndexForDate(d) {
|
||||
return (d.getFullYear() - start.getFullYear()) * 12
|
||||
+ (d.getMonth() - start.getMonth());
|
||||
}
|
||||
|
||||
// Generate lanes using template
|
||||
let laneBlocks = "";
|
||||
laneNames.forEach((laneName, laneIdx) => {
|
||||
const laneY = top + laneIdx * (laneHeight + laneGap);
|
||||
const laneWidth = left + months.length * monthWidth;
|
||||
|
||||
const laneValues = {
|
||||
LANE_X: 40,
|
||||
LANE_Y: laneY - 24,
|
||||
LANE_WIDTH: laneWidth,
|
||||
LANE_HEIGHT: laneHeight,
|
||||
LABEL_X: 56,
|
||||
LABEL_Y: laneY - 4,
|
||||
LANE_NAME: this.escapeXml(laneName)
|
||||
};
|
||||
|
||||
let laneElement = this.replacePlaceholders(laneTemplate, laneValues);
|
||||
laneElement = laneElement.replace(/style="display:\s*none;?"/, '');
|
||||
laneElement = laneElement.replace(/id="lane-template"/, '');
|
||||
laneBlocks += laneElement;
|
||||
|
||||
// Generate tasks for this lane
|
||||
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;
|
||||
|
||||
const taskValues = {
|
||||
TASK_X: cx,
|
||||
TASK_Y: cy,
|
||||
TEXT_X: cx + 12,
|
||||
TEXT_Y: cy + 4,
|
||||
TASK_ID: this.escapeXml(it.id || ""),
|
||||
TASK_TITLE: this.escapeXml(it.title || "")
|
||||
};
|
||||
|
||||
let taskElement = this.replacePlaceholders(taskTemplate, taskValues);
|
||||
taskElement = taskElement.replace(/style="display:\s*none;?"/, '');
|
||||
taskElement = taskElement.replace(/id="task-template"/, '');
|
||||
laneBlocks += taskElement;
|
||||
});
|
||||
});
|
||||
|
||||
// Calculate dimensions
|
||||
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;
|
||||
},
|
||||
|
||||
// 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;
|
||||
@@ -156,14 +321,5 @@ window.timelineGenerator = {
|
||||
${monthGraphics}
|
||||
${laneBlocks}
|
||||
</svg>`;
|
||||
},
|
||||
|
||||
escapeXml(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
};
|
||||
|
||||
81
my-project/template-v2.svg
Normal file
81
my-project/template-v2.svg
Normal file
@@ -0,0 +1,81 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);">
|
||||
<defs>
|
||||
<!-- Enhanced month indicator styling -->
|
||||
<linearGradient id="monthHeaderGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#3b82f6;stop-opacity:0.15"/>
|
||||
<stop offset="100%" style="stop-color:#60a5fa;stop-opacity:0.08"/>
|
||||
</linearGradient>
|
||||
|
||||
<!-- Drop shadow for month labels -->
|
||||
<filter id="textShadow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feDropShadow dx="1" dy="1" stdDeviation="1.5" flood-color="#3b82f6" flood-opacity="0.4"/>
|
||||
</filter>
|
||||
|
||||
<!-- Subtle background grid pattern -->
|
||||
<pattern id="bgGrid" width="40" height="40" patternUnits="userSpaceOnUse">
|
||||
<rect width="40" height="40" fill="transparent"/>
|
||||
<circle cx="20" cy="20" r="1" fill="#60a5fa" opacity="0.1"/>
|
||||
</pattern>
|
||||
|
||||
<!-- Template elements (hidden, will be cloned) -->
|
||||
<g id="month-template" style="display: none;">
|
||||
<line x1="{{MONTH_X}}" y1="{{GRID_TOP}}" x2="{{MONTH_X}}" y2="{{GRID_BOTTOM}}"
|
||||
stroke="#60a5fa" stroke-width="2" opacity="0.6"/>
|
||||
<rect x="{{MONTH_X_OFFSET}}" y="{{MONTH_LABEL_Y_OFFSET}}" width="60" height="25"
|
||||
fill="#3b82f6" opacity="0.1" rx="4"/>
|
||||
<text x="{{MONTH_TEXT_X}}" y="{{MONTH_LABEL_Y}}" fill="#3b82f6"
|
||||
font-size="13" font-weight="600">{{MONTH_LABEL}}</text>
|
||||
<rect x="{{MONTH_SEP_X}}" y="{{GRID_TOP}}" width="2" height="{{GRID_HEIGHT}}"
|
||||
fill="#60a5fa" opacity="0.3"/>
|
||||
</g>
|
||||
|
||||
<g id="lane-template" style="display: none;">
|
||||
<rect x="{{LANE_X}}" y="{{LANE_Y}}" width="{{LANE_WIDTH}}" height="{{LANE_HEIGHT}}"
|
||||
fill="rgba(255,255,255,0.7)" stroke="#60a5fa" stroke-width="1" opacity="0.5" rx="8"/>
|
||||
<text x="{{LABEL_X}}" y="{{LABEL_Y}}" fill="#3b82f6"
|
||||
font-size="14" font-weight="700">{{LANE_NAME}}</text>
|
||||
</g>
|
||||
|
||||
<g id="task-template" style="display: none;">
|
||||
<circle cx="{{TASK_X}}" cy="{{TASK_Y}}" r="6"
|
||||
fill="#3b82f6" stroke="#60a5fa" stroke-width="2"/>
|
||||
<text x="{{TEXT_X}}" y="{{TEXT_Y}}" font-size="12" fill="#3b82f6" font-weight="500">
|
||||
<tspan class="item-id">{{TASK_ID}}: </tspan>
|
||||
<tspan class="item-title" fill="#1e40af">{{TASK_TITLE}}</tspan>
|
||||
</text>
|
||||
</g>
|
||||
</defs>
|
||||
|
||||
<!-- Background with subtle pattern -->
|
||||
<rect width="100%" height="100%" fill="url(#bgGrid)"/>
|
||||
|
||||
<!-- Enhanced month header background -->
|
||||
<rect x="0" y="0" width="100%" height="130" fill="url(#monthHeaderGrad)"
|
||||
stroke="#3b82f6" stroke-width="1" opacity="0.6"/>
|
||||
|
||||
<!-- Title area with visual indicator -->
|
||||
<rect x="10" y="10" width="300" height="60" fill="#ffffff"
|
||||
stroke="#3b82f6" stroke-width="2" rx="8" opacity="0.9"/>
|
||||
<text x="20" y="35" fill="#3b82f6" font-size="16" font-weight="bold" filter="url(#textShadow)">
|
||||
📅 Template V2 Active
|
||||
</text>
|
||||
<text x="20" y="55" fill="#60a5fa" font-size="11" font-weight="500">
|
||||
Template-based rendering with cloned elements ✨
|
||||
</text>
|
||||
|
||||
<!-- Month indicators with enhanced styling -->
|
||||
<g class="enhanced-months" transform="translate(0,0)">
|
||||
<rect x="0" y="75" width="100%" height="55" fill="rgba(59, 130, 246, 0.05)"
|
||||
stroke="#60a5fa" stroke-width="1"/>
|
||||
{{MONTHS}}
|
||||
</g>
|
||||
|
||||
<!-- Lane content -->
|
||||
<g class="enhanced-lanes">
|
||||
{{LANES}}
|
||||
</g>
|
||||
|
||||
<!-- Decorative border -->
|
||||
<rect x="1" y="1" width="calc(100% - 2)" height="calc(100% - 2)"
|
||||
fill="none" stroke="#3b82f6" stroke-width="2" rx="4" opacity="0.7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
@@ -164,4 +164,141 @@ describe('Timeline Generator', () => {
|
||||
expect(result).toContain('T-2')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Template-based rendering', () => {
|
||||
let items, config
|
||||
|
||||
beforeEach(() => {
|
||||
items = createSampleItems()
|
||||
config = createSampleProject()
|
||||
})
|
||||
|
||||
describe('hasTemplateElements', () => {
|
||||
it('should detect template elements', () => {
|
||||
const templateWithElements = `
|
||||
<svg>
|
||||
<defs>
|
||||
<g id="month-template"></g>
|
||||
<g id="lane-template"></g>
|
||||
<g id="task-template"></g>
|
||||
</defs>
|
||||
</svg>
|
||||
`
|
||||
expect(timelineGenerator.hasTemplateElements(templateWithElements)).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false when template elements are missing', () => {
|
||||
const templateWithoutElements = '<svg><g>{{MONTHS}}</g></svg>'
|
||||
expect(timelineGenerator.hasTemplateElements(templateWithoutElements)).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false when only some template elements exist', () => {
|
||||
const partialTemplate = '<svg><g id="month-template"></g></svg>'
|
||||
expect(timelineGenerator.hasTemplateElements(partialTemplate)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('extractTemplate', () => {
|
||||
it('should extract template element by id', () => {
|
||||
const svg = `
|
||||
<svg>
|
||||
<g id="month-template">
|
||||
<line x1="{{X}}" y1="0"/>
|
||||
<text>{{LABEL}}</text>
|
||||
</g>
|
||||
</svg>
|
||||
`
|
||||
const result = timelineGenerator.extractTemplate(svg, 'month-template')
|
||||
expect(result).toContain('id="month-template"')
|
||||
expect(result).toContain('{{X}}')
|
||||
expect(result).toContain('{{LABEL}}')
|
||||
})
|
||||
|
||||
it('should return null when template not found', () => {
|
||||
const svg = '<svg><g id="other"></g></svg>'
|
||||
const result = timelineGenerator.extractTemplate(svg, 'month-template')
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('replacePlaceholders', () => {
|
||||
it('should replace all placeholders with values', () => {
|
||||
const template = '<text x="{{X}}" y="{{Y}}">{{LABEL}}</text>'
|
||||
const values = { X: 100, Y: 200, LABEL: 'Test' }
|
||||
const result = timelineGenerator.replacePlaceholders(template, values)
|
||||
expect(result).toBe('<text x="100" y="200">Test</text>')
|
||||
})
|
||||
|
||||
it('should handle multiple occurrences of same placeholder', () => {
|
||||
const template = '<g><rect x="{{X}}"/><circle cx="{{X}}"/></g>'
|
||||
const values = { X: 50 }
|
||||
const result = timelineGenerator.replacePlaceholders(template, values)
|
||||
expect(result).toBe('<g><rect x="50"/><circle cx="50"/></g>')
|
||||
})
|
||||
|
||||
it('should leave unmatched placeholders unchanged', () => {
|
||||
const template = '<text>{{LABEL}} {{OTHER}}</text>'
|
||||
const values = { LABEL: 'Test' }
|
||||
const result = timelineGenerator.replacePlaceholders(template, values)
|
||||
expect(result).toContain('Test')
|
||||
expect(result).toContain('{{OTHER}}')
|
||||
})
|
||||
})
|
||||
|
||||
describe('generateFromTemplates', () => {
|
||||
it('should generate SVG using template elements', async () => {
|
||||
// Read actual template-v2.svg
|
||||
const fs = await import('fs/promises')
|
||||
const templateV2 = await fs.readFile('./example/template-v2.svg', 'utf-8')
|
||||
|
||||
const result = timelineGenerator.generate(items, config, templateV2)
|
||||
|
||||
// Should contain SVG structure
|
||||
expect(result).toContain('<svg')
|
||||
expect(result).toContain('width=')
|
||||
expect(result).toContain('height=')
|
||||
|
||||
// Should not contain template placeholders
|
||||
expect(result).not.toContain('{{MONTH_X}}')
|
||||
expect(result).not.toContain('{{LANE_Y}}')
|
||||
expect(result).not.toContain('{{TASK_X}}')
|
||||
|
||||
// Should contain actual data
|
||||
expect(result).toContain('T-1')
|
||||
expect(result).toContain('First Task')
|
||||
expect(result).toContain('Development')
|
||||
})
|
||||
|
||||
it('should fall back to hardcoded generation when templates incomplete', () => {
|
||||
const incompleteTemplate = `
|
||||
<svg>
|
||||
<defs>
|
||||
<g id="month-template"></g>
|
||||
</defs>
|
||||
{{MONTHS}}
|
||||
{{LANES}}
|
||||
</svg>
|
||||
`
|
||||
|
||||
const result = timelineGenerator.generate(items, config, incompleteTemplate)
|
||||
|
||||
// Should still generate valid SVG
|
||||
expect(result).toContain('<svg')
|
||||
expect(result).toContain('T-1')
|
||||
expect(result).toContain('Development')
|
||||
})
|
||||
|
||||
it('should preserve template styling in generated output', async () => {
|
||||
const fs = await import('fs/promises')
|
||||
const templateV2 = await fs.readFile('./example/template-v2.svg', 'utf-8')
|
||||
|
||||
const result = timelineGenerator.generate(items, config, templateV2)
|
||||
|
||||
// Should preserve gradient and filter definitions
|
||||
expect(result).toContain('monthHeaderGrad')
|
||||
expect(result).toContain('textShadow')
|
||||
expect(result).toContain('bgGrid')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user