@tpitre/story-ui 4.5.2 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -334,8 +334,136 @@ function fixUnexpectedTokens(code) {
334
334
  for (const fix of fixes) {
335
335
  fixedCode = fixedCode.replace(fix.pattern, fix.replacement);
336
336
  }
337
+ // CRITICAL: Fix unescaped apostrophes in title strings
338
+ // LLMs often generate titles like: title: 'Women's Athletic' instead of 'Women\'s Athletic'
339
+ fixedCode = fixUnescapedApostrophesInTitles(fixedCode);
337
340
  return fixedCode;
338
341
  }
342
+ /**
343
+ * Fixes unescaped apostrophes in title strings
344
+ * LLMs often generate: title: 'Women's Athletic Dashboard'
345
+ * Which should be: title: 'Women\'s Athletic Dashboard'
346
+ *
347
+ * Also handles cases where some apostrophes are escaped and others aren't:
348
+ * title: 'Women\'s Athletic Dashboard's Athletic Dashboard'
349
+ */
350
+ function fixUnescapedApostrophesInTitles(code) {
351
+ const lines = code.split('\n');
352
+ for (let i = 0; i < lines.length; i++) {
353
+ const line = lines[i];
354
+ // Skip if no title property
355
+ if (!line.includes('title:'))
356
+ continue;
357
+ // Find title: followed by a quote character
358
+ const titleStartMatch = line.match(/title:\s*(['"])/);
359
+ if (!titleStartMatch)
360
+ continue;
361
+ const quoteChar = titleStartMatch[1];
362
+ const titleKeywordIndex = line.indexOf('title:');
363
+ const openQuoteIndex = line.indexOf(quoteChar, titleKeywordIndex);
364
+ // Find the closing quote by looking for quote followed by comma or brace at end of line
365
+ // The pattern is: quote + optional whitespace + (comma or brace) + optional whitespace + end
366
+ const endPattern = new RegExp(`${quoteChar === "'" ? "'" : '"'}\\s*[,}]\\s*$`);
367
+ const endMatch = line.match(endPattern);
368
+ if (!endMatch)
369
+ continue;
370
+ const closeQuoteIndex = line.lastIndexOf(quoteChar);
371
+ if (closeQuoteIndex <= openQuoteIndex)
372
+ continue;
373
+ // Extract content between opening and closing quotes
374
+ const content = line.substring(openQuoteIndex + 1, closeQuoteIndex);
375
+ // Process the content character by character to escape unescaped quotes
376
+ let fixedContent = '';
377
+ for (let j = 0; j < content.length; j++) {
378
+ const char = content[j];
379
+ const prevChar = j > 0 ? content[j - 1] : '';
380
+ if (char === quoteChar && prevChar !== '\\') {
381
+ // This is an unescaped quote inside the string - escape it
382
+ fixedContent += '\\' + char;
383
+ }
384
+ else {
385
+ fixedContent += char;
386
+ }
387
+ }
388
+ // Only modify if we actually made changes
389
+ if (fixedContent !== content) {
390
+ const beforeQuote = line.substring(0, openQuoteIndex + 1);
391
+ const afterQuote = line.substring(closeQuoteIndex);
392
+ lines[i] = beforeQuote + fixedContent + afterQuote;
393
+ }
394
+ }
395
+ let fixedCode = lines.join('\n');
396
+ // Fix duplicated title segments (LLM sometimes repeats parts of the title)
397
+ // e.g., "Women's Athletic Dashboard's Athletic Dashboard" -> "Women's Athletic Dashboard"
398
+ fixedCode = fixDuplicatedTitleSegments(fixedCode);
399
+ return fixedCode;
400
+ }
401
+ /**
402
+ * Fixes duplicated segments in titles
403
+ * LLMs sometimes generate: "Women's Athletic Dashboard's Athletic Dashboard"
404
+ * Which should be: "Women's Athletic Dashboard"
405
+ */
406
+ function fixDuplicatedTitleSegments(code) {
407
+ const lines = code.split('\n');
408
+ for (let i = 0; i < lines.length; i++) {
409
+ const line = lines[i];
410
+ if (!line.includes('title:'))
411
+ continue;
412
+ // Find title: followed by a quote
413
+ const titleMatch = line.match(/title:\s*(['"])/);
414
+ if (!titleMatch)
415
+ continue;
416
+ const quoteChar = titleMatch[1];
417
+ const openQuoteIndex = line.indexOf(quoteChar, line.indexOf('title:'));
418
+ const closeQuoteIndex = line.lastIndexOf(quoteChar);
419
+ if (closeQuoteIndex <= openQuoteIndex)
420
+ continue;
421
+ // Extract title content (handling escaped quotes)
422
+ const content = line.substring(openQuoteIndex + 1, closeQuoteIndex);
423
+ // Normalize escaped quotes for word processing
424
+ const normalizedContent = content.replace(/\\'/g, "'");
425
+ // Split into words
426
+ const words = normalizedContent.split(/\s+/);
427
+ // Look for repeated word sequences
428
+ let modified = false;
429
+ let newWords = [...words];
430
+ for (let windowSize = 2; windowSize <= Math.floor(words.length / 2); windowSize++) {
431
+ for (let j = 0; j <= newWords.length - windowSize * 2; j++) {
432
+ const segment1 = newWords.slice(j, j + windowSize).join(' ');
433
+ const segment2 = newWords.slice(j + windowSize, j + windowSize * 2).join(' ');
434
+ if (segment1 === segment2 && segment1.length > 3) {
435
+ newWords = [...newWords.slice(0, j + windowSize), ...newWords.slice(j + windowSize * 2)];
436
+ modified = true;
437
+ break;
438
+ }
439
+ }
440
+ if (modified)
441
+ break;
442
+ }
443
+ // Also look for possessive duplications: "Dashboard's Athletic" ... "Dashboard"
444
+ // Pattern: something ending with 's + words + that same word
445
+ if (!modified) {
446
+ const possessivePattern = /^(.+?)(['\\]?s\s+)(.+?)\s+\1$/i;
447
+ const match = newWords.join(' ').match(possessivePattern);
448
+ if (match) {
449
+ newWords = (match[1] + match[2] + match[3]).split(/\s+/);
450
+ modified = true;
451
+ }
452
+ }
453
+ if (modified) {
454
+ // Re-escape apostrophes for the output
455
+ let newContent = newWords.join(' ');
456
+ if (quoteChar === "'") {
457
+ // Re-escape internal single quotes
458
+ newContent = newContent.replace(/(?<!\\)'/g, "\\'");
459
+ }
460
+ const beforeQuote = line.substring(0, openQuoteIndex + 1);
461
+ const afterQuote = line.substring(closeQuoteIndex);
462
+ lines[i] = beforeQuote + newContent + afterQuote;
463
+ }
464
+ }
465
+ return lines.join('\n');
466
+ }
339
467
  /**
340
468
  * Fixes unterminated string literals
341
469
  */