diff --git a/example/template-v2.svg b/example/template-v2.svg
new file mode 100644
index 0000000..cc4a6c2
--- /dev/null
+++ b/example/template-v2.svg
@@ -0,0 +1,81 @@
+
diff --git a/generator.js b/generator.js
index 08dd92e..b730b6b 100644
--- a/generator.js
+++ b/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, "'");
+ },
+
+ // 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(`]*>([\\s\\S]*?)`, '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(
+ /