123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362 |
- import { useMemo } from 'react';
- import { View, Text, RichText } from '@tarojs/components';
- // 定义RichText节点类型
- interface RichTextNode {
- name?: string;
- attrs?: Record<string, string>;
- children?: (RichTextNode | { type: 'text'; text: string })[];
- type?: 'text';
- text?: string;
- }
- // 列表项接口(包含内容和可能的子列表)
- interface ListItem {
- content: React.ReactNode;
- children?: ListLevel[]; // 嵌套在该项下的子列表
- key: string; // 唯一标识
- }
- // 列表层级信息接口
- interface ListLevel {
- type: 'ul' | 'ol';
- items: ListItem[];
- depth: number; // 缩进深度,用于判断嵌套关系
- }
- // 生成稳定的key(基于内容哈希)
- const getStableKey = (content: string, index: number = 0): string => {
- let hash = 0;
- const contentWithIndex = content + index.toString();
- for (let i = 0; i < contentWithIndex.length; i++) {
- const char = contentWithIndex.charCodeAt(i);
- hash = ((hash << 5) - hash) + char;
- hash = hash & hash; // 转换为32位整数
- }
- return `hash-${Math.abs(hash)}`;
- };
- // 行内元素解析规则
- const inlineRules = [
- {
- regex: /`([^`]+)`/,
- createNode: (content: string) => ({
- name: 'code',
- attrs: {},
- children: parseInlineToNodes(content)
- })
- },
- {
- regex: /!\[(.*?)\]\((.*?)\)/,
- createNode: (alt: string, url: string) => ({
- name: 'img',
- attrs: { src: url, alt },
- children: []
- })
- },
- {
- regex: /\[(.*?)\]\((.*?)\)/,
- createNode: (text: string, url: string) => ({
- name: 'a',
- attrs: { href: url },
- children: parseInlineToNodes(text)
- })
- },
- {
- regex: /\*\*(.*?)\*\*/,
- createNode: (content: string) => ({
- name: 'strong',
- attrs: {},
- children: parseInlineToNodes(content)
- })
- },
- {
- regex: /__(.*?)__/,
- createNode: (content: string) => ({
- name: 'strong',
- attrs: {},
- children: parseInlineToNodes(content)
- })
- },
- {
- regex: /\*(.*?)\*/,
- createNode: (content: string) => ({
- name: 'em',
- attrs: {},
- children: parseInlineToNodes(content)
- })
- },
- {
- regex: /_(.*?)_/,
- createNode: (content: string) => ({
- name: 'em',
- attrs: {},
- children: parseInlineToNodes(content)
- })
- }
- ];
- // 递归解析行内元素
- const parseInlineToNodes = (text: string): RichTextNode[] => {
- const nodes: RichTextNode[] = [];
- let remaining = text;
- while (remaining.length > 0) {
- let matched = false;
- let bestMatch: { index: number; rule: any; groups: string[] } | null = null;
- for (const rule of inlineRules) {
- const matches = remaining.match(rule.regex);
- if (matches && matches.index !== undefined) {
- if (!bestMatch || matches.index < bestMatch.index) {
- bestMatch = {
- index: matches.index,
- rule,
- groups: matches.slice(1)
- };
- }
- matched = true;
- }
- }
- if (bestMatch) {
- if (bestMatch.index > 0) {
- nodes.push({ type: 'text', text: remaining.substring(0, bestMatch.index) });
- }
- const node = bestMatch.rule.createNode(...bestMatch.groups);
- nodes.push(node);
- const fullMatch = remaining.match(bestMatch.rule.regex)![0];
- remaining = remaining.substring(bestMatch.index + fullMatch.length);
- } else if (matched) {
- nodes.push({ type: 'text', text: remaining });
- break;
- } else {
- nodes.push({ type: 'text', text: remaining });
- break;
- }
- }
- return nodes;
- };
- // 计算列表项的缩进深度(4个空格为一级)
- const getIndentDepth = (line: string): number => {
- const spaces = line.match(/^\s*/)![0];
- return Math.floor(spaces.length / 4); // 每4个空格算一级缩进
- };
- // 渲染列表项及其子列表
- const renderListItems = (items: ListItem[], level: number) => {
- return items.map((item, index) => {
- // 渲染子列表
- const renderChildLists = () => {
- if (!item.children || item.children.length === 0) return null;
-
- return item.children.map((childLevel, childIndex) => {
- const childListKey = getStableKey(`child-list-${childLevel.type}-${childIndex}`, index);
- return (
- <View
- key={childListKey}
- className={`markdown-list markdown-${childLevel.type} depth-${childLevel.depth}`}
- >
- {renderListItems(childLevel.items, level + 1)}
- </View>
- );
- });
- };
- return (
- <View key={item.key} className="markdown-list-item">
- <View className="markdown-list-item-content">{item.content}</View>
- {renderChildLists()}
- </View>
- );
- });
- };
- interface MarkdownParserProps {
- content: string;
- }
- const MarkdownParser = ({ content }: MarkdownParserProps) => {
- const parsedElements = useMemo(() => {
- const lines = content.split('\n');
- const elements: React.ReactNode[] = [];
- let inCodeBlock = false;
- let currentCodeBlock = '';
- let codeLang = '';
-
- // 列表栈:管理嵌套列表层级(栈顶为当前层级)
- const listStack: ListLevel[] = [];
- // 刷新当前列表(将栈中所有列表弹出并嵌套组装)
- const flushLists = () => {
- if (listStack.length === 0) return;
-
- // 从顶层渲染列表
- listStack.forEach((level, index) => {
- const listKey = getStableKey(`list-${level.type}-${level.depth}-${index}`);
- elements.push(
- <View key={listKey} className={`markdown-list markdown-${level.type} depth-${level.depth}`}>
- {renderListItems(level.items, index)}
- </View>
- );
- });
-
- // 清空栈
- listStack.length = 0;
- };
- // 处理代码块
- const flushCodeBlock = () => {
- if (inCodeBlock && currentCodeBlock) {
- const codeKey = getStableKey(currentCodeBlock);
- elements.push(
- <View key={codeKey} className="markdown-code-block">
- {codeLang && <Text className="markdown-code-lang">{codeLang}</Text>}
- <Text className="markdown-code">{currentCodeBlock}</Text>
- </View>
- );
- inCodeBlock = false;
- currentCodeBlock = '';
- codeLang = '';
- }
- };
- lines.forEach((line, lineIndex) => {
- // 处理空行(保留结构,但不添加多余元素)
- const trimmedLine = line.trim();
- if (!trimmedLine && !inCodeBlock) return;
- // 处理代码块
- if (/^```/.test(trimmedLine)) {
- flushLists(); // 遇到代码块,先刷新所有列表
- if (inCodeBlock) {
- flushCodeBlock();
- } else {
- inCodeBlock = true;
- codeLang = trimmedLine.replace(/^```/, '').trim();
- }
- return;
- }
- if (inCodeBlock) {
- currentCodeBlock += line + '\n';
- return;
- }
- // 处理标题
- const headingMatch = line.match(/^\s*#{1,6}\s+/);
- if (headingMatch) {
- flushLists(); // 遇到标题,刷新所有列表
- const level = headingMatch[0].match(/#/g)?.length || 1;
- const text = line.replace(/^\s*#{1,6}\s+/, '');
- const headingKey = getStableKey(`h${level}-${text}`, lineIndex);
- elements.push(
- <View key={headingKey} className={`markdown-h${level}`}>
- <RichText nodes={parseInlineToNodes(text)} />
- </View>
- );
- return;
- }
- // 处理无序列表和有序列表(统一显示为无序列表)
- const listMatch = line.match(/^\s*([-*+]|\d+\.)\s+/);
- if (listMatch) {
- const depth = getIndentDepth(line); // 当前列表项的缩进深度
- // 提取文本内容(移除列表标记)
- const text = line.replace(/^\s*([-*+]|\d+\.)\s+/, '');
- const itemKey = getStableKey(`list-item-${text}`, lineIndex);
-
- // 创建新列表项
- const newItem: ListItem = {
- content: <RichText nodes={parseInlineToNodes(text)} />,
- key: itemKey
- };
- // 1. 调整列表栈到当前深度(弹出过深的层级)
- while (listStack.length > depth) {
- const poppedLevel = listStack.pop()!;
- // 如果栈不为空,将弹出的层级作为上一级最后一个项的子列表
- if (listStack.length > 0) {
- const parentLevel = listStack[listStack.length - 1];
- if (parentLevel.items.length > 0) {
- const lastParentItem = parentLevel.items[parentLevel.items.length - 1];
- if (!lastParentItem.children) {
- lastParentItem.children = [];
- }
- lastParentItem.children.push(poppedLevel);
- }
- } else {
- // 如果栈为空,直接渲染弹出的层级
- const listKey = getStableKey(`unexpected-list-${poppedLevel.type}-${poppedLevel.depth}`);
- elements.push(
- <View key={listKey} className={`markdown-list markdown-${poppedLevel.type} depth-${poppedLevel.depth}`}>
- {renderListItems(poppedLevel.items, 0)}
- </View>
- );
- }
- }
- // 2. 如果栈为空或当前深度大于栈顶深度,创建新层级(统一使用ul类型)
- if (listStack.length === 0 || depth > listStack[listStack.length - 1].depth) {
- listStack.push({
- type: 'ul', // 所有列表都显示为无序列表
- items: [newItem],
- depth
- });
- } else {
- // 3. 否则添加到当前层级
- listStack[listStack.length - 1].items.push(newItem);
- }
- return;
- }
- // 处理引用
- const quoteMatch = line.match(/^\s*>\s+/);
- if (quoteMatch) {
- flushLists();
- const text = line.replace(/^\s*>\s+/, '');
- const quoteKey = getStableKey(`quote-${text}`, lineIndex);
- elements.push(
- <View key={quoteKey} className="markdown-quote">
- <RichText nodes={parseInlineToNodes(text)} />
- </View>
- );
- return;
- }
- // 处理分隔线
- if (/^\s*[-*]{3,}\s*$/.test(line)) {
- flushLists();
- const hrKey = getStableKey(`hr-${line}`, lineIndex);
- elements.push(
- <View key={hrKey} className="markdown-hr" />
- );
- return;
- }
- // 处理普通文本行
- flushLists(); // 普通文本前刷新所有列表
- const paragraphKey = getStableKey(`p-${line}`, lineIndex);
- elements.push(
- <View key={paragraphKey} className="markdown-paragraph">
- <RichText nodes={parseInlineToNodes(line)} />
- </View>
- );
- });
- // 解析结束后刷新剩余列表和代码块
- flushLists();
- flushCodeBlock();
- return elements;
- }, [content]);
- return (
- <View className="markdown-viewer">
- {parsedElements}
- </View>
- );
- };
- export default MarkdownParser;
|