From b5e4550efdb0ffa92729d08f41a9994e660a4746 Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 28 Nov 2025 03:24:33 +0100 Subject: [PATCH] feat: add custom placeholder mapping for non-standard templates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- TEMPLATE_V2_GUIDE.md | 26 +++++++++++++++++++++++ generator.js | 4 +++- test/generator.test.js | 47 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) diff --git a/TEMPLATE_V2_GUIDE.md b/TEMPLATE_V2_GUIDE.md index f2db0cc..dc01cd8 100644 --- a/TEMPLATE_V2_GUIDE.md +++ b/TEMPLATE_V2_GUIDE.md @@ -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). +**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 These appear in the main template body (not in template elements): diff --git a/generator.js b/generator.js index d097b11..7fdacb2 100644 --- a/generator.js +++ b/generator.js @@ -173,9 +173,11 @@ window.timelineGenerator = { }; // Dynamically add data placeholders from all item properties + const placeholderMapping = cfg.placeholderMapping || {}; for (const [key, value] of Object.entries(it)) { 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 || ""); } } diff --git a/test/generator.test.js b/test/generator.test.js index 9632c56..8156e77 100644 --- a/test/generator.test.js +++ b/test/generator.test.js @@ -278,5 +278,52 @@ describe('Timeline Generator', () => { expect(viewBoxMatch[1]).toBe(widthMatch[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 = ` + + + + + + {{MONTHS}} + {{LANES}} + ` + + 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}}') + }) }) })