generated from coulomb/repo-seed
feat: add custom placeholder mapping for non-standard templates
Added optional placeholderMapping to project.json for templates that
don't follow the standard ITEM_{PROPERTY} convention.
Features:
- Override default placeholder names per field
- Useful for legacy templates or migration from other tools
- Fallback to ITEM_{PROPERTY} convention when not specified
Example usage:
{
"fieldMapping": {
"id": "ID",
"title": "Title"
},
"placeholderMapping": {
"id": "TASK_ID", // Use {{TASK_ID}} instead of {{ITEM_ID}}
"title": "TASK_NAME" // Use {{TASK_NAME}} instead of {{ITEM_TITLE}}
}
}
Changes:
- generator.js: Check for cfg.placeholderMapping before using default
- Added 2 new tests for custom mapping behavior
- Updated TEMPLATE_V2_GUIDE.md with documentation and examples
All 58 tests passing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -113,6 +113,32 @@ The following placeholders become available:
|
|||||||
|
|
||||||
**Note:** The `due` field is used for positioning and is not available as a placeholder (use `{{MONTH_LABEL}}` for date display).
|
**Note:** The `due` field is used for positioning and is not available as a placeholder (use `{{MONTH_LABEL}}` for date display).
|
||||||
|
|
||||||
|
**Custom Placeholder Mapping:**
|
||||||
|
|
||||||
|
If your template uses non-standard placeholder names, you can override the default convention using `placeholderMapping` in project.json:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"fieldMapping": {
|
||||||
|
"id": "ID",
|
||||||
|
"title": "Title",
|
||||||
|
"assignee": "Assignee"
|
||||||
|
},
|
||||||
|
"placeholderMapping": {
|
||||||
|
"id": "TASK_ID", // Use {{TASK_ID}} instead of {{ITEM_ID}}
|
||||||
|
"title": "TASK_NAME", // Use {{TASK_NAME}} instead of {{ITEM_TITLE}}
|
||||||
|
"assignee": "ASSIGNEE" // Use {{ASSIGNEE}} instead of {{ITEM_ASSIGNEE}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This is useful when:
|
||||||
|
- Working with existing templates that use different naming conventions
|
||||||
|
- Migrating from other timeline tools
|
||||||
|
- Maintaining compatibility with legacy templates
|
||||||
|
|
||||||
|
Without `placeholderMapping`, the default `ITEM_{PROPERTY}` convention is used.
|
||||||
|
|
||||||
### Global Placeholders
|
### Global Placeholders
|
||||||
|
|
||||||
These appear in the main template body (not in template elements):
|
These appear in the main template body (not in template elements):
|
||||||
|
|||||||
@@ -173,9 +173,11 @@ window.timelineGenerator = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Dynamically add data placeholders from all item properties
|
// Dynamically add data placeholders from all item properties
|
||||||
|
const placeholderMapping = cfg.placeholderMapping || {};
|
||||||
for (const [key, value] of Object.entries(it)) {
|
for (const [key, value] of Object.entries(it)) {
|
||||||
if (key !== 'due') { // Skip due date as it's used for positioning
|
if (key !== 'due') { // Skip due date as it's used for positioning
|
||||||
const placeholderName = `ITEM_${key.toUpperCase()}`;
|
// Use custom placeholder name if defined, otherwise use convention
|
||||||
|
const placeholderName = placeholderMapping[key] || `ITEM_${key.toUpperCase()}`;
|
||||||
itemValues[placeholderName] = this.escapeXml(value || "");
|
itemValues[placeholderName] = this.escapeXml(value || "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -278,5 +278,52 @@ describe('Timeline Generator', () => {
|
|||||||
expect(viewBoxMatch[1]).toBe(widthMatch[1])
|
expect(viewBoxMatch[1]).toBe(widthMatch[1])
|
||||||
expect(viewBoxMatch[2]).toBe(heightMatch[1])
|
expect(viewBoxMatch[2]).toBe(heightMatch[1])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should support custom placeholder mapping', () => {
|
||||||
|
const customConfig = {
|
||||||
|
...config,
|
||||||
|
placeholderMapping: {
|
||||||
|
id: 'TASK_ID', // Map item.id to {{TASK_ID}} instead of {{ITEM_ID}}
|
||||||
|
title: 'TASK_NAME' // Map item.title to {{TASK_NAME}} instead of {{ITEM_TITLE}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create template with custom placeholders
|
||||||
|
const customTemplate = `<svg xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<g id="month-template" style="display:none">
|
||||||
|
<text x="{{MONTH_TEXT_X}}" y="{{MONTH_LABEL_Y}}">{{MONTH_LABEL}}</text>
|
||||||
|
</g>
|
||||||
|
<g id="lane-template" style="display:none">
|
||||||
|
<text>{{LANE_NAME}}</text>
|
||||||
|
</g>
|
||||||
|
<g id="item-template" style="display:none">
|
||||||
|
<text x="{{TEXT_X}}" y="{{TEXT_Y}}">{{TASK_ID}}: {{TASK_NAME}}</text>
|
||||||
|
</g>
|
||||||
|
</defs>
|
||||||
|
{{MONTHS}}
|
||||||
|
{{LANES}}
|
||||||
|
</svg>`
|
||||||
|
|
||||||
|
const result = timelineGenerator.generate(items, customConfig, customTemplate)
|
||||||
|
|
||||||
|
// Should use custom placeholder names
|
||||||
|
expect(result).toContain('T-1: First Task') // TASK_ID: TASK_NAME
|
||||||
|
expect(result).toContain('T-2: Second Task')
|
||||||
|
|
||||||
|
// Should not contain default placeholder syntax
|
||||||
|
expect(result).not.toContain('{{ITEM_ID}}')
|
||||||
|
expect(result).not.toContain('{{ITEM_TITLE}}')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should use default convention when placeholderMapping is not provided', () => {
|
||||||
|
const template = createSampleTemplate()
|
||||||
|
const result = timelineGenerator.generate(items, config, template)
|
||||||
|
|
||||||
|
// Should use default ITEM_* convention
|
||||||
|
expect(result).toContain('T-1 First Task') // Default template has space, not colon
|
||||||
|
expect(result).not.toContain('{{ITEM_ID}}')
|
||||||
|
expect(result).not.toContain('{{ITEM_TITLE}}')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user