Skip to content
This repository was archived by the owner on Aug 8, 2025. It is now read-only.

Commit 7f27150

Browse files
committed
[feature]: added table support;
1 parent edd8895 commit 7f27150

File tree

1 file changed

+79
-39
lines changed

1 file changed

+79
-39
lines changed

src/component/markdown/MarkdownToHTMLRenderer.tsx

Lines changed: 79 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,8 @@ const parseInlineMarkdown = (line: string): React.ReactNode[] => {
1616
{ regex: /\*\*(.*?)\*\*/, tag: 'strong' },
1717
{ regex: /\*(.*?)\*/, tag: 'em' },
1818
{ regex: /`([^`]+)`/, tag: 'code' },
19-
{ regex: /\[([^\]]+)\]\(([^)]+)\)/, tag: 'a' },
19+
{ regex: /\[([^\]]+)]\(([^)]+)\)/, tag: 'a' },
2020
{ regex: /(https?:\/\/[^\s)]+[^\s).,;:'"\]\s])/, tag: 'auto-link' },
21-
{ regex: /---/, tag: 'hr' },
2221
];
2322

2423
while (line.length) {
@@ -53,9 +52,10 @@ const parseInlineMarkdown = (line: string): React.ReactNode[] => {
5352
</code>
5453
);
5554
} else if (pattern.tag === 'a') {
55+
const isUrl = /^https?:\/\//.test(content);
5656
parts.push(
5757
<a key={parts.length} href={match[2]} target="_blank" rel="noreferrer">
58-
{content}
58+
{isUrl ? match[2] : content}
5959
</a>
6060
);
6161
} else if (pattern.tag === 'auto-link') {
@@ -64,8 +64,6 @@ const parseInlineMarkdown = (line: string): React.ReactNode[] => {
6464
{match[1]}
6565
</a>
6666
);
67-
} else if (pattern.tag === 'hr') {
68-
parts.push(<hr key={parts.length} style={{ border: '1px solid #ccc', margin: '16px 0' }} />);
6967
}
7068

7169
line = remaining;
@@ -101,19 +99,65 @@ const parseMarkdown = (raw: string) => {
10199
}
102100
};
103101

104-
lines.forEach((line, index) => {
102+
let i = 0;
103+
while (i < lines.length) {
104+
const line = lines[i];
105+
106+
if (/^(\s*)([-*_])(\s*\2){2,}\s*$/.test(line)) {
107+
flushList(i);
108+
elements.push(
109+
<hr key={`hr-${i}`} style={{ margin: '16px 0', border: 'none', borderTop: '1px solid #ccc' }} />
110+
);
111+
i++;
112+
continue;
113+
}
114+
115+
if (/^\|(.+)\|$/.test(line) && i + 1 < lines.length && /^\|[-\s|]+?\|$/.test(lines[i + 1])) {
116+
flushList(i);
117+
const headers = line.split('|').slice(1, -1).map(h => h.trim());
118+
const rows: string[][] = [];
119+
i += 2;
120+
121+
while (i < lines.length && /^\|(.+)\|$/.test(lines[i])) {
122+
const cells = lines[i].split('|').slice(1, -1).map(c => c.trim());
123+
rows.push(cells);
124+
i++;
125+
}
126+
127+
elements.push(
128+
<table key={`table-${i}`} style={{ borderCollapse: 'collapse', margin: '12px 0' }}>
129+
<thead>
130+
<tr>
131+
{headers.map((h, j) => (
132+
<th key={`th-${j}`} style={{ border: '1px solid #ccc', padding: 6, background: '#f7f7f7' }}>
133+
{parseInlineMarkdown(h)}
134+
</th>
135+
))}
136+
</tr>
137+
</thead>
138+
<tbody>
139+
{rows.map((row, rowIndex) => (
140+
<tr key={`tr-${rowIndex}`}>
141+
{row.map((cell, colIndex) => (
142+
<td key={`td-${rowIndex}-${colIndex}`} style={{ border: '1px solid #ccc', padding: 6 }}>
143+
{parseInlineMarkdown(cell)}
144+
</td>
145+
))}
146+
</tr>
147+
))}
148+
</tbody>
149+
</table>
150+
);
151+
continue;
152+
}
153+
105154
if (line.trim().startsWith('```')) {
106-
flushList(index);
155+
flushList(i);
107156
if (inCodeBlock) {
108157
elements.push(
109158
<pre
110-
key={`code-${index}`}
111-
style={{
112-
background: '#1e1e1e',
113-
color: '#fff',
114-
padding: 12,
115-
overflowX: 'auto',
116-
}}
159+
key={`code-${i}`}
160+
style={{ background: '#1e1e1e', color: '#fff', padding: 12, overflowX: 'auto' }}
117161
>
118162
<code>{codeBuffer.join('\n')}</code>
119163
</pre>
@@ -123,64 +167,60 @@ const parseMarkdown = (raw: string) => {
123167
} else {
124168
inCodeBlock = true;
125169
}
126-
return;
170+
i++;
171+
continue;
127172
}
128173

129174
if (inCodeBlock) {
130175
codeBuffer.push(line);
131-
return;
176+
i++;
177+
continue;
132178
}
133179

134-
// blockquote
135180
if (line.startsWith('>')) {
136-
flushList(index);
181+
flushList(i);
137182
elements.push(
138-
<blockquote
139-
key={`quote-${index}`}
140-
style={{
141-
borderLeft: '4px solid #ccc',
142-
paddingLeft: 12,
143-
margin: '12px 0',
144-
}}
145-
>
183+
<blockquote key={`quote-${i}`} style={{ borderLeft: '4px solid #ccc', paddingLeft: 12, margin: '12px 0' }}>
146184
{parseInlineMarkdown(line.slice(1).trim())}
147185
</blockquote>
148186
);
149-
return;
187+
i++;
188+
continue;
150189
}
151190

152-
// heading
153191
const headingMatch = line.match(/^(#{1,6})\s+(.*)/);
154192
if (headingMatch) {
155-
flushList(index);
193+
flushList(i);
156194
const level = headingMatch[1].length;
157195
const HeadingTag = `h${level}` as keyof JSX.IntrinsicElements;
158196
elements.push(
159-
<HeadingTag key={`h-${index}`} style={{ margin: '12px 0' }}>
197+
<HeadingTag key={`h-${i}`} style={{ margin: '12px 0' }}>
160198
{parseInlineMarkdown(headingMatch[2])}
161199
</HeadingTag>
162200
);
163-
return;
201+
i++;
202+
continue;
164203
}
165204

166-
// list
167205
if (/^\s*[-*+]\s+/.test(line)) {
168206
const content = line.replace(/^[-*+]\s+/, '');
169-
currentListItems.push(<li key={`li-${index}`}>{parseInlineMarkdown(content)}</li>);
170-
return;
207+
currentListItems.push(<li key={`li-${i}`}>{parseInlineMarkdown(content)}</li>);
208+
i++;
209+
continue;
171210
} else {
172-
flushList(index);
211+
flushList(i);
173212
}
174213

175214
const trimmed = line.trim();
176215
if (trimmed.length > 0) {
177216
elements.push(
178-
<p key={`p-${index}`}>{parseInlineMarkdown(trimmed)}</p>
217+
<p key={`p-${i}`}>{parseInlineMarkdown(trimmed)}</p>
179218
);
180219
}
181-
});
182220

183-
// Flush any remaining list at end
221+
i++;
222+
}
223+
184224
flushList(lines.length);
185225

186226
return elements;
@@ -190,4 +230,4 @@ const MarkdownToHTMLRenderer: React.FC<MarkdownToHTMLRendererProps> = ({ markdow
190230
return <div style={{ fontFamily: 'system-ui', lineHeight: 1.6 }}>{parseMarkdown(markdown)}</div>;
191231
};
192232

193-
export default MarkdownToHTMLRenderer;
233+
export default MarkdownToHTMLRenderer;

0 commit comments

Comments
 (0)