Initial ecommerce standalone package

This commit is contained in:
2026-06-10 14:06:16 +08:00
commit 3d98933e24
241 changed files with 135283 additions and 0 deletions
+236
View File
@@ -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;
}