Initial ecommerce standalone package
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export function renderPlainMarkdownText(text: string, keyPrefix: string): ReactNode[] {
|
||||
const nodes: ReactNode[] = [];
|
||||
const pattern = /https?:\/\/[^\s)]+/g;
|
||||
let cursor = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = pattern.exec(text)) !== null) {
|
||||
if (match.index > cursor) {
|
||||
nodes.push(text.slice(cursor, match.index));
|
||||
}
|
||||
|
||||
const url = match[0];
|
||||
nodes.push(
|
||||
<a key={`${keyPrefix}-link-${match.index}`} href={url} target="_blank" rel="noreferrer">
|
||||
{url}
|
||||
</a>,
|
||||
);
|
||||
cursor = match.index + url.length;
|
||||
}
|
||||
|
||||
if (cursor < text.length) {
|
||||
nodes.push(text.slice(cursor));
|
||||
}
|
||||
|
||||
return nodes.length > 0 ? nodes : [text];
|
||||
}
|
||||
|
||||
export function renderMarkdownInline(text: string, keyPrefix: string): ReactNode[] {
|
||||
const nodes: ReactNode[] = [];
|
||||
const pattern = /(`[^`\n]+`|\*\*[\s\S]+?\*\*|__[\s\S]+?__|~~[\s\S]+?~~|\[[^\]]+\]\(https?:\/\/[^)\s]+\)|https?:\/\/[^\s)]+)/g;
|
||||
let cursor = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = pattern.exec(text)) !== null) {
|
||||
const token = match[0];
|
||||
if (match.index > cursor) {
|
||||
nodes.push(...renderPlainMarkdownText(text.slice(cursor, match.index), `${keyPrefix}-text-${cursor}`));
|
||||
}
|
||||
|
||||
if (token.startsWith("`") && token.endsWith("`")) {
|
||||
nodes.push(<code key={`${keyPrefix}-code-${match.index}`}>{token.slice(1, -1)}</code>);
|
||||
} else if ((token.startsWith("**") && token.endsWith("**")) || (token.startsWith("__") && token.endsWith("__"))) {
|
||||
nodes.push(<strong key={`${keyPrefix}-strong-${match.index}`}>{renderMarkdownInline(token.slice(2, -2), `${keyPrefix}-strong-${match.index}`)}</strong>);
|
||||
} else if (token.startsWith("~~") && token.endsWith("~~")) {
|
||||
nodes.push(<del key={`${keyPrefix}-del-${match.index}`}>{renderMarkdownInline(token.slice(2, -2), `${keyPrefix}-del-${match.index}`)}</del>);
|
||||
} else if (token.startsWith("[") && token.includes("](") && token.endsWith(")")) {
|
||||
const labelEnd = token.indexOf("](");
|
||||
const label = token.slice(1, labelEnd);
|
||||
const href = token.slice(labelEnd + 2, -1);
|
||||
nodes.push(
|
||||
<a key={`${keyPrefix}-md-link-${match.index}`} href={href} target="_blank" rel="noreferrer">
|
||||
{renderMarkdownInline(label, `${keyPrefix}-md-link-label-${match.index}`)}
|
||||
</a>,
|
||||
);
|
||||
} else {
|
||||
nodes.push(
|
||||
<a key={`${keyPrefix}-raw-link-${match.index}`} href={token} target="_blank" rel="noreferrer">
|
||||
{token}
|
||||
</a>,
|
||||
);
|
||||
}
|
||||
|
||||
cursor = match.index + token.length;
|
||||
}
|
||||
|
||||
if (cursor < text.length) {
|
||||
nodes.push(...renderPlainMarkdownText(text.slice(cursor), `${keyPrefix}-text-${cursor}`));
|
||||
}
|
||||
|
||||
return nodes.length > 0 ? nodes : [text];
|
||||
}
|
||||
|
||||
export function splitMarkdownTableRow(line: string): string[] {
|
||||
return line
|
||||
.trim()
|
||||
.replace(/^\|/, "")
|
||||
.replace(/\|$/, "")
|
||||
.split("|")
|
||||
.map((cell) => cell.trim());
|
||||
}
|
||||
|
||||
export function isMarkdownTableDivider(line: string): boolean {
|
||||
return /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(line);
|
||||
}
|
||||
|
||||
export function isMarkdownBlockStart(line: string, nextLine?: string): boolean {
|
||||
const trimmed = line.trim();
|
||||
return (
|
||||
trimmed === "" ||
|
||||
/^```/.test(trimmed) ||
|
||||
/^#{1,4}\s+/.test(trimmed) ||
|
||||
/^>\s?/.test(trimmed) ||
|
||||
/^[-*+]\s+/.test(trimmed) ||
|
||||
/^\d+[.)]\s+/.test(trimmed) ||
|
||||
/^[-*_]{3,}$/.test(trimmed) ||
|
||||
(line.includes("|") && !!nextLine && isMarkdownTableDivider(nextLine))
|
||||
);
|
||||
}
|
||||
|
||||
export function renderMarkdownBlocks(text: string): ReactNode[] {
|
||||
const lines = text.replace(/\r\n?/g, "\n").trim().split("\n");
|
||||
const blocks: ReactNode[] = [];
|
||||
let index = 0;
|
||||
|
||||
while (index < lines.length) {
|
||||
const line = lines[index] ?? "";
|
||||
const trimmed = line.trim();
|
||||
|
||||
if (!trimmed) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const fenceMatch = trimmed.match(/^```(\S+)?/);
|
||||
if (fenceMatch) {
|
||||
const language = fenceMatch[1] || "";
|
||||
const codeLines: string[] = [];
|
||||
index += 1;
|
||||
while (index < lines.length && !lines[index].trim().startsWith("```")) {
|
||||
codeLines.push(lines[index]);
|
||||
index += 1;
|
||||
}
|
||||
if (index < lines.length) index += 1;
|
||||
blocks.push(
|
||||
<figure key={`code-${index}`} className="ai-chat-markdown-code">
|
||||
{language ? <figcaption>{language}</figcaption> : null}
|
||||
<pre>
|
||||
<code>{codeLines.join("\n")}</code>
|
||||
</pre>
|
||||
</figure>,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const headingMatch = trimmed.match(/^(#{1,4})\s+(.+)$/);
|
||||
if (headingMatch) {
|
||||
const level = Math.min(headingMatch[1].length, 4);
|
||||
const Tag = `h${level}` as keyof JSX.IntrinsicElements;
|
||||
blocks.push(<Tag key={`heading-${index}`}>{renderMarkdownInline(headingMatch[2], `heading-${index}`)}</Tag>);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/^[-*_]{3,}$/.test(trimmed)) {
|
||||
blocks.push(<hr key={`rule-${index}`} />);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.includes("|") && index + 1 < lines.length && isMarkdownTableDivider(lines[index + 1])) {
|
||||
const headers = splitMarkdownTableRow(line);
|
||||
const rows: string[][] = [];
|
||||
index += 2;
|
||||
while (index < lines.length && lines[index].includes("|") && lines[index].trim()) {
|
||||
rows.push(splitMarkdownTableRow(lines[index]));
|
||||
index += 1;
|
||||
}
|
||||
blocks.push(
|
||||
<div key={`table-${index}`} className="ai-chat-markdown-table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{headers.map((header, cellIndex) => (
|
||||
<th key={`table-head-${cellIndex}`}>{renderMarkdownInline(header, `table-head-${cellIndex}`)}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((row, rowIndex) => (
|
||||
<tr key={`table-row-${rowIndex}`}>
|
||||
{headers.map((_, cellIndex) => (
|
||||
<td key={`table-cell-${rowIndex}-${cellIndex}`}>
|
||||
{renderMarkdownInline(row[cellIndex] || "", `table-cell-${rowIndex}-${cellIndex}`)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/^>\s?/.test(trimmed)) {
|
||||
const quoteLines: string[] = [];
|
||||
while (index < lines.length && /^>\s?/.test(lines[index].trim())) {
|
||||
quoteLines.push(lines[index].trim().replace(/^>\s?/, ""));
|
||||
index += 1;
|
||||
}
|
||||
blocks.push(
|
||||
<blockquote key={`quote-${index}`}>
|
||||
{quoteLines.map((quoteLine, quoteIndex) => (
|
||||
<p key={`quote-line-${quoteIndex}`}>{renderMarkdownInline(quoteLine, `quote-${index}-${quoteIndex}`)}</p>
|
||||
))}
|
||||
</blockquote>,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const unorderedMatch = trimmed.match(/^[-*+]\s+(.+)$/);
|
||||
const orderedMatch = trimmed.match(/^\d+[.)]\s+(.+)$/);
|
||||
if (unorderedMatch || orderedMatch) {
|
||||
const ordered = Boolean(orderedMatch);
|
||||
const ListTag = ordered ? "ol" : "ul";
|
||||
const items: string[] = [];
|
||||
while (index < lines.length) {
|
||||
const itemMatch = ordered ? lines[index].trim().match(/^\d+[.)]\s+(.+)$/) : lines[index].trim().match(/^[-*+]\s+(.+)$/);
|
||||
if (!itemMatch) break;
|
||||
items.push(itemMatch[1]);
|
||||
index += 1;
|
||||
}
|
||||
blocks.push(
|
||||
<ListTag key={`list-${index}`}>
|
||||
{items.map((item, itemIndex) => (
|
||||
<li key={`list-item-${itemIndex}`}>{renderMarkdownInline(item, `list-${index}-${itemIndex}`)}</li>
|
||||
))}
|
||||
</ListTag>,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const paragraphLines: string[] = [line];
|
||||
index += 1;
|
||||
while (index < lines.length && !isMarkdownBlockStart(lines[index], lines[index + 1])) {
|
||||
paragraphLines.push(lines[index]);
|
||||
index += 1;
|
||||
}
|
||||
const paragraphText = paragraphLines.join("\n").trim();
|
||||
blocks.push(<p key={`paragraph-${index}`}>{renderMarkdownInline(paragraphText, `paragraph-${index}`)}</p>);
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
Reference in New Issue
Block a user