@daviddh/llm-markdown-whatsapp 0.0.1

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.
Files changed (138) hide show
  1. package/.prettierrc +17 -0
  2. package/CLAUDE.md +155 -0
  3. package/README.md +304 -0
  4. package/eslint.config.mjs +28 -0
  5. package/jest.config.js +40 -0
  6. package/package.json +61 -0
  7. package/packages/core/dist/__tests__/splitChatText.basic.test.d.ts +2 -0
  8. package/packages/core/dist/__tests__/splitChatText.basic.test.d.ts.map +1 -0
  9. package/packages/core/dist/__tests__/splitChatText.basic.test.js +100 -0
  10. package/packages/core/dist/__tests__/splitChatText.coverageLists.test.d.ts +2 -0
  11. package/packages/core/dist/__tests__/splitChatText.coverageLists.test.d.ts.map +1 -0
  12. package/packages/core/dist/__tests__/splitChatText.coverageLists.test.js +88 -0
  13. package/packages/core/dist/__tests__/splitChatText.coverageProcessors.test.d.ts +2 -0
  14. package/packages/core/dist/__tests__/splitChatText.coverageProcessors.test.d.ts.map +1 -0
  15. package/packages/core/dist/__tests__/splitChatText.coverageProcessors.test.js +108 -0
  16. package/packages/core/dist/__tests__/splitChatText.coverageQuestions.test.d.ts +2 -0
  17. package/packages/core/dist/__tests__/splitChatText.coverageQuestions.test.d.ts.map +1 -0
  18. package/packages/core/dist/__tests__/splitChatText.coverageQuestions.test.js +74 -0
  19. package/packages/core/dist/__tests__/splitChatText.dataProtection.test.d.ts +2 -0
  20. package/packages/core/dist/__tests__/splitChatText.dataProtection.test.d.ts.map +1 -0
  21. package/packages/core/dist/__tests__/splitChatText.dataProtection.test.js +80 -0
  22. package/packages/core/dist/__tests__/splitChatText.dataTests1.test.d.ts +2 -0
  23. package/packages/core/dist/__tests__/splitChatText.dataTests1.test.d.ts.map +1 -0
  24. package/packages/core/dist/__tests__/splitChatText.dataTests1.test.js +124 -0
  25. package/packages/core/dist/__tests__/splitChatText.dataTests2.test.d.ts +2 -0
  26. package/packages/core/dist/__tests__/splitChatText.dataTests2.test.d.ts.map +1 -0
  27. package/packages/core/dist/__tests__/splitChatText.dataTests2.test.js +122 -0
  28. package/packages/core/dist/__tests__/splitChatText.edgeCases.test.d.ts +2 -0
  29. package/packages/core/dist/__tests__/splitChatText.edgeCases.test.d.ts.map +1 -0
  30. package/packages/core/dist/__tests__/splitChatText.edgeCases.test.js +132 -0
  31. package/packages/core/dist/__tests__/splitChatText.helpers.d.ts +2 -0
  32. package/packages/core/dist/__tests__/splitChatText.helpers.d.ts.map +1 -0
  33. package/packages/core/dist/__tests__/splitChatText.helpers.js +5 -0
  34. package/packages/core/dist/__tests__/splitChatText.punctuation.test.d.ts +2 -0
  35. package/packages/core/dist/__tests__/splitChatText.punctuation.test.d.ts.map +1 -0
  36. package/packages/core/dist/__tests__/splitChatText.punctuation.test.js +98 -0
  37. package/packages/core/dist/__tests__/splitChatText.realWorld.test.d.ts +2 -0
  38. package/packages/core/dist/__tests__/splitChatText.realWorld.test.d.ts.map +1 -0
  39. package/packages/core/dist/__tests__/splitChatText.realWorld.test.js +104 -0
  40. package/packages/core/dist/__tests__/splitChatText.urlProtection.test.d.ts +2 -0
  41. package/packages/core/dist/__tests__/splitChatText.urlProtection.test.d.ts.map +1 -0
  42. package/packages/core/dist/__tests__/splitChatText.urlProtection.test.js +82 -0
  43. package/packages/core/dist/__tests__/strs.splitChatText.test.d.ts +2 -0
  44. package/packages/core/dist/__tests__/strs.splitChatText.test.d.ts.map +1 -0
  45. package/packages/core/dist/__tests__/strs.splitChatText.test.js +992 -0
  46. package/packages/core/dist/chatSplit/breakProcessor.d.ts +4 -0
  47. package/packages/core/dist/chatSplit/breakProcessor.d.ts.map +1 -0
  48. package/packages/core/dist/chatSplit/breakProcessor.js +67 -0
  49. package/packages/core/dist/chatSplit/constants.d.ts +35 -0
  50. package/packages/core/dist/chatSplit/constants.d.ts.map +1 -0
  51. package/packages/core/dist/chatSplit/constants.js +34 -0
  52. package/packages/core/dist/chatSplit/index.d.ts +2 -0
  53. package/packages/core/dist/chatSplit/index.d.ts.map +1 -0
  54. package/packages/core/dist/chatSplit/index.js +1 -0
  55. package/packages/core/dist/chatSplit/listNormalization.d.ts +13 -0
  56. package/packages/core/dist/chatSplit/listNormalization.d.ts.map +1 -0
  57. package/packages/core/dist/chatSplit/listNormalization.js +140 -0
  58. package/packages/core/dist/chatSplit/listProcessor.d.ts +6 -0
  59. package/packages/core/dist/chatSplit/listProcessor.d.ts.map +1 -0
  60. package/packages/core/dist/chatSplit/listProcessor.js +61 -0
  61. package/packages/core/dist/chatSplit/mergeProcessor.d.ts +3 -0
  62. package/packages/core/dist/chatSplit/mergeProcessor.d.ts.map +1 -0
  63. package/packages/core/dist/chatSplit/mergeProcessor.js +88 -0
  64. package/packages/core/dist/chatSplit/paragraphProcessor.d.ts +14 -0
  65. package/packages/core/dist/chatSplit/paragraphProcessor.d.ts.map +1 -0
  66. package/packages/core/dist/chatSplit/paragraphProcessor.js +66 -0
  67. package/packages/core/dist/chatSplit/periodProcessor.d.ts +4 -0
  68. package/packages/core/dist/chatSplit/periodProcessor.d.ts.map +1 -0
  69. package/packages/core/dist/chatSplit/periodProcessor.js +110 -0
  70. package/packages/core/dist/chatSplit/positionHelpers.d.ts +12 -0
  71. package/packages/core/dist/chatSplit/positionHelpers.d.ts.map +1 -0
  72. package/packages/core/dist/chatSplit/positionHelpers.js +57 -0
  73. package/packages/core/dist/chatSplit/productCardProcessor.d.ts +12 -0
  74. package/packages/core/dist/chatSplit/productCardProcessor.d.ts.map +1 -0
  75. package/packages/core/dist/chatSplit/productCardProcessor.js +138 -0
  76. package/packages/core/dist/chatSplit/punctuationNormalization.d.ts +5 -0
  77. package/packages/core/dist/chatSplit/punctuationNormalization.d.ts.map +1 -0
  78. package/packages/core/dist/chatSplit/punctuationNormalization.js +103 -0
  79. package/packages/core/dist/chatSplit/questionProcessor.d.ts +6 -0
  80. package/packages/core/dist/chatSplit/questionProcessor.d.ts.map +1 -0
  81. package/packages/core/dist/chatSplit/questionProcessor.js +212 -0
  82. package/packages/core/dist/chatSplit/sections.d.ts +23 -0
  83. package/packages/core/dist/chatSplit/sections.d.ts.map +1 -0
  84. package/packages/core/dist/chatSplit/sections.js +153 -0
  85. package/packages/core/dist/chatSplit/splitChatText.d.ts +6 -0
  86. package/packages/core/dist/chatSplit/splitChatText.d.ts.map +1 -0
  87. package/packages/core/dist/chatSplit/splitChatText.js +119 -0
  88. package/packages/core/dist/chatSplit/splitConstants.d.ts +3 -0
  89. package/packages/core/dist/chatSplit/splitConstants.d.ts.map +1 -0
  90. package/packages/core/dist/chatSplit/splitConstants.js +2 -0
  91. package/packages/core/dist/chatSplit/splitProcessors.d.ts +22 -0
  92. package/packages/core/dist/chatSplit/splitProcessors.d.ts.map +1 -0
  93. package/packages/core/dist/chatSplit/splitProcessors.js +105 -0
  94. package/packages/core/dist/chatSplit/textHelpers.d.ts +27 -0
  95. package/packages/core/dist/chatSplit/textHelpers.d.ts.map +1 -0
  96. package/packages/core/dist/chatSplit/textHelpers.js +77 -0
  97. package/packages/core/dist/chatSplit/urlNormalization.d.ts +7 -0
  98. package/packages/core/dist/chatSplit/urlNormalization.d.ts.map +1 -0
  99. package/packages/core/dist/chatSplit/urlNormalization.js +13 -0
  100. package/packages/core/dist/index.d.ts +2 -0
  101. package/packages/core/dist/index.d.ts.map +1 -0
  102. package/packages/core/dist/index.js +1 -0
  103. package/packages/core/jest.config.js +23 -0
  104. package/packages/core/package.json +38 -0
  105. package/packages/core/src/__tests__/splitChatText.basic.test.ts +123 -0
  106. package/packages/core/src/__tests__/splitChatText.coverageLists.test.ts +108 -0
  107. package/packages/core/src/__tests__/splitChatText.coverageProcessors.test.ts +172 -0
  108. package/packages/core/src/__tests__/splitChatText.coverageQuestions.test.ts +95 -0
  109. package/packages/core/src/__tests__/splitChatText.dataProtection.test.ts +96 -0
  110. package/packages/core/src/__tests__/splitChatText.dataTests1.test.ts +137 -0
  111. package/packages/core/src/__tests__/splitChatText.dataTests2.test.ts +134 -0
  112. package/packages/core/src/__tests__/splitChatText.edgeCases.test.ts +157 -0
  113. package/packages/core/src/__tests__/splitChatText.helpers.ts +6 -0
  114. package/packages/core/src/__tests__/splitChatText.punctuation.test.ts +113 -0
  115. package/packages/core/src/__tests__/splitChatText.realWorld.test.ts +118 -0
  116. package/packages/core/src/__tests__/splitChatText.urlProtection.test.ts +102 -0
  117. package/packages/core/src/chatSplit/breakProcessor.ts +103 -0
  118. package/packages/core/src/chatSplit/constants.ts +50 -0
  119. package/packages/core/src/chatSplit/index.ts +1 -0
  120. package/packages/core/src/chatSplit/listNormalization.ts +189 -0
  121. package/packages/core/src/chatSplit/listProcessor.ts +74 -0
  122. package/packages/core/src/chatSplit/mergeProcessor.ts +124 -0
  123. package/packages/core/src/chatSplit/paragraphProcessor.ts +86 -0
  124. package/packages/core/src/chatSplit/periodProcessor.ts +148 -0
  125. package/packages/core/src/chatSplit/positionHelpers.ts +66 -0
  126. package/packages/core/src/chatSplit/productCardProcessor.ts +184 -0
  127. package/packages/core/src/chatSplit/punctuationNormalization.ts +142 -0
  128. package/packages/core/src/chatSplit/questionProcessor.ts +298 -0
  129. package/packages/core/src/chatSplit/sections.ts +243 -0
  130. package/packages/core/src/chatSplit/splitChatText.ts +156 -0
  131. package/packages/core/src/chatSplit/splitConstants.ts +2 -0
  132. package/packages/core/src/chatSplit/splitProcessors.ts +153 -0
  133. package/packages/core/src/chatSplit/textHelpers.ts +86 -0
  134. package/packages/core/src/chatSplit/urlNormalization.ts +17 -0
  135. package/packages/core/src/index.ts +1 -0
  136. package/packages/core/tsconfig.build.json +4 -0
  137. package/packages/core/tsconfig.json +25 -0
  138. package/tsconfig.json +19 -0
@@ -0,0 +1,95 @@
1
+ import { describe, expect, test } from '@jest/globals';
2
+
3
+ import { splitChatText } from '../index.js';
4
+
5
+ const MIN_SPLIT_COUNT = 2;
6
+ const SINGLE_CHUNK = 1;
7
+
8
+ describe('Question processor - parenthetical clarification split', () => {
9
+ test('should split after parenthetical when text follows and gap is large', () => {
10
+ const input =
11
+ 'Este texto tiene información bastante detallada sobre nuestros productos y servicios disponibles actualmente?' +
12
+ ' (en cualquier talla y color disponible actualmente en nuestra tienda del centro comercial y sus alrededores)?' +
13
+ ' Si necesitas otra opción avísame por favor.';
14
+ const result = splitChatText(input);
15
+ const hasParenthetical = result.some((chunk) => chunk.includes('centro comercial'));
16
+ expect(hasParenthetical).toBe(true);
17
+ const hasContinuation = result.some((chunk) => chunk.includes('Si necesitas otra opción'));
18
+ expect(hasContinuation).toBe(true);
19
+ expect(result.length).toBeGreaterThanOrEqual(MIN_SPLIT_COUNT);
20
+ });
21
+
22
+ test('should not split when parenthetical has no text after and gap is large', () => {
23
+ const input =
24
+ 'Este texto tiene información bastante detallada sobre nuestros productos y servicios disponibles actualmente?' +
25
+ ' (en cualquier talla y color disponible actualmente en nuestra tienda del centro comercial y sus alrededores)?';
26
+ const result = splitChatText(input);
27
+ expect(result).toHaveLength(SINGLE_CHUNK);
28
+ });
29
+ });
30
+
31
+ describe('Question processor - long question emoji paths', () => {
32
+ test('should not split long question when only emoji follows', () => {
33
+ const input =
34
+ '¿Qué te parece esta opción del Nike Air Max 90 en color Hueso claro y Oliva neutro y Gris universitario? 😊';
35
+ const result = splitChatText(input);
36
+ expect(result).toEqual([input]);
37
+ });
38
+
39
+ test('should split long question with emoji and text after', () => {
40
+ const input =
41
+ '¿Qué te parece esta opción del Nike Air Max 90 en color Hueso claro y Oliva neutro y Gris universitario de Nike?' +
42
+ ' 😊 Tenemos muchas más opciones disponibles en nuestra tienda.';
43
+ const result = splitChatText(input);
44
+ const hasQuestionWithEmoji = result.some(
45
+ (chunk) => chunk.includes('Gris universitario de Nike?') && chunk.includes('😊')
46
+ );
47
+ expect(hasQuestionWithEmoji).toBe(true);
48
+ const hasAfter = result.some((chunk) => chunk.includes('Tenemos muchas más opciones'));
49
+ expect(hasAfter).toBe(true);
50
+ });
51
+ });
52
+
53
+ describe('Question processor - short question emoji paths', () => {
54
+ test('should split short question with emoji and text after', () => {
55
+ const input =
56
+ '¿Te gusta este producto? 😊 Podemos buscar otras opciones de productos disponibles para ti en la tienda virtual ahora mismo.';
57
+ const result = splitChatText(input);
58
+ const hasQuestionWithEmoji = result.some((chunk) => chunk.includes('¿Te gusta este producto? 😊'));
59
+ expect(hasQuestionWithEmoji).toBe(true);
60
+ const hasTextAfter = result.some((chunk) => chunk.includes('Podemos buscar otras opciones'));
61
+ expect(hasTextAfter).toBe(true);
62
+ expect(result.length).toBeGreaterThanOrEqual(MIN_SPLIT_COUNT);
63
+ });
64
+ });
65
+
66
+ describe('Question processor - contiguous questions emoji paths', () => {
67
+ test('should split contiguous questions with emoji and text after', () => {
68
+ const input =
69
+ '¿Te gusta? ¿Lo quieres? 😊 Tenemos descuentos especiales hoy disponibles para todos nuestros clientes.';
70
+ const result = splitChatText(input);
71
+ const hasQuestionsWithEmoji = result.some(
72
+ (chunk) => chunk.includes('¿Te gusta?') && chunk.includes('¿Lo quieres?') && chunk.includes('😊')
73
+ );
74
+ expect(hasQuestionsWithEmoji).toBe(true);
75
+ const hasTextAfter = result.some((chunk) => chunk.includes('Tenemos descuentos especiales'));
76
+ expect(hasTextAfter).toBe(true);
77
+ });
78
+
79
+ test('should not split contiguous questions when only emoji follows', () => {
80
+ const input = '¿Te gusta? ¿Lo quieres? 😊';
81
+ const result = splitChatText(input);
82
+ expect(result).toEqual([input]);
83
+ });
84
+ });
85
+
86
+ describe('Question processor - non-emoji uppercase after question', () => {
87
+ test('should split when uppercase text follows short question', () => {
88
+ const input =
89
+ '¿Te gusta este producto? Podemos buscar otras opciones para ti en la tienda de productos y accesorios deportivos.';
90
+ const result = splitChatText(input);
91
+ expect(result.length).toBeGreaterThanOrEqual(MIN_SPLIT_COUNT);
92
+ const hasQuestion = result.some((chunk) => chunk.includes('¿Te gusta este producto?'));
93
+ expect(hasQuestion).toBe(true);
94
+ });
95
+ });
@@ -0,0 +1,96 @@
1
+ import { describe, expect, test } from '@jest/globals';
2
+
3
+ import { splitChatText } from '../index.js';
4
+
5
+ describe('Number and price protection', () => {
6
+ test('should not split formatted numbers with periods', () => {
7
+ const input =
8
+ 'El precio total es $1.000.000 y puedes pagarlo en cuotas. También tenemos un descuento del 20% si pagas de contado.';
9
+ const expected = [
10
+ 'El precio total es $1.000.000 y puedes pagarlo en cuotas.',
11
+ 'También tenemos un descuento del 20% si pagas de contado.',
12
+ ];
13
+ expect(splitChatText(input)).toEqual(expected);
14
+ });
15
+
16
+ test('should handle multiple formatted numbers', () => {
17
+ const input =
18
+ 'Los precios van desde $100.000 hasta $5.000.000 dependiendo del modelo. También ofrecemos financiamiento desde $50.000 mensuales.';
19
+ const expected = [
20
+ 'Los precios van desde $100.000 hasta $5.000.000 dependiendo del modelo.',
21
+ 'También ofrecemos financiamiento desde $50.000 mensuales.',
22
+ ];
23
+ expect(splitChatText(input)).toEqual(expected);
24
+ });
25
+
26
+ test('should handle decimal numbers', () => {
27
+ const input =
28
+ 'La medida exacta es 15.5 centímetros y el peso es aproximadamente 2.3 kilogramos. La precisión es del 99.9% según las especificaciones técnicas.';
29
+ const expected = [
30
+ 'La medida exacta es 15.5 centímetros y el peso es aproximadamente 2.3 kilogramos.',
31
+ 'La precisión es del 99.9% según las especificaciones técnicas.',
32
+ ];
33
+ expect(splitChatText(input)).toEqual(expected);
34
+ });
35
+
36
+ test('should handle numbers without currency symbols', () => {
37
+ const input =
38
+ 'La producción anual es de 1.500.000 unidades y el crecimiento proyectado es del 25.5% para el próximo año. Las ventas superaron los 2.000.000 de unidades.';
39
+ const expected = [
40
+ 'La producción anual es de 1.500.000 unidades y el crecimiento proyectado es del 25.5% para el próximo año.',
41
+ 'Las ventas superaron los 2.000.000 de unidades.',
42
+ ];
43
+ expect(splitChatText(input)).toEqual(expected);
44
+ });
45
+ });
46
+
47
+ describe('Email protection - basic', () => {
48
+ test('should not split email addresses', () => {
49
+ const input =
50
+ 'Para contactarnos puedes escribir a juan.perez@example.com y te responderemos pronto. También puedes llamar al teléfono disponible.';
51
+ const expected = [
52
+ 'Para contactarnos puedes escribir a juan.perez@example.com y te responderemos pronto.',
53
+ 'También puedes llamar al teléfono disponible.',
54
+ ];
55
+ expect(splitChatText(input)).toEqual(expected);
56
+ });
57
+
58
+ test('should handle multiple email addresses', () => {
59
+ const input =
60
+ 'Contacta a support.team@company.co.uk para soporte técnico o a ventas.info@company.co.uk para información comercial. Nuestro equipo está disponible.';
61
+ const expected = [
62
+ 'Contacta a support.team@company.co.uk para soporte técnico o a ventas.info@company.co.uk para información comercial.',
63
+ 'Nuestro equipo está disponible.',
64
+ ];
65
+ expect(splitChatText(input)).toEqual(expected);
66
+ });
67
+ });
68
+
69
+ describe('Email protection - lists', () => {
70
+ test('should handle emails in numbered lists', () => {
71
+ const input =
72
+ 'Por favor envíame los siguientes datos:\n\n1. Nombre completo\n2. Correo electrónico (ejemplo: juan.perez@gmail.com)\n3. Número de teléfono\n\nTe contactaremos pronto.';
73
+ const expected = [
74
+ 'Por favor envíame los siguientes datos:',
75
+ '1. Nombre completo\n2. Correo electrónico (ejemplo: juan.perez@gmail.com)\n3. Número de teléfono',
76
+ 'Te contactaremos pronto.',
77
+ ];
78
+ expect(splitChatText(input)).toEqual(expected);
79
+ });
80
+
81
+ test('should keep numbered lists together without splitting at periods', () => {
82
+ const input = `📋 Perfecto, David 😊. Para procesar tu pedido contra‑entrega, necesito algunos datos:
83
+ 1. Nombre completo
84
+ 2. Email
85
+ 3. Cédula
86
+ 4. Dirección completa (incluye barrio y, si aplica, detalles del apartamento/torre/conjunto).`;
87
+ const expected = [
88
+ '📋 Perfecto, David 😊. Para procesar tu pedido contra‑entrega, necesito algunos datos:',
89
+ `1. Nombre completo
90
+ 2. Email
91
+ 3. Cédula
92
+ 4. Dirección completa (incluye barrio y, si aplica, detalles del apartamento/torre/conjunto).`,
93
+ ];
94
+ expect(splitChatText(input)).toEqual(expected);
95
+ });
96
+ });
@@ -0,0 +1,137 @@
1
+ import { describe, expect, test } from '@jest/globals';
2
+
3
+ import { splitChatText } from '../index.js';
4
+
5
+ describe('Data tests - numbered list normalization', () => {
6
+ test('Test 1: Numbered lists should not split at periods', () => {
7
+ const input = `📋 Para procesar tu pedido necesito algunos datos: 1. Nombre completo 2. Email 3. Cédula 4. Dirección completa (dirección exacta con barrio y si aplican detalles del apartamento/torre/conjunto).`;
8
+ const expected = [
9
+ `📋 Para procesar tu pedido necesito algunos datos:`,
10
+ `1. Nombre completo\n2. Email\n3. Cédula\n4. Dirección completa (dirección exacta con barrio y si aplican detalles del apartamento/torre/conjunto).`,
11
+ ];
12
+ expect(splitChatText(input)).toEqual(expected);
13
+ });
14
+
15
+ test('Test 2: Bullet lists should not split within items', () => {
16
+ const input = `Encontré estas opciones:\n\n- Nike Pegasus Plus – Zapatillas de alto rendimiento para maratones y running, con amortiguación ZoomX Foam y parte superior Flyknit que se adapta al pie. Disponibles en negro y en una combinación multicolor.\n- Nike Air Max 90 – Modelo clásico con suela tipo waffle y la icónica amortiguación Air visible, en tonos neutros como hueso claro/oliva/gris universitario.\n- Tenis de skateboarding – Zapatillas diseñadas para skate con suela vulcanizada y Zoom Air, disponibles en blanco con varios materiales (cuero, gamuza, algodón) y también en negro.\n¿Cuál de estos modelos te interesa más? 😊`;
17
+ const expected = [
18
+ `Encontré estas opciones:`,
19
+ `- Nike Pegasus Plus – Zapatillas de alto rendimiento para maratones y running, con amortiguación ZoomX Foam y parte superior Flyknit que se adapta al pie. Disponibles en negro y en una combinación multicolor.`,
20
+ `- Nike Air Max 90 – Modelo clásico con suela tipo waffle y la icónica amortiguación Air visible, en tonos neutros como hueso claro/oliva/gris universitario.`,
21
+ `- Tenis de skateboarding – Zapatillas diseñadas para skate con suela vulcanizada y Zoom Air, disponibles en blanco con varios materiales (cuero, gamuza, algodón) y también en negro.`,
22
+ `¿Cuál de estos modelos te interesa más? 😊`,
23
+ ];
24
+ expect(splitChatText(input)).toEqual(expected);
25
+ });
26
+ });
27
+
28
+ describe('Data tests - bullet list integrity', () => {
29
+ test('Test 3: Bullet lists should keep each item intact', () => {
30
+ const input = `Encontré estas opciones:\n\n- Tenis de skateboarding: Zapatillas diseñadas para skateboarding con suela vulcanizada, amortiguación Zoom Air y una parte superior rediseñada para un mejor ajuste y comodidad. Disponibles en varios colores y materiales como cuero, gamuza, lona y algodón. 👟\n- Nike Pegasus Plus: Zapatillas de alto rendimiento para running con espuma ZoomX Foam de largo completo, parte superior Flyknit transpirable y suela de goma resistente para tracción. Ideales para maratones y entrenamientos diarios. 🏃‍♂️\n- Nike Air Max 90: Calzado clásico de running con suela tipo waffle y amortiguación Air visible. Ofrece ventilación y comodidad con un diseño icónico y materiales de alta calidad. 👟\n- Nike Dunk Low Retro: Modelo clásico con parte superior de cuero auténtico y sintético, entresuela de espuma ligera y suela de goma con punto de pivote. Disponible en combinaciones de colores.`;
31
+ const expected = [
32
+ `Encontré estas opciones:`,
33
+ `- Tenis de skateboarding: Zapatillas diseñadas para skateboarding con suela vulcanizada, amortiguación Zoom Air y una parte superior rediseñada para un mejor ajuste y comodidad. Disponibles en varios colores y materiales como cuero, gamuza, lona y algodón. 👟`,
34
+ `- Nike Pegasus Plus: Zapatillas de alto rendimiento para running con espuma ZoomX Foam de largo completo, parte superior Flyknit transpirable y suela de goma resistente para tracción. Ideales para maratones y entrenamientos diarios. 🏃‍♂️`,
35
+ `- Nike Air Max 90: Calzado clásico de running con suela tipo waffle y amortiguación Air visible. Ofrece ventilación y comodidad con un diseño icónico y materiales de alta calidad. 👟`,
36
+ `- Nike Dunk Low Retro: Modelo clásico con parte superior de cuero auténtico y sintético, entresuela de espuma ligera y suela de goma con punto de pivote. Disponible en combinaciones de colores.`,
37
+ ];
38
+ expect(splitChatText(input)).toEqual(expected);
39
+ });
40
+ });
41
+
42
+ describe('Data tests - emoji and product descriptions', () => {
43
+ test('Test 4: emoji with question splitting', () => {
44
+ const input = `Lamentablemente, el Nike Pegasus Plus no está disponible en algodón. Sin embargo, tenemos tenis de skateboarding en algodón (color blanco) y otras opciones en este material. 😊 ¿Te gustaría continuar con alguna de estas alternativas o buscar otro producto?`;
45
+ const expected = [
46
+ 'Lamentablemente, el Nike Pegasus Plus no está disponible en algodón.',
47
+ 'Sin embargo, tenemos tenis de skateboarding en algodón (color blanco) y otras opciones en este material.',
48
+ '😊 ¿Te gustaría continuar con alguna de estas alternativas o buscar otro producto?',
49
+ ];
50
+ expect(splitChatText(input)).toEqual(expected);
51
+ });
52
+
53
+ test('Test 5: product description with question', () => {
54
+ const input = `Encontré esta opción: Tenis de skateboarding (Algodón, Color: Blanco) – Zapatillas diseñadas para skateboarding con suela vulcanizada, amortiguación Zoom Air y una parte superior rediseñada para un mejor ajuste y comodidad. 👍 ¿Te gusta el producto Tenis de skateboarding?`;
55
+ const expected = [
56
+ 'Encontré esta opción: Tenis de skateboarding (Algodón, Color: Blanco) – Zapatillas diseñadas para skateboarding con suela vulcanizada, amortiguación Zoom Air y una parte superior rediseñada para un mejor ajuste y comodidad.',
57
+ '👍 ¿Te gusta el producto Tenis de skateboarding?',
58
+ ];
59
+ expect(splitChatText(input)).toEqual(expected);
60
+ });
61
+ });
62
+
63
+ describe('Data tests - small chunk merging', () => {
64
+ test('Test 6: Small chunks should merge with next chunk', () => {
65
+ const input = `Mónica, ¿qué te parece la Nike Pegasus Plus? 👟 Precio: $1.015.000. Tall. 38, 41, 43. Colores: Negro, Azul/Espuma/Verde/Negro. Amortiguación ligera y transpirable. ¿Te gusta?`;
66
+ const expected = [
67
+ 'Mónica, ¿qué te parece la Nike Pegasus Plus? 👟 Precio: $1.015.000.',
68
+ 'Tall. 38, 41, 43. Colores: Negro, Azul/Espuma/Verde/Negro. Amortiguación ligera y transpirable. ¿Te gusta?',
69
+ ];
70
+ expect(splitChatText(input)).toEqual(expected);
71
+ });
72
+
73
+ test('Test 8: Small chunks with price should merge with next chunk', () => {
74
+ const input = `Nike Air Max 90 – $724.950. Tallas: 40‑43. Colores: Blanco, Gris, Rojo, Hueso, Oliva, Cueva. Suela waffle y Air visible. ¿Qué talla? 👟`;
75
+ const expected = [
76
+ `Nike Air Max 90 – $724.950.`,
77
+ `Tallas: 40‑43. Colores: Blanco, Gris, Rojo, Hueso, Oliva, Cueva. Suela waffle y Air visible. ¿Qué talla? 👟`,
78
+ ];
79
+ expect(splitChatText(input)).toEqual(expected);
80
+ });
81
+
82
+ test('Test 11: Small chunks with tallas should merge', () => {
83
+ const input = `Kiara, Nike Dunk Low Retro: $724.950. Tallas 39-43. Colores: Burdeos/Vinotinto, Azul, Oliva neutro/Caqui claro, Blanco/Blanco/Negro. Diseño icónico. Gusta o quieres otro color? 👟✨`;
84
+ const expected = [
85
+ `Kiara, Nike Dunk Low Retro: $724.950.`,
86
+ `Tallas 39-43. Colores: Burdeos/Vinotinto, Azul, Oliva neutro/Caqui claro, Blanco/Blanco/Negro. Diseño icónico. Gusta o quieres otro color? 👟✨`,
87
+ ];
88
+ expect(splitChatText(input)).toEqual(expected);
89
+ });
90
+ });
91
+
92
+ describe('Data tests - numbered list item splitting', () => {
93
+ test('Test 7: Numbered lists should not split items', () => {
94
+ const input = `Encontré dos opciones geniales para correr, Sebastián 👟:\n1. Nike Air Max 90 – $724.950, tallas 40-43. Colores variados, suela waffle.\n2. Nike Pegasus Plus – $1.015.000, tallas 38, 41, 43. Negro, azul y verde. ¿Cuál te gusta más?`;
95
+ const expected = [
96
+ `Encontré dos opciones geniales para correr, Sebastián 👟:`,
97
+ `1. Nike Air Max 90 – $724.950, tallas 40-43. Colores variados, suela waffle.`,
98
+ `2. Nike Pegasus Plus – $1.015.000, tallas 38, 41, 43. Negro, azul y verde. ¿Cuál te gusta más?`,
99
+ ];
100
+ expect(splitChatText(input)).toEqual(expected);
101
+ });
102
+
103
+ test('Test 9: Long list items (> 150 chars) should split by items', () => {
104
+ const input = `Encontré estas opciones:\nNike Trail – Chaqueta de running impermeable con acabado repelente al agua, ajuste holgado y tonos tierra. Disponible en color multicolor y tallas XS, S, M. Es ideal para correr en el bosque o en la montaña y mantenerse seco. 🏃‍♂️\nNike Sportswear Breaking Windrunner – Chaqueta amplia con acabado repelente al agua en color negro, con gráficos de átomos giratorios. Disponible en tallas XS, S, M. Perfecta para actividades urbanas o al aire libre, manteniendo la comodidad y la protección contra la lluvia. ☔\n\n¿Cuál de estas chaquetas te interesa más?`;
105
+ const expected = [
106
+ `Encontré estas opciones:`,
107
+ `Nike Trail – Chaqueta de running impermeable con acabado repelente al agua, ajuste holgado y tonos tierra. Disponible en color multicolor y tallas XS, S, M. Es ideal para correr en el bosque o en la montaña y mantenerse seco. 🏃‍♂️`,
108
+ `Nike Sportswear Breaking Windrunner – Chaqueta amplia con acabado repelente al agua en color negro, con gráficos de átomos giratorios. Disponible en tallas XS, S, M. Perfecta para actividades urbanas o al aire libre, manteniendo la comodidad y la protección contra la lluvia. ☔`,
109
+ `¿Cuál de estas chaquetas te interesa más?`,
110
+ ];
111
+ expect(splitChatText(input)).toEqual(expected);
112
+ });
113
+ });
114
+
115
+ describe('Data tests - long product list and emoji merge', () => {
116
+ test('Test 10: Long product list with bullet items', () => {
117
+ const input = `¡Perfecto, Leydi! Encontré varias opciones de zapatos deportivos que podrían interesarte. Aquí te detallo algunas:\n\n- Tenis de skateboarding Janoski por 430.000: Son ideales para un estilo casual y dinámico, con excelente agarre para actividades como el skate o uso diario, ofreciendo comodidad y flexibilidad gracias a su diseño vulcanizado y amortiguación Zoom Air.\n\n- Nike Pegasus Plus por 1.015.000: Perfectos para running y entrenamientos intensos, con espuma ZoomX para un retorno de energía superior y una parte superior Flyknit transpirable que se adapta perfectamente al pie.\n\n- Nike Air Max 90 por 724.950: Un clásico con amortiguación Air visible para comodidad todo el día, suela waffle para tracción y un diseño versátil que combina estilo retro con rendimiento en running o uso casual.\n\n- Nike Air Force 1 por 749.950: Icónicos y duraderos, con cuero premium y amortiguación Nike Air para un confort excepcional, ideales para la calle o la cancha con un toque atemporal.\n\n¿Cuál de estos te gusta más, o prefieres que busque algo específico como color o talla?`;
118
+ const expected = [
119
+ `¡Perfecto, Leydi! Encontré varias opciones de zapatos deportivos que podrían interesarte. Aquí te detallo algunas:`,
120
+ `- Tenis de skateboarding Janoski por 430.000: Son ideales para un estilo casual y dinámico, con excelente agarre para actividades como el skate o uso diario, ofreciendo comodidad y flexibilidad gracias a su diseño vulcanizado y amortiguación Zoom Air.`,
121
+ `- Nike Pegasus Plus por 1.015.000: Perfectos para running y entrenamientos intensos, con espuma ZoomX para un retorno de energía superior y una parte superior Flyknit transpirable que se adapta perfectamente al pie.`,
122
+ `- Nike Air Max 90 por 724.950: Un clásico con amortiguación Air visible para comodidad todo el día, suela waffle para tracción y un diseño versátil que combina estilo retro con rendimiento en running o uso casual.`,
123
+ `- Nike Air Force 1 por 749.950: Icónicos y duraderos, con cuero premium y amortiguación Nike Air para un confort excepcional, ideales para la calle o la cancha con un toque atemporal.`,
124
+ `¿Cuál de estos te gusta más, o prefieres que busque algo específico como color o talla?`,
125
+ ];
126
+ expect(splitChatText(input)).toEqual(expected);
127
+ });
128
+
129
+ test('Test 12: Small emoji-only chunks should merge backward', () => {
130
+ const input = `¡Perfecto! ✅❤️\n\nDe las dos opciones, la *Nike Trail* es ideal si buscas algo ligero y con ajuste holgado para terrenos complicados, mientras que la *Nike Sportswear Breaking Windrunner* te ofrece un tejido más absorbente y un diseño más clásico en negro.\n\n¿Te inclinas por alguna de ellas?\n\nY, si ya sabes la talla, dime cuál prefieres para que pueda confirmar disponibilidad y enviarte los detalles de envío. 🚚💨`;
131
+ const expected = [
132
+ `¡Perfecto! ✅❤️\n\nDe las dos opciones, la *Nike Trail* es ideal si buscas algo ligero y con ajuste holgado para terrenos complicados, mientras que la *Nike Sportswear Breaking Windrunner* te ofrece un tejido más absorbente y un diseño más clásico en negro.\n\n¿Te inclinas por alguna de ellas?`,
133
+ `Y, si ya sabes la talla, dime cuál prefieres para que pueda confirmar disponibilidad y enviarte los detalles de envío. 🚚💨`,
134
+ ];
135
+ expect(splitChatText(input)).toEqual(expected);
136
+ });
137
+ });
@@ -0,0 +1,134 @@
1
+ import { describe, expect, test } from '@jest/globals';
2
+
3
+ import { splitChatText } from '../index.js';
4
+
5
+ describe('Data tests - product cards with emoji pattern', () => {
6
+ test('Test 13: Product card lists with shopping emoji should split into individual cards', () => {
7
+ const input = `Encontré estas opciones:\n\n1. 🛍️ Zapatillas Pegasus Plus: 💵 $1.015.000\n📏 Color: Negro, Azul glacial/Espuma menta/Verde impacto/Negro.\n📏 Talla Calzado: 43, 41, 38.\n✅ Zapatillas Pegasus Plus: ultraligeras, con amortiguación ZoomX y gran transpirabilidad, diseñadas para running intensivo y maratones, ideal para tus entrenamientos de carrera.\n\n2. 🛍️ Zapaillas ISPA Sense: 💵 $804.900\n📏 Talla Calzado: 38, 39, 40, 41, 42, 43.\n✅ Zapaillas ISPA Sense: estilo casual con buena comodidad, pueden servir para trotes ligeros o uso diario, aunque no están optimizadas para alto rendimiento de running.\n\n¿Cuál de estos productos te gusta?`;
8
+ const expected = [
9
+ `Encontré estas opciones:`,
10
+ `🛍️ Zapatillas Pegasus Plus: 💵 $1.015.000\n📏 Color: Negro, Azul glacial/Espuma menta/Verde impacto/Negro.\n📏 Talla Calzado: 43, 41, 38.\n✅ Zapatillas Pegasus Plus: ultraligeras, con amortiguación ZoomX y gran transpirabilidad, diseñadas para running intensivo y maratones, ideal para tus entrenamientos de carrera.`,
11
+ `🛍️ Zapaillas ISPA Sense: 💵 $804.900\n📏 Talla Calzado: 38, 39, 40, 41, 42, 43.\n✅ Zapaillas ISPA Sense: estilo casual con buena comodidad, pueden servir para trotes ligeros o uso diario, aunque no están optimizadas para alto rendimiento de running.`,
12
+ `¿Cuál de estos productos te gusta?`,
13
+ ];
14
+ expect(splitChatText(input)).toEqual(expected);
15
+ });
16
+ });
17
+
18
+ describe('Data tests - product cards with bold formatting', () => {
19
+ test('Test 14: Bold formatted product cards should split into individual cards', () => {
20
+ const input = `Encontré estas opciones:\n\n**1. 🛍️ Tenis Skateboarding:** 💵 $430.000\n📏 **Color:** Blanco/Rosa óxido/Negro, Blanco, Negro.\n📏 **Talla Calzado:** 40, 38, 39, 41.\n📏 **Material:** Cuero, Gamuza, Lona, Algodon.\n✅ Este porque amas a Luisa\n\n**2. 🛍️ Zapatillas ISPA Axis:** 💵 $902.000\n📏 **Talla Calzado:** 38, 39, 40, 41, 42, 43.\n✅ Este por si acaso\n\n¿Cuál te gusta más?`;
21
+ const expected = [
22
+ `Encontré estas opciones:`,
23
+ `**🛍️ Tenis Skateboarding:** 💵 $430.000\n📏 **Color:** Blanco/Rosa óxido/Negro, Blanco, Negro.\n📏 **Talla Calzado:** 40, 38, 39, 41.\n📏 **Material:** Cuero, Gamuza, Lona, Algodon.\n✅ Este porque amas a Luisa`,
24
+ `**🛍️ Zapatillas ISPA Axis:** 💵 $902.000\n📏 **Talla Calzado:** 38, 39, 40, 41, 42, 43.\n✅ Este por si acaso`,
25
+ `¿Cuál te gusta más?`,
26
+ ];
27
+ expect(splitChatText(input)).toEqual(expected);
28
+ });
29
+ });
30
+
31
+ describe('Data tests - product cards with italic formatting', () => {
32
+ test('Test 15: Italic formatted product cards should split into individual cards', () => {
33
+ const input = `Encontré estas opciones:\n\n*1. 🛍️ Tenis Skateboarding:* 💵 $430.000\n📏 *Color:* Blanco/Rosa óxido/Negro, Blanco, Negro.\n📏 *Talla Calzado:* 40, 38, 39, 41.\n📏 *Material:* Cuero, Gamuza, Lona, Algodon.\n✅ Este porque amas a Luisa\n\n*2. 🛍️ Zapatillas ISPA Axis:* 💵 $902.000\n📏 *Talla Calzado:* 38, 39, 40, 41, 42, 43.\n✅ Este por si acaso\n\n¿Cuál te gusta más?`;
34
+ const expected = [
35
+ `Encontré estas opciones:`,
36
+ `*🛍️ Tenis Skateboarding:* 💵 $430.000\n📏 *Color:* Blanco/Rosa óxido/Negro, Blanco, Negro.\n📏 *Talla Calzado:* 40, 38, 39, 41.\n📏 *Material:* Cuero, Gamuza, Lona, Algodon.\n✅ Este porque amas a Luisa`,
37
+ `*🛍️ Zapatillas ISPA Axis:* 💵 $902.000\n📏 *Talla Calzado:* 38, 39, 40, 41, 42, 43.\n✅ Este por si acaso`,
38
+ `¿Cuál te gusta más?`,
39
+ ];
40
+ expect(splitChatText(input)).toEqual(expected);
41
+ });
42
+ });
43
+
44
+ describe('Data tests - product card trailing questions', () => {
45
+ test('Test 16: Product card lists should separate trailing questions', () => {
46
+ const input = `Encontré estas opciones:\n1. 🛍️ Zapatillas Pegasus Plus\n💵 Precio: $1.015.000\n🌈 Color: Negro, Azul glacial/Espuma menta/Verde impacto/Negro.\n👟 Talla Calzado: 43, 41, 38.\n✅ Zapatillas de alto rendimiento diseñadas para running, con amortiguación ZoomX Foam y Flyknit ligero, ideales para entrenamientos intensivos y maratones.\n\n2. 🛍️ Zapaillas ISPA Sense\n💵 Precio: $804.900\n👟 Talla Calzado: 38, 39, 40, 41, 42, 43.\n¿Cuál de estos productos te gusta?`;
47
+ const expected = [
48
+ `Encontré estas opciones:`,
49
+ `🛍️ Zapatillas Pegasus Plus\n💵 Precio: $1.015.000\n🌈 Color: Negro, Azul glacial/Espuma menta/Verde impacto/Negro.\n👟 Talla Calzado: 43, 41, 38.\n✅ Zapatillas de alto rendimiento diseñadas para running, con amortiguación ZoomX Foam y Flyknit ligero, ideales para entrenamientos intensivos y maratones.`,
50
+ `🛍️ Zapaillas ISPA Sense\n💵 Precio: $804.900\n👟 Talla Calzado: 38, 39, 40, 41, 42, 43.`,
51
+ `¿Cuál de estos productos te gusta?`,
52
+ ];
53
+ expect(splitChatText(input)).toEqual(expected);
54
+ });
55
+ });
56
+
57
+ const MARKDOWN_TITLES_INPUT = `¡Mira estas opciones de chaquetas para hombre! 👀
58
+
59
+ 1. *Conjunto Chaqueta y Pantaloneta - Hombre Urbano*
60
+ 💵 Precio: $554.950
61
+ 🌈 Color: Negro
62
+ 👕 Talla Ropa: XL, S, M, L
63
+ ✅ Conjunto de chaqueta y pantaloneta casual, perfecto para looks deportivos y comodidad en el día a día.
64
+
65
+ 2. *Chaqueta Hombre Urbano*
66
+ 💵 Precio: $425.950
67
+ 👕 Talla Ropa: M, S, L
68
+ 🌈 Color: Blanco/Blanco/Negro
69
+ ✅ Chaqueta urbana ligera y moderna, ideal para estilo streetwear y uso diario.
70
+
71
+ ¿Cuál de estos productos te gusta? 🤔`;
72
+
73
+ describe('Data tests - product cards with markdown titles', () => {
74
+ test('Test 17: Markdown title product cards should split into individual cards', () => {
75
+ const expected = [
76
+ `¡Mira estas opciones de chaquetas para hombre! 👀`,
77
+ `*Conjunto Chaqueta y Pantaloneta - Hombre Urbano*\n💵 Precio: $554.950\n🌈 Color: Negro\n👕 Talla Ropa: XL, S, M, L\n✅ Conjunto de chaqueta y pantaloneta casual, perfecto para looks deportivos y comodidad en el día a día.`,
78
+ `*Chaqueta Hombre Urbano*\n💵 Precio: $425.950\n👕 Talla Ropa: M, S, L\n🌈 Color: Blanco/Blanco/Negro\n✅ Chaqueta urbana ligera y moderna, ideal para estilo streetwear y uso diario.`,
79
+ `¿Cuál de estos productos te gusta? 🤔`,
80
+ ];
81
+ expect(splitChatText(MARKDOWN_TITLES_INPUT)).toEqual(expected);
82
+ });
83
+ });
84
+
85
+ describe('Data tests - inline product cards', () => {
86
+ test('Test 18: Inline product cards should be normalized with line breaks', () => {
87
+ const input = `¡Encontré estas opciones de chaquetas para hombre! **1. 🛍️ Conjunto Chaqueta y Pantaloneta - Hombre Urbano** 💵 **Precio:** $554.950 🌈 **Color:** Negro. 👕 **Talla Ropa:** XL, S, M, L. ✅ Conjunto completo de chaqueta y pantaloneta, ideal para estilo athleisure y comodidad diaria. **2. 🛍️ Chaqueta Hombre Urbano** 💵 **Precio:** $425.950 👕 **Talla Ropa:** M, S, L. 🌈 **Color:** Blanco/Blanco/Negro. ✅ Chaqueta urbana ligera y moderna, perfecta para looks casuales y streetwear. ¿Cuál de estos productos te gusta?`;
88
+ const expected = [
89
+ `¡Encontré estas opciones de chaquetas para hombre!`,
90
+ `**🛍️ Conjunto Chaqueta y Pantaloneta - Hombre Urbano**\n💵 **Precio:** $554.950\n🌈 **Color:** Negro.\n👕 **Talla Ropa:** XL, S, M, L.\n✅ Conjunto completo de chaqueta y pantaloneta, ideal para estilo athleisure y comodidad diaria.`,
91
+ `**🛍️ Chaqueta Hombre Urbano**\n💵 **Precio:** $425.950\n👕 **Talla Ropa:** M, S, L.\n🌈 **Color:** Blanco/Blanco/Negro.\n✅ Chaqueta urbana ligera y moderna, perfecta para looks casuales y streetwear.`,
92
+ `¿Cuál de estos productos te gusta?`,
93
+ ];
94
+ expect(splitChatText(input)).toEqual(expected);
95
+ });
96
+
97
+ test('Test 19: Single inline product card should separate question', () => {
98
+ const input = `¡Buenos días, un gusto tenerte en Nike! Mi nombre es Valentina. ¿Cuál es tu nombre? 🤟🏼 Sí, tenemos disponibles las Zapatillas Nike P‑6000 en blanco. La versión en blanco y negro no está en stock, pero esta es muy similar. Te presento una opción: **1. 🛍️ Zapatillas Mujer P‑6000** 💵 **Precio:** $659.000 🌈 **Color:** Blanco, Azul. 👟 **Talla Calzado:** 38, 39, 40, 41, 42, 43. ✅ Estas zapatillas retro estilo P‑6000 combinan mesh y cuero sintético en un diseño vintage que se adapta a looks casuales y urbanos. ¿Te gusta así? 😎`;
99
+ const expected = [
100
+ `¡Buenos días, un gusto tenerte en Nike! Mi nombre es Valentina. ¿Cuál es tu nombre? 🤟🏼 Sí, tenemos disponibles las Zapatillas Nike P‑6000 en blanco. La versión en blanco y negro no está en stock, pero esta es muy similar. Te presento una opción:`,
101
+ `**🛍️ Zapatillas Mujer P‑6000**\n💵 **Precio:** $659.000\n🌈 **Color:** Blanco, Azul.\n👟 **Talla Calzado:** 38, 39, 40, 41, 42, 43.\n✅ Estas zapatillas retro estilo P‑6000 combinan mesh y cuero sintético en un diseño vintage que se adapta a looks casuales y urbanos.`,
102
+ `¿Te gusta así? 😎`,
103
+ ];
104
+ expect(splitChatText(input)).toEqual(expected);
105
+ });
106
+ });
107
+
108
+ describe('Data tests - order confirmation and misc', () => {
109
+ test('Test 20: Order confirmation messages should split at blank lines', () => {
110
+ const input = `Tu orden fue creada exitosamente.\n*✅ ID de la orden:* 161d\n*🛍️ Productos:* 1 Zapatillas Air Max 90\n*💵 Total:* $759.950\n*📍 Dirección:* Carrera 20 a # 56 - 77, Apartamento 500, Bogotá, D.C., Bogotá, D.C.\n\nEn los próximos días estará llegando tu pedido.\nMuchas gracias por tu compra.`;
111
+ const expected = [
112
+ `Tu orden fue creada exitosamente.\n*✅ ID de la orden:* 161d\n*🛍️ Productos:* 1 Zapatillas Air Max 90\n*💵 Total:* $759.950\n*📍 Dirección:* Carrera 20 a # 56 - 77, Apartamento 500, Bogotá, D.C., Bogotá, D.C.`,
113
+ `En los próximos días estará llegando tu pedido.\nMuchas gracias por tu compra.`,
114
+ ];
115
+ expect(splitChatText(input)).toEqual(expected);
116
+ });
117
+
118
+ test('Test 21: Intro with emoji before numbered list should split correctly', () => {
119
+ const input = `Para procesar tu pedido me faltan algunos datos: 😎 1. Nombre completo 2. Email 3. Cédula 4. Dirección completa (dirección exacta con barrio y si aplican detalles del apartamento/torre/conjunto) 🚀`;
120
+ const expected = [
121
+ `Para procesar tu pedido me faltan algunos datos: 😎`,
122
+ `1. Nombre completo\n2. Email\n3. Cédula\n4. Dirección completa (dirección exacta con barrio y si aplican detalles del apartamento/torre/conjunto) 🚀`,
123
+ ];
124
+ expect(splitChatText(input)).toEqual(expected);
125
+ });
126
+
127
+ test('Test 22: Inline numbered list after question mark should add line breaks', () => {
128
+ const input = `Mira que encontré varias ciudades llamadas Cartagena. ¿Cuál es la tuya, Camila? 😎 1. Cartagena de Indias, Bolívar 2. Cartagena del Chairá, Caquetá 🏝`;
129
+ const expected = [
130
+ `Mira que encontré varias ciudades llamadas Cartagena. ¿Cuál es la tuya, Camila? 😎\n1. Cartagena de Indias, Bolívar\n2. Cartagena del Chairá, Caquetá 🏝`,
131
+ ];
132
+ expect(splitChatText(input)).toEqual(expected);
133
+ });
134
+ });
@@ -0,0 +1,157 @@
1
+ import { describe, expect, test } from '@jest/globals';
2
+
3
+ import { splitChatText } from '../index.js';
4
+
5
+ describe('Edge cases - empty and basic input', () => {
6
+ test('should return empty array for empty string', () => {
7
+ expect(splitChatText('')).toEqual([]);
8
+ });
9
+
10
+ test('should return empty array for null/undefined', () => {
11
+ expect(splitChatText(null)).toEqual([]);
12
+ expect(splitChatText(undefined)).toEqual([]);
13
+ });
14
+
15
+ test('should handle text with only newlines and spaces', () => {
16
+ const input = '\n\n \n ';
17
+ const expected = ['\n\n \n '];
18
+ expect(splitChatText(input)).toEqual(expected);
19
+ });
20
+
21
+ test('should handle mixed punctuation', () => {
22
+ const input = '¡Hola! ¿Cómo estás? ¡Qué bueno verte! Me alegra mucho poder ayudarte hoy.';
23
+ const expected = ['¡Hola! ¿Cómo estás? ¡Qué bueno verte! Me alegra mucho poder ayudarte hoy.'];
24
+ expect(splitChatText(input)).toEqual(expected);
25
+ });
26
+ });
27
+
28
+ describe('Edge cases - emoji handling', () => {
29
+ test('should handle text with emojis', () => {
30
+ const input =
31
+ '¡Hola! 😊 ¿Te gusta este producto? 🛍️ Tenemos descuentos especiales hoy. También puedes ver nuestro catálogo completo en línea. 📱💻';
32
+ const expected = [
33
+ '¡Hola! 😊 ¿te gusta este producto? 🛍️ Tenemos descuentos especiales hoy.',
34
+ 'También puedes ver nuestro catálogo completo en línea. 📱💻',
35
+ ];
36
+ expect(splitChatText(input)).toEqual(expected);
37
+ });
38
+
39
+ test('should keep emoji with question instead of starting new segment', () => {
40
+ const input = `¿Qué deseas hacer ahora? 😊
41
+ •Continuar con este pedido
42
+ •Comprar más productos
43
+ •Ver carrito
44
+ •Eliminar producto
45
+ •Reemplazar producto`;
46
+ const expected = [
47
+ `¿Qué deseas hacer ahora? 😊
48
+ `,
49
+ `•Continuar con este pedido
50
+ •Comprar más productos
51
+ •Ver carrito
52
+ •Eliminar producto
53
+ •Reemplazar producto`,
54
+ ];
55
+ expect(splitChatText(input)).toEqual(expected);
56
+ });
57
+ });
58
+
59
+ describe('Edge cases - question continuation and formatting', () => {
60
+ test('should not split at question mark when lowercase text follows', () => {
61
+ const input =
62
+ '¡Hola! Me llamo Antonia. Estoy a tu servicio en Nike. Una tienda deportiva donde podrás encontrar zapatos, ropa y accesorios icónicos de la moda y la innovación en el deporte. Por favor, dime ¿cuál es tu nombre? para conocerte mejor 😊';
63
+ const result = splitChatText(input);
64
+ const hasQuestionWithContinuation = result.some((chunk) =>
65
+ chunk.includes('¿cuál es tu nombre? para conocerte')
66
+ );
67
+ expect(hasQuestionWithContinuation).toBe(true);
68
+ const hasBrokenContinuation = result.some((chunk) => chunk.trim().startsWith('para conocerte mejor'));
69
+ expect(hasBrokenContinuation).toBe(false);
70
+ });
71
+
72
+ test('should handle text with markdown formatting', () => {
73
+ const input =
74
+ 'Este producto tiene **características premium** y viene con *garantía extendida*. Puedes ver más detalles en `especificaciones técnicas`. También incluye soporte 24/7.';
75
+ const expected = [
76
+ 'Este producto tiene **características premium** y viene con *garantía extendida*.',
77
+ 'Puedes ver más detalles en `especificaciones técnicas`. También incluye soporte 24/7.',
78
+ ];
79
+ expect(splitChatText(input)).toEqual(expected);
80
+ });
81
+ });
82
+
83
+ describe('Edge cases - abbreviation protection', () => {
84
+ test('should handle abbreviations like "etc." without breaking parentheses', () => {
85
+ const input =
86
+ '¡Perfecto! Para generar el enlace de pago y confirmar el total, necesito que me indiques:\n\n1. **Barrio**\n2. **Dirección exacta** (calle, número, referencia, etc.)\n\n¿Me puedes proporcionar esa información?';
87
+ const expected = [
88
+ '¡Perfecto! Para generar el enlace de pago y confirmar el total, necesito que me indiques:',
89
+ '1. **Barrio**\n2. **Dirección exacta** (calle, número, referencia, etc.)',
90
+ '¿Me puedes proporcionar esa información?',
91
+ ];
92
+ expect(splitChatText(input)).toEqual(expected);
93
+ });
94
+
95
+ test('should protect common abbreviations (etc., e.g., i.e., Dr., Mr.)', () => {
96
+ const input =
97
+ 'Necesito algunos datos personales (nombre, edad, etc.) para continuar. El Dr. Pérez te atenderá pronto.';
98
+ const expected = [
99
+ 'Necesito algunos datos personales (nombre, edad, etc.) para continuar.',
100
+ 'El Dr. Pérez te atenderá pronto.',
101
+ ];
102
+ expect(splitChatText(input)).toEqual(expected);
103
+ });
104
+ });
105
+
106
+ describe('Edge cases - location and complex abbreviations', () => {
107
+ test('should protect location abbreviations like D.C., U.S., U.K.', () => {
108
+ const input = `📦 Resumen final de tu pedido:
109
+ * Producto: Nike Sportswear Breaking Windrunner (1 unidad)
110
+ - Color: Negro
111
+ - Talla: M
112
+ - Precio: $388.465
113
+ * Envío a Bogotá D.C.: $5.000
114
+ Total a pagar (contra‑entrega): $393.465
115
+ ¿Confirmas? 😊`;
116
+ const expected = [
117
+ `📦 Resumen final de tu pedido:
118
+ * Producto: Nike Sportswear Breaking Windrunner (1 unidad)
119
+ - Color: Negro
120
+ - Talla: M
121
+ - Precio: $388.465
122
+ * Envío a Bogotá D.C.: $5.000
123
+ Total a pagar (contra‑entrega): $393.465
124
+ ¿Confirmas? 😊`,
125
+ ];
126
+ expect(splitChatText(input)).toEqual(expected);
127
+ });
128
+
129
+ test('should handle abbreviations with periods (S.A., E.U.A.)', () => {
130
+ const input =
131
+ 'La empresa S.A. fue fundada en el año 2020 por el Dr. Juan Pérez. Actualmente opera en E.U.A. y varios países de América Latina.';
132
+ const expected = [
133
+ 'La empresa S.A. fue fundada en el año 2020 por el Dr. Juan Pérez.',
134
+ 'Actualmente opera en E.U.A. y varios países de América Latina.',
135
+ ];
136
+ expect(splitChatText(input)).toEqual(expected);
137
+ });
138
+ });
139
+
140
+ describe('Edge cases - version numbers and long sentences', () => {
141
+ test('should handle text with version numbers', () => {
142
+ const input =
143
+ 'La nueva versión 2.5.1 incluye mejoras significativas en rendimiento. Actualizada desde la versión 2.4.8 con nuevas funcionalidades. Compatible con iOS 15.0 y superior.';
144
+ const expected = [
145
+ 'La nueva versión 2.5.1 incluye mejoras significativas en rendimiento.',
146
+ 'Actualizada desde la versión 2.4.8 con nuevas funcionalidades. Compatible con iOS 15.0 y superior.',
147
+ ];
148
+ expect(splitChatText(input)).toEqual(expected);
149
+ });
150
+
151
+ test('should handle very long single sentence', () => {
152
+ const input =
153
+ 'Este es un producto extraordinario que ha sido diseñado con la más alta calidad y atención al detalle, utilizando materiales premium importados directamente desde Europa y Asia, garantizando durabilidad excepcional, rendimiento superior y satisfacción total del cliente, respaldado por nuestro equipo de ingenieros especialistas con más de 20 años de experiencia en la industria.';
154
+ const expected = [input];
155
+ expect(splitChatText(input)).toEqual(expected);
156
+ });
157
+ });
@@ -0,0 +1,6 @@
1
+ const ZERO = 0;
2
+
3
+ export function countOccurrences(text: string, pattern: RegExp): number {
4
+ const matches = text.match(pattern);
5
+ return matches?.length ?? ZERO;
6
+ }