Spaces:
Running
Running
fix: enhance chart extraction - multi-chart support, ES6 shorthand, and brace-balanced parsing
Browse files- Detect multi-chart widgets and route to Puppeteer renderer
- Replace fragile regex with balanced-brace matching for Chart config extraction
- Support Chart.js aliases (C, custom constructors) beyond 'Chart'
- Use balanced bracket matching for variable declarations instead of simple regex
- Filter preamble to only include referenced variables (avoid scope pollution)
- Detect and provide defaults for ES6 shorthand function parameters (e.g. type='pie')
- Add extensive debug logging for chart parsing failures
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
server.js
CHANGED
|
@@ -478,9 +478,21 @@ class WidgetRenderer {
|
|
| 478 |
const startTime = Date.now();
|
| 479 |
console.log(`[WIDGET] renderChart START: title=${title}`);
|
| 480 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 481 |
const config = this.extractChartConfig(widgetHtml);
|
| 482 |
if (!config) {
|
| 483 |
console.log(`[WIDGET] renderChart FAIL: no chart config found, falling back`);
|
|
|
|
| 484 |
return null;
|
| 485 |
}
|
| 486 |
|
|
@@ -502,6 +514,7 @@ class WidgetRenderer {
|
|
| 502 |
}
|
| 503 |
|
| 504 |
extractChartConfig(widgetHtml) {
|
|
|
|
| 505 |
const containerMatch = widgetHtml.match(/<div[^>]*style=["']([^"']*?)["']/);
|
| 506 |
let width = 650, height = 300;
|
| 507 |
|
|
@@ -527,22 +540,61 @@ class WidgetRenderer {
|
|
| 527 |
|
| 528 |
console.log(`[WIDGET] extractChartConfig: width=${width} (${widthSource}), height=${height} (${heightSource})`);
|
| 529 |
|
| 530 |
-
|
| 531 |
if (!chartFnMatch) {
|
| 532 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 533 |
return null;
|
| 534 |
}
|
| 535 |
|
| 536 |
-
let configStr =
|
|
|
|
|
|
|
| 537 |
configStr = configStr.replace(/,\s*\}\s*\)\s*;?\s*$/, '}');
|
|
|
|
| 538 |
|
| 539 |
configStr = configStr.replace(/\bbackgroundImage\s*:\s*['"`][^'`]*['"`]/g, '');
|
| 540 |
configStr = configStr.replace(/,\s*}\)\s*;?$/g, '}');
|
| 541 |
|
| 542 |
-
//
|
| 543 |
// Chart.js configs often reference variables defined outside the config object
|
| 544 |
-
// e.g. const labels = [...]; new Chart(el, { data: { labels, datasets: [{ data }] } });
|
| 545 |
-
const scriptMatch = widgetHtml.match(/<script[^>]*>([\s\S]*?)<\/script>/
|
| 546 |
const allScriptBlocks = [];
|
| 547 |
if (scriptMatch) {
|
| 548 |
for (const s of scriptMatch) {
|
|
@@ -552,26 +604,116 @@ class WidgetRenderer {
|
|
| 552 |
}
|
| 553 |
const fullScript = allScriptBlocks.join('\n');
|
| 554 |
|
| 555 |
-
// Find all const/let/var declarations
|
| 556 |
const varDeclarations = [];
|
| 557 |
-
const
|
| 558 |
-
let
|
| 559 |
-
while ((
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 563 |
|
| 564 |
-
|
| 565 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 566 |
|
| 567 |
-
// Find which variables are referenced in the config string
|
| 568 |
-
const referencedVars = varDeclarations.filter(v => configStr.includes(v.name));
|
| 569 |
if (referencedVars.length > 0) {
|
| 570 |
console.log(`[WIDGET] extractChartConfig DEBUG: Config references ${referencedVars.length} external variables: ${referencedVars.map(v => v.name).join(', ')}`);
|
| 571 |
}
|
| 572 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 573 |
try {
|
| 574 |
-
const chartConfig = new Function(
|
| 575 |
console.log(`[WIDGET] extractChartConfig DEBUG: Parsed OK — type=${chartConfig.type}, labels=${chartConfig.data?.labels?.length}, datasets=${chartConfig.data?.datasets?.length}`);
|
| 576 |
return { width, height, chartConfig };
|
| 577 |
} catch (parseError) {
|
|
|
|
| 478 |
const startTime = Date.now();
|
| 479 |
console.log(`[WIDGET] renderChart START: title=${title}`);
|
| 480 |
|
| 481 |
+
const canvasCount = (widgetHtml.match(/<canvas/g) || []).length;
|
| 482 |
+
const newChartCount = (widgetHtml.match(/new\s+Chart\s*\(/gi) || []).length +
|
| 483 |
+
(widgetHtml.match(/new\s+[A-Z]\s*\(/g) || []).length +
|
| 484 |
+
(widgetHtml.match(/new\s+C\s*\(/g) || []).length;
|
| 485 |
+
const isMultiChart = canvasCount > 1 || newChartCount > 1;
|
| 486 |
+
|
| 487 |
+
if (isMultiChart) {
|
| 488 |
+
console.log(`[WIDGET] renderChart: multi-chart detected (canvas=${canvasCount}, newChart=${newChartCount}), using Puppeteer`);
|
| 489 |
+
return this.renderWidgetPuppeteer(widgetHtml, 'chart', title, 5000);
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
const config = this.extractChartConfig(widgetHtml);
|
| 493 |
if (!config) {
|
| 494 |
console.log(`[WIDGET] renderChart FAIL: no chart config found, falling back`);
|
| 495 |
+
console.log(`[WIDGET] renderChart DEBUG: widgetHtml length=${widgetHtml.length}, first 500 chars:\n${widgetHtml.substring(0, 500)}`);
|
| 496 |
return null;
|
| 497 |
}
|
| 498 |
|
|
|
|
| 514 |
}
|
| 515 |
|
| 516 |
extractChartConfig(widgetHtml) {
|
| 517 |
+
console.log(`[WIDGET] extractChartConfig: widgetHtml length=${widgetHtml.length}`);
|
| 518 |
const containerMatch = widgetHtml.match(/<div[^>]*style=["']([^"']*?)["']/);
|
| 519 |
let width = 650, height = 300;
|
| 520 |
|
|
|
|
| 540 |
|
| 541 |
console.log(`[WIDGET] extractChartConfig: width=${width} (${widthSource}), height=${height} (${heightSource})`);
|
| 542 |
|
| 543 |
+
let chartFnMatch = widgetHtml.match(/new\s+Chart\s*\([^,]+,\s*(\{)/);
|
| 544 |
if (!chartFnMatch) {
|
| 545 |
+
chartFnMatch = widgetHtml.match(/new\s+([A-Z])\s*\([^,]+,\s*(\{)/);
|
| 546 |
+
}
|
| 547 |
+
if (!chartFnMatch) {
|
| 548 |
+
chartFnMatch = widgetHtml.match(/new\s+\w+\s*\([^,]+,\s*(\{)/);
|
| 549 |
+
}
|
| 550 |
+
if (!chartFnMatch) {
|
| 551 |
+
console.log(`[WIDGET] extractChartConfig: FAIL — new Chart() or alias pattern not found`);
|
| 552 |
+
console.log(`[WIDGET] extractChartConfig: DEBUG widgetHtml (first 800 chars):\n${widgetHtml.substring(0, 800)}`);
|
| 553 |
+
return null;
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
+
// Find the start of the config object
|
| 557 |
+
const configStart = chartFnMatch.index + chartFnMatch[0].length - 1; // position of the opening '{'
|
| 558 |
+
let depth = 0;
|
| 559 |
+
let inString = null;
|
| 560 |
+
let configEnd = -1;
|
| 561 |
+
|
| 562 |
+
for (let i = configStart; i < widgetHtml.length; i++) {
|
| 563 |
+
const ch = widgetHtml[i];
|
| 564 |
+
if (inString) {
|
| 565 |
+
if (ch === inString) {
|
| 566 |
+
// Check for escape character
|
| 567 |
+
if (i > configStart && widgetHtml[i - 1] === '\\') continue;
|
| 568 |
+
inString = null;
|
| 569 |
+
}
|
| 570 |
+
continue;
|
| 571 |
+
}
|
| 572 |
+
if (ch === "'" || ch === '"' || ch === '`') { inString = ch; continue; }
|
| 573 |
+
if (ch === '{') depth++;
|
| 574 |
+
else if (ch === '}') {
|
| 575 |
+
depth--;
|
| 576 |
+
if (depth === 0) { configEnd = i; break; }
|
| 577 |
+
}
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
+
if (configEnd === -1) {
|
| 581 |
+
console.log(`[WIDGET] extractChartConfig: FAIL — could not find matching closing brace`);
|
| 582 |
return null;
|
| 583 |
}
|
| 584 |
|
| 585 |
+
let configStr = widgetHtml.substring(configStart, configEnd + 1);
|
| 586 |
+
|
| 587 |
+
// Clean up trailing artifacts (e.g., trailing commas, extra closing parens)
|
| 588 |
configStr = configStr.replace(/,\s*\}\s*\)\s*;?\s*$/, '}');
|
| 589 |
+
configStr = configStr.replace(/,\s*}\s*$/, '}');
|
| 590 |
|
| 591 |
configStr = configStr.replace(/\bbackgroundImage\s*:\s*['"`][^'`]*['"`]/g, '');
|
| 592 |
configStr = configStr.replace(/,\s*}\)\s*;?$/g, '}');
|
| 593 |
|
| 594 |
+
// Extract variable declarations from the script to resolve external references
|
| 595 |
// Chart.js configs often reference variables defined outside the config object
|
| 596 |
+
// e.g. const labels = [...]; const type = 'doughnut'; new Chart(el, { type, data: { labels, datasets: [{ data, ...(type === 'doughnut' ? { cutout: '58%' } : {}) }] } });
|
| 597 |
+
const scriptMatch = widgetHtml.match(/<script[^>]*>([\s\S]*?)<\/script>/gi);
|
| 598 |
const allScriptBlocks = [];
|
| 599 |
if (scriptMatch) {
|
| 600 |
for (const s of scriptMatch) {
|
|
|
|
| 604 |
}
|
| 605 |
const fullScript = allScriptBlocks.join('\n');
|
| 606 |
|
| 607 |
+
// Find all const/let/var declarations using balanced bracket matching for values
|
| 608 |
const varDeclarations = [];
|
| 609 |
+
const varDeclStart = /\b(?:const|let|var)\s+(\w+)\s*=\s*/g;
|
| 610 |
+
let startMatch;
|
| 611 |
+
while ((startMatch = varDeclStart.exec(fullScript)) !== null) {
|
| 612 |
+
const varName = startMatch[1];
|
| 613 |
+
const valueStart = startMatch.index + startMatch[0].length;
|
| 614 |
+
|
| 615 |
+
// Determine the value by matching the first character
|
| 616 |
+
const firstChar = fullScript[valueStart];
|
| 617 |
+
let valueEnd = -1;
|
| 618 |
+
|
| 619 |
+
if (firstChar === '[' || firstChar === '{') {
|
| 620 |
+
// Balanced bracket matching for arrays/objects
|
| 621 |
+
const openChar = firstChar;
|
| 622 |
+
const closeChar = openChar === '[' ? ']' : '}';
|
| 623 |
+
let depth = 0;
|
| 624 |
+
let inStr = null;
|
| 625 |
+
for (let i = valueStart; i < fullScript.length; i++) {
|
| 626 |
+
const ch = fullScript[i];
|
| 627 |
+
if (inStr) {
|
| 628 |
+
if (ch === inStr && (i === valueStart || fullScript[i - 1] !== '\\')) inStr = null;
|
| 629 |
+
continue;
|
| 630 |
+
}
|
| 631 |
+
if (ch === "'" || ch === '"' || ch === '`') { inStr = ch; continue; }
|
| 632 |
+
if (ch === openChar) depth++;
|
| 633 |
+
else if (ch === closeChar) {
|
| 634 |
+
depth--;
|
| 635 |
+
if (depth === 0) { valueEnd = i + 1; break; }
|
| 636 |
+
}
|
| 637 |
+
}
|
| 638 |
+
} else if (firstChar === "'" || firstChar === '"' || firstChar === '`') {
|
| 639 |
+
// String literal
|
| 640 |
+
const quoteChar = firstChar;
|
| 641 |
+
for (let i = valueStart + 1; i < fullScript.length; i++) {
|
| 642 |
+
if (fullScript[i] === quoteChar && fullScript[i - 1] !== '\\') {
|
| 643 |
+
valueEnd = i + 1;
|
| 644 |
+
break;
|
| 645 |
+
}
|
| 646 |
+
}
|
| 647 |
+
} else if (/[\d]/.test(firstChar)) {
|
| 648 |
+
// Numeric literal
|
| 649 |
+
const numMatch = fullScript.substring(valueStart).match(/^[\d.]+/);
|
| 650 |
+
if (numMatch) valueEnd = valueStart + numMatch[0].length;
|
| 651 |
+
}
|
| 652 |
|
| 653 |
+
if (valueEnd > valueStart) {
|
| 654 |
+
varDeclarations.push({ name: varName, value: fullScript.substring(valueStart, valueEnd) });
|
| 655 |
+
}
|
| 656 |
+
}
|
| 657 |
+
console.log(`[WIDGET] extractChartConfig DEBUG: Found ${varDeclarations.length} variable declarations: ${varDeclarations.map(v => `${v.name}=${v.value.substring(0, 30)}${v.value.length > 30 ? '...' : ''}`).join(', ')}`);
|
| 658 |
+
|
| 659 |
+
// Build a preamble with ONLY the variables that are actually referenced in the config string
|
| 660 |
+
// This avoids polluting the scope with unrelated variables
|
| 661 |
+
const referencedVars = varDeclarations.filter(v => {
|
| 662 |
+
// Check if the variable name appears as a standalone identifier in the config
|
| 663 |
+
const refRegex = new RegExp(`(?<![\\w])${v.name}(?![\\w])`);
|
| 664 |
+
return refRegex.test(configStr);
|
| 665 |
+
});
|
| 666 |
|
|
|
|
|
|
|
| 667 |
if (referencedVars.length > 0) {
|
| 668 |
console.log(`[WIDGET] extractChartConfig DEBUG: Config references ${referencedVars.length} external variables: ${referencedVars.map(v => v.name).join(', ')}`);
|
| 669 |
}
|
| 670 |
|
| 671 |
+
// Detect ES6 shorthand properties in the config that reference undeclared variables
|
| 672 |
+
// e.g. { type, data, labels } where 'type' is a function parameter, not a declared variable
|
| 673 |
+
// The pattern `{ identifier,` or `{ identifier }` means shorthand for `{ identifier: identifier }`
|
| 674 |
+
const undeclaredVars = new Set();
|
| 675 |
+
// Look for shorthand patterns: bare identifier used as property key (followed by comma or })
|
| 676 |
+
// Also check spread conditionals: ...(type === 'doughnut' ? { ... } : {})
|
| 677 |
+
const spreadRegex = /\.\.\.\s*\(\s*(\w+)\s*===/g;
|
| 678 |
+
let cMatch;
|
| 679 |
+
while ((cMatch = spreadRegex.exec(configStr)) !== null) {
|
| 680 |
+
const varName = cMatch[1];
|
| 681 |
+
if (!varDeclarations.some(d => d.name === varName)) {
|
| 682 |
+
undeclaredVars.add(varName);
|
| 683 |
+
}
|
| 684 |
+
}
|
| 685 |
+
// Also check direct shorthand: `{ type,` or `{ type }` at top-level of config
|
| 686 |
+
// Note: this regex can match inside string literals (e.g. rgba values), so we skip numeric captures
|
| 687 |
+
const shorthandRegex = /(?:^|[\{,])\s*(\w+)\s*(?=[,}])/gm;
|
| 688 |
+
let shMatch;
|
| 689 |
+
while ((shMatch = shorthandRegex.exec(configStr)) !== null) {
|
| 690 |
+
const varName = shMatch[1];
|
| 691 |
+
// Skip purely numeric matches (these are from inside string literals like rgba values)
|
| 692 |
+
if (/^\d+$/.test(varName)) continue;
|
| 693 |
+
if (varName.length > 1 && varName.length <= 20 && !varDeclarations.some(d => d.name === varName)) {
|
| 694 |
+
// Verify this is NOT a property key (which would be followed by ':')
|
| 695 |
+
const pos = shMatch.index + shMatch[0].length;
|
| 696 |
+
if (pos >= configStr.length || configStr[pos] !== ':') {
|
| 697 |
+
undeclaredVars.add(varName);
|
| 698 |
+
}
|
| 699 |
+
}
|
| 700 |
+
}
|
| 701 |
+
|
| 702 |
+
if (undeclaredVars.size > 0) {
|
| 703 |
+
console.log(`[WIDGET] extractChartConfig DEBUG: Found ${undeclaredVars.size} undeclared shorthand vars (function params): ${[...undeclaredVars].join(', ')} — providing default values`);
|
| 704 |
+
}
|
| 705 |
+
|
| 706 |
+
const preamble = referencedVars.map(v => `var ${v.name} = ${v.value};`).join('\n');
|
| 707 |
+
// Add fallback values for undeclared vars (e.g. type='pie' for pie/doughnut charts)
|
| 708 |
+
const fallbackPreamble = [...undeclaredVars].map(v => {
|
| 709 |
+
if (v === 'type') return `var ${v} = 'pie';`;
|
| 710 |
+
return `var ${v} = undefined;`;
|
| 711 |
+
}).join('\n');
|
| 712 |
+
|
| 713 |
+
const fullPreamble = preamble + (fallbackPreamble ? '\n' + fallbackPreamble : '');
|
| 714 |
+
|
| 715 |
try {
|
| 716 |
+
const chartConfig = new Function(fullPreamble + '\nreturn ' + configStr)();
|
| 717 |
console.log(`[WIDGET] extractChartConfig DEBUG: Parsed OK — type=${chartConfig.type}, labels=${chartConfig.data?.labels?.length}, datasets=${chartConfig.data?.datasets?.length}`);
|
| 718 |
return { width, height, chartConfig };
|
| 719 |
} catch (parseError) {
|