| New file |
| | |
| | | <template> |
| | | <div class="markdown-body assistant-markdown break-words" v-html="renderedContent"></div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | const props = defineProps({ |
| | | content: { |
| | | type: String, |
| | | default: '' |
| | | } |
| | | }) |
| | | |
| | | const renderedContent = computed(() => renderMarkdown(props.content)) |
| | | |
| | | function renderMarkdown(content) { |
| | | const normalized = normalizeMarkdownSource(content) |
| | | if (!normalized) { |
| | | return '' |
| | | } |
| | | |
| | | const lines = normalized.split('\n') |
| | | const html = [] |
| | | |
| | | for (let index = 0; index < lines.length; ) { |
| | | const line = lines[index].trim() |
| | | |
| | | if (!line) { |
| | | index += 1 |
| | | continue |
| | | } |
| | | |
| | | if (/^```/.test(line)) { |
| | | const codeLines = [] |
| | | index += 1 |
| | | while (index < lines.length && !/^```/.test(lines[index].trim())) { |
| | | codeLines.push(lines[index]) |
| | | index += 1 |
| | | } |
| | | if (index < lines.length) { |
| | | index += 1 |
| | | } |
| | | html.push(`<pre><code>${escapeHtml(codeLines.join('\n'))}</code></pre>`) |
| | | continue |
| | | } |
| | | |
| | | if (/^\s*#{1,6}\s+/.test(line)) { |
| | | html.push(renderHeadingLine(line)) |
| | | index += 1 |
| | | continue |
| | | } |
| | | |
| | | if (/^\s*[-*]\s+/.test(line)) { |
| | | const listLines = [] |
| | | while (index < lines.length && /^\s*[-*]\s+/.test(lines[index].trim())) { |
| | | listLines.push(lines[index].trim()) |
| | | index += 1 |
| | | } |
| | | html.push(`<ul>${listLines |
| | | .map((item) => `<li>${renderMarkdownInline(item.replace(/^\s*[-*]\s+/, ''))}</li>`) |
| | | .join('')}</ul>`) |
| | | continue |
| | | } |
| | | |
| | | if (/^\s*\d+\.\s+/.test(line)) { |
| | | const listLines = [] |
| | | while (index < lines.length && /^\s*\d+\.\s+/.test(lines[index].trim())) { |
| | | listLines.push(lines[index].trim()) |
| | | index += 1 |
| | | } |
| | | html.push(`<ol>${listLines |
| | | .map((item) => `<li>${renderMarkdownInline(item.replace(/^\s*\d+\.\s+/, ''))}</li>`) |
| | | .join('')}</ol>`) |
| | | continue |
| | | } |
| | | |
| | | if (/^\s*>\s?/.test(line)) { |
| | | const quoteLines = [] |
| | | while (index < lines.length && /^\s*>\s?/.test(lines[index].trim())) { |
| | | quoteLines.push(lines[index].trim().replace(/^\s*>\s?/, '')) |
| | | index += 1 |
| | | } |
| | | html.push(`<blockquote>${quoteLines.map((item) => `<p>${renderMarkdownInline(item)}</p>`).join('')}</blockquote>`) |
| | | continue |
| | | } |
| | | |
| | | const paragraphLines = [] |
| | | while (index < lines.length) { |
| | | const current = lines[index].trim() |
| | | if ( |
| | | !current || |
| | | /^```/.test(current) || |
| | | /^\s*#{1,6}\s+/.test(current) || |
| | | /^\s*[-*]\s+/.test(current) || |
| | | /^\s*\d+\.\s+/.test(current) || |
| | | /^\s*>\s?/.test(current) |
| | | ) { |
| | | break |
| | | } |
| | | paragraphLines.push(current) |
| | | index += 1 |
| | | } |
| | | |
| | | if (paragraphLines.length) { |
| | | html.push(`<p>${paragraphLines.map((item) => renderMarkdownInline(item)).join('<br>')}</p>`) |
| | | } else { |
| | | index += 1 |
| | | } |
| | | } |
| | | |
| | | return html.join('') |
| | | } |
| | | |
| | | function normalizeMarkdownSource(content) { |
| | | return String(content || '') |
| | | .replace(/\r\n/g, '\n') |
| | | .replace(/<br\s*\/?>/gi, '\n') |
| | | .replace(/<\/p\s*>/gi, '\n\n') |
| | | .replace(/<p[^>]*>/gi, '') |
| | | .replace(/<\/div\s*>/gi, '\n') |
| | | .replace(/<div[^>]*>/gi, '') |
| | | .replace(/ /gi, ' ') |
| | | .replace(/\n{3,}/g, '\n\n') |
| | | .trim() |
| | | } |
| | | |
| | | function renderHeadingLine(line) { |
| | | const level = Math.min((line.match(/^(\s*#+)/)?.[0].trim().length || 1), 6) |
| | | const title = line.replace(/^\s*#{1,6}\s+/, '') |
| | | return `<h${level}>${renderMarkdownInline(title)}</h${level}>` |
| | | } |
| | | |
| | | function renderMarkdownInline(value) { |
| | | let result = escapeHtml(value) |
| | | result = result.replace(/`([^`]+)`/g, '<code>$1</code>') |
| | | result = result.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>') |
| | | result = result.replace(/\*([^*\n]+)\*/g, '<em>$1</em>') |
| | | result = result.replace( |
| | | /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, |
| | | '<a href="$2" target="_blank" rel="noreferrer noopener">$1</a>' |
| | | ) |
| | | return result |
| | | } |
| | | |
| | | function escapeHtml(value) { |
| | | return String(value) |
| | | .replace(/&/g, '&') |
| | | .replace(/</g, '<') |
| | | .replace(/>/g, '>') |
| | | .replace(/"/g, '"') |
| | | .replace(/'/g, ''') |
| | | } |
| | | </script> |
| | | |
| | | <style lang="scss"> |
| | | @use '@styles/core/md.scss'; |
| | | </style> |
| | | |
| | | <style scoped> |
| | | :deep(.assistant-markdown) { |
| | | color: var(--art-gray-800); |
| | | } |
| | | |
| | | :deep(.assistant-markdown > :first-child) { |
| | | margin-top: 0; |
| | | } |
| | | |
| | | :deep(.assistant-markdown > :last-child) { |
| | | margin-bottom: 0; |
| | | } |
| | | |
| | | :deep(.assistant-markdown p) { |
| | | margin-bottom: 0.6rem; |
| | | } |
| | | |
| | | :deep(.assistant-markdown ul), |
| | | :deep(.assistant-markdown ol) { |
| | | padding-left: 1.25rem; |
| | | } |
| | | |
| | | :deep(.assistant-markdown pre) { |
| | | overflow-x: auto; |
| | | border-radius: 14px; |
| | | background: var(--el-fill-color-light); |
| | | padding: 0.875rem 1rem; |
| | | } |
| | | |
| | | :deep(.assistant-markdown code) { |
| | | border-radius: 8px; |
| | | background: var(--el-fill-color-light); |
| | | padding: 0.1rem 0.35rem; |
| | | font-size: 0.875em; |
| | | } |
| | | |
| | | :deep(.assistant-markdown pre code) { |
| | | background: transparent; |
| | | padding: 0; |
| | | } |
| | | </style> |