/ src / utils / formatRenderers.tsx
formatRenderers.tsx
  1  import React from 'react';
  2  
  3  /**
  4   * Renders content in various formats (markdown, plain text, etc.)
  5   */
  6  
  7  interface FormatRendererProps {
  8      content: string;
  9      format: string;
 10  }
 11  
 12  // Function to process emphasis within a line of text for markdown
 13  const processEmphasis = (text: string): JSX.Element[] => {
 14      // Split the text by the markers for bold, italic, and code
 15      const parts: JSX.Element[] = [];
 16      let currentText = '';
 17      let inBold = false;
 18      let inItalic = false;
 19      let inCode = false;
 20  
 21      for (let i = 0; i < text.length; i++) {
 22          // Handle code (backticks)
 23          if (text[i] === '`' && (!inBold && !inItalic)) {
 24              if (currentText) {
 25                  parts.push(<span key={`text-${parts.length}`}>{currentText}</span>);
 26                  currentText = '';
 27              }
 28              inCode = !inCode;
 29              if (!inCode) {
 30                  // End of code section
 31                  const codeText = text.substring(text.indexOf('`', i - currentText.length - 1) + 1, i);
 32                  parts.push(<code key={`code-${parts.length}`} className="bg-gray-100 text-red-600 px-1 py-0.5 rounded font-mono text-sm">{codeText}</code>);
 33                  currentText = '';
 34              }
 35              continue;
 36          }
 37  
 38          if (inCode) {
 39              // Collecting code content
 40              continue;
 41          }
 42  
 43          // Handle bold (**text**)
 44          if (text.substring(i, i + 2) === '**' && !inItalic) {
 45              if (currentText) {
 46                  parts.push(<span key={`text-${parts.length}`}>{currentText}</span>);
 47                  currentText = '';
 48              }
 49              inBold = !inBold;
 50              i++; // Skip the second asterisk
 51              continue;
 52          }
 53  
 54          // Handle italic (*text*)
 55          if (text[i] === '*' && text[i - 1] !== '*' && text[i + 1] !== '*' && !inBold) {
 56              if (currentText) {
 57                  parts.push(<span key={`text-${parts.length}`}>{currentText}</span>);
 58                  currentText = '';
 59              }
 60              inItalic = !inItalic;
 61              continue;
 62          }
 63  
 64          // If we're in bold or italic, collect the text
 65          if (inBold) {
 66              currentText += text[i];
 67              if (i === text.length - 1 || (text.substring(i + 1, i + 3) === '**')) {
 68                  parts.push(<strong key={`bold-${parts.length}`}>{currentText}</strong>);
 69                  currentText = '';
 70                  inBold = false;
 71                  i += 2; // Skip the closing asterisks
 72              }
 73          } else if (inItalic) {
 74              currentText += text[i];
 75              if (i === text.length - 1 || text[i + 1] === '*') {
 76                  parts.push(<em key={`italic-${parts.length}`}>{currentText}</em>);
 77                  currentText = '';
 78                  inItalic = false;
 79                  i++; // Skip the closing asterisk
 80              }
 81          } else {
 82              currentText += text[i];
 83          }
 84      }
 85  
 86      // Add any remaining text
 87      if (currentText) {
 88          parts.push(<span key={`text-${parts.length}`}>{currentText}</span>);
 89      }
 90  
 91      return parts;
 92  };
 93  
 94  // Parse and render markdown content
 95  export const renderMarkdown = (text: string): JSX.Element[] => {
 96      const lines = text.split('\n');
 97      const result: JSX.Element[] = [];
 98      let inList = false;
 99      let listItems: JSX.Element[] = [];
100      let inOrderedList = false;
101      let inCodeBlock = false;
102      let codeContent = '';
103      let key = 0;
104  
105      const processListItems = () => {
106          if (listItems.length > 0) {
107              if (inOrderedList) {
108                  result.push(<ol key={key++} className="ml-5 list-decimal space-y-1 my-3">{listItems}</ol>);
109              } else {
110                  result.push(<ul key={key++} className="ml-5 list-disc space-y-1 my-3">{listItems}</ul>);
111              }
112              listItems = [];
113          }
114          inList = false;
115          inOrderedList = false;
116      };
117  
118      const processCodeBlock = () => {
119          if (codeContent) {
120              result.push(
121                  <pre key={key++} className="bg-gray-100 p-3 rounded-md my-3 overflow-auto">
122                      <code>{codeContent}</code>
123                  </pre>
124              );
125              codeContent = '';
126          }
127          inCodeBlock = false;
128      };
129  
130      lines.forEach((line) => {
131          // Code blocks
132          if (line.startsWith('```')) {
133              if (inCodeBlock) {
134                  processCodeBlock();
135              } else {
136                  inCodeBlock = true;
137              }
138              return;
139          }
140  
141          if (inCodeBlock) {
142              codeContent += line + '\n';
143              return;
144          }
145  
146          // Horizontal rule
147          if (line.match(/^(\*{3,}|-{3,}|_{3,})$/)) {
148              if (inList) processListItems();
149              result.push(<hr key={key++} className="my-4 border-t border-gray-200" />);
150              return;
151          }
152  
153          // Headers
154          if (line.startsWith('# ')) {
155              if (inList) processListItems();
156              result.push(<h1 key={key++} className="text-2xl font-bold mt-6 mb-3">{processEmphasis(line.substring(2))}</h1>);
157              return;
158          }
159          if (line.startsWith('## ')) {
160              if (inList) processListItems();
161              result.push(<h2 key={key++} className="text-xl font-bold mt-5 mb-2">{processEmphasis(line.substring(3))}</h2>);
162              return;
163          }
164          if (line.startsWith('### ')) {
165              if (inList) processListItems();
166              result.push(<h3 key={key++} className="text-lg font-bold mt-4 mb-2">{processEmphasis(line.substring(4))}</h3>);
167              return;
168          }
169          if (line.startsWith('#### ')) {
170              if (inList) processListItems();
171              result.push(<h4 key={key++} className="text-base font-bold mt-3 mb-2">{processEmphasis(line.substring(5))}</h4>);
172              return;
173          }
174          if (line.startsWith('##### ')) {
175              if (inList) processListItems();
176              result.push(<h5 key={key++} className="text-sm font-bold mt-3 mb-1">{processEmphasis(line.substring(6))}</h5>);
177              return;
178          }
179          if (line.startsWith('###### ')) {
180              if (inList) processListItems();
181              result.push(<h6 key={key++} className="text-xs font-bold mt-2 mb-1">{processEmphasis(line.substring(7))}</h6>);
182              return;
183          }
184  
185          // List items
186          const unorderedListMatch = line.match(/^(\s*[-*+]\s+)(.+)$/);
187          if (unorderedListMatch) {
188              if (inOrderedList) {
189                  processListItems(); // End the ordered list before starting unordered
190              }
191              inList = true;
192              const content = unorderedListMatch[2];
193              listItems.push(<li key={`list-${key++}`}>{processEmphasis(content)}</li>);
194              return;
195          }
196  
197          const orderedListMatch = line.match(/^(\s*\d+\.\s+)(.+)$/);
198          if (orderedListMatch) {
199              if (!inOrderedList && inList) {
200                  processListItems(); // End the unordered list before starting ordered
201              }
202              inList = true;
203              inOrderedList = true;
204              const content = orderedListMatch[2];
205              listItems.push(<li key={`list-${key++}`}>{processEmphasis(content)}</li>);
206              return;
207          }
208  
209          // If we were in a list but this line isn't a list item, end the list
210          if (inList && line.trim() !== '') {
211              processListItems();
212          }
213  
214          // Empty line
215          if (line.trim() === '') {
216              return;
217          }
218  
219          // Regular paragraph
220          result.push(<p key={key++} className="my-2">{processEmphasis(line)}</p>);
221      });
222  
223      // Clean up any remaining lists or code blocks
224      if (inList) processListItems();
225      if (inCodeBlock) processCodeBlock();
226  
227      return result;
228  };
229  
230  // Render plain text content
231  export const renderPlainText = (text: string): JSX.Element[] => {
232      return text.split('\n').map((line, i) => (
233          line.trim() === '' ? <br key={i} /> : <p key={i} className="my-1">{line}</p>
234      ));
235  };
236  
237  // Main format renderer component
238  export const FormatRenderer: React.FC<FormatRendererProps> = ({ content, format }) => {
239      let renderedContent: JSX.Element | JSX.Element[];
240  
241      switch (format.toLowerCase()) {
242          case 'markdown':
243          case 'md':
244              renderedContent = renderMarkdown(content);
245              break;
246  
247          case 'plain':
248          case 'text':
249          case 'txt':
250          default:
251              renderedContent = renderPlainText(content);
252              break;
253      }
254  
255      return (
256          <div className="format-renderer prose prose-sm prose-blue max-w-none">
257              {renderedContent}
258          </div>
259      );
260  };
261  
262  export default FormatRenderer;