XWX-AI Claude Opus 4.6 commited on
Commit
d842f0a
·
1 Parent(s): 44a8902

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>

Files changed (1) hide show
  1. server.js +160 -18
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
- const chartFnMatch = widgetHtml.match(/new\s+Chart\s*\([^,]+,\s*({[\s\S]*})\s*\)\s*;?/);
531
  if (!chartFnMatch) {
532
- console.log(`[WIDGET] extractChartConfig: FAIL — new Chart() pattern not found`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
533
  return null;
534
  }
535
 
536
- let configStr = chartFnMatch[1];
 
 
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
- // DEBUG: Extract variable declarations from the script to resolve external references
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>/g);
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 in the script
556
  const varDeclarations = [];
557
- const varDeclRegex = /\b(?:const|let|var)\s+(\w+)\s*=\s*([\s\S]*?);/g;
558
- let varMatch;
559
- while ((varMatch = varDeclRegex.exec(fullScript)) !== null) {
560
- varDeclarations.push({ name: varMatch[1], value: varMatch[2] });
561
- }
562
- console.log(`[WIDGET] extractChartConfig DEBUG: Found ${varDeclarations.length} variable declarations: ${varDeclarations.map(v => v.name).join(', ')}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
563
 
564
- // Build a preamble with all variable declarations to resolve external references
565
- const preamble = varDeclarations.map(v => `var ${v.name} = ${v.value};`).join('\n');
 
 
 
 
 
 
 
 
 
 
 
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(preamble + '\nreturn ' + configStr)();
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) {