Files
big-agi/src/modules/blocks/blocks.ts
T
2024-04-24 03:31:00 -07:00

115 lines
4.2 KiB
TypeScript

import type { Diff as TextDiff } from '@sanity/diff-match-patch';
import { heuristicIsHtml } from './RenderHtml';
import { heuristicLegacyImageBlocks, heuristicMarkdownImageReferenceBlocks } from './RenderImage';
// Block types
export type Block = CodeBlock | DiffBlock | HtmlBlock | ImageBlock | TextBlock;
export type CodeBlock = { type: 'code'; blockTitle: string; blockCode: string; complete: boolean; };
export type DiffBlock = { type: 'diff'; textDiffs: TextDiff[] };
export type HtmlBlock = { type: 'html'; html: string; };
export type ImageBlock = { type: 'image'; url: string; alt?: string }; // Added optional alt property
export type TextBlock = { type: 'text'; content: string; }; // for Text or Markdown
export function areBlocksEqual(a: Block, b: Block): boolean {
if (a.type !== b.type)
return false;
switch (a.type) {
case 'code':
return a.blockTitle === (b as CodeBlock).blockTitle && a.blockCode === (b as CodeBlock).blockCode && a.complete === (b as CodeBlock).complete;
case 'diff':
return false; // diff blocks are never equal
case 'html':
return a.html === (b as HtmlBlock).html;
case 'image':
return a.url === (b as ImageBlock).url && a.alt === (b as ImageBlock).alt;
case 'text':
return a.content === (b as TextBlock).content;
}
}
export function parseMessageBlocks(text: string, disableParsing: boolean, forceTextDiffs?: TextDiff[]): Block[] {
if (disableParsing)
return [{ type: 'text', content: text }];
if (forceTextDiffs && forceTextDiffs.length >= 1)
return [{ type: 'diff', textDiffs: forceTextDiffs }];
// special case: this could be generated by a proxy that returns an HTML page instead of the API response
if (heuristicIsHtml(text))
return [{ type: 'html', html: text }];
// special case: markdown image references (e.g. ![alt text](https://example.com/image.png))
const mdImageBlocks = heuristicMarkdownImageReferenceBlocks(text);
if (mdImageBlocks)
return mdImageBlocks;
// special case: legacy prodia images
const legacyImageBlocks = heuristicLegacyImageBlocks(text);
if (legacyImageBlocks)
return legacyImageBlocks;
const regexPatterns = {
// was: \w\x20\\.+-_ for tge filename, but was missing too much
// REVERTED THIS: was: (`{3,}\n?|$), but was matching backticks within blocks. so now it must end with a newline or stop
codeBlock: /`{3,}([\S\x20]+)?\n([\s\S]*?)(`{3,}\n?|$)/g,
htmlCodeBlock: /<!DOCTYPE html>([\s\S]*?)<\/html>/g,
svgBlock: /<svg (xmlns|width|viewBox)=([\s\S]*?)<\/svg>/g,
};
const blocks: Block[] = [];
let lastIndex = 0;
while (true) {
// find the first match (if any) trying all the regexes
let match: RegExpExecArray | null = null;
let matchType: keyof typeof regexPatterns | null = null;
for (const type in regexPatterns) {
const regex = regexPatterns[type as keyof typeof regexPatterns];
regex.lastIndex = lastIndex;
const currentMatch = regex.exec(text);
if (currentMatch && (match === null || currentMatch.index < match.index)) {
match = currentMatch;
matchType = type as keyof typeof regexPatterns;
}
}
if (match === null)
break;
// anything leftover before the match is text
if (match.index > lastIndex)
blocks.push({ type: 'text', content: text.slice(lastIndex, match.index) });
// add the block
switch (matchType) {
case 'codeBlock':
const blockTitle: string = (match[1] || '').trim();
const blockCode: string = match[2].trim();
const blockEnd: string = match[3];
blocks.push({ type: 'code', blockTitle, blockCode, complete: blockEnd.startsWith('```') });
break;
case 'htmlCodeBlock':
const html: string = `<!DOCTYPE html>${match[1]}</html>`;
blocks.push({ type: 'code', blockTitle: 'html', blockCode: html, complete: true });
break;
case 'svgBlock':
blocks.push({ type: 'code', blockTitle: 'svg', blockCode: match[0], complete: true });
break;
}
// advance the pointer
lastIndex = match.index + match[0].length;
}
// remainder is text
if (lastIndex < text.length)
blocks.push({ type: 'text', content: text.slice(lastIndex) });
return blocks;
}