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:
2025-11-27 09:10:18 +01:00
parent cf86b45b93
commit b17d6a30e4
4 changed files with 464 additions and 9 deletions

View File

@@ -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')
})
})
})
})