1
This commit is contained in:
parent
cc6ce8729a
commit
bdaaa83bf6
@ -327,3 +327,98 @@ body::after {
|
||||
.score-bar-gradient-low {
|
||||
background: linear-gradient(90deg, #585860, #3a3a40);
|
||||
}
|
||||
|
||||
/* Markdown rendered inside AI responses */
|
||||
.prose h1,
|
||||
.prose h2,
|
||||
.prose h3,
|
||||
.prose h4 {
|
||||
color: var(--text-primary);
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.prose h1 {
|
||||
margin: 1rem 0 0.5rem;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.prose h2 {
|
||||
margin: 0.9rem 0 0.45rem;
|
||||
font-size: 0.98rem;
|
||||
}
|
||||
|
||||
.prose h3 {
|
||||
margin: 0.8rem 0 0.4rem;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.prose h4 {
|
||||
margin: 0.7rem 0 0.35rem;
|
||||
font-size: 0.86rem;
|
||||
}
|
||||
|
||||
.prose p {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.prose code {
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 6px;
|
||||
background: var(--surface-2);
|
||||
padding: 0.1rem 0.3rem;
|
||||
color: var(--accent-cyan);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.prose pre {
|
||||
overflow-x: auto;
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 12px;
|
||||
background: var(--surface-1);
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.prose pre code {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.markdown-table-wrap {
|
||||
margin: 0.75rem 0;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.markdown-table-wrap table {
|
||||
width: 100%;
|
||||
min-width: 520px;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.markdown-table-wrap th,
|
||||
.markdown-table-wrap td {
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
padding: 0.55rem 0.65rem;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.markdown-table-wrap th {
|
||||
background: var(--surface-2);
|
||||
color: var(--text-primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.markdown-table-wrap td {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.markdown-table-wrap tr:last-child td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
@ -1,34 +1,120 @@
|
||||
export function markdownToHtml(md: string): string {
|
||||
return md
|
||||
function escapeHtml(value: string): string {
|
||||
return value
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/^## (.+)$/gm, "<h2>$1</h2>")
|
||||
.replace(/^### (.+)$/gm, "<h3>$1</h3>")
|
||||
.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
|
||||
.replace(/^- (.+)$/gm, "<li>$1</li>")
|
||||
.replace(/(<li>.*<\/li>\n?)+/g, (m) => `<ul>${m}</ul>`)
|
||||
.replace(/\n{2,}/g, "</p><p>")
|
||||
.replace(/^(?!<[hulo])/gm, "<p>")
|
||||
.replace(/(?<![>])$/gm, "</p>")
|
||||
.replace(/<p><\/p>/g, "")
|
||||
.replace(/<p>(<h[23]>)/g, "$1")
|
||||
.replace(/(<\/h[23]>)<\/p>/g, "$1")
|
||||
.replace(/<p>(<ul>)/g, "$1")
|
||||
.replace(/(<\/ul>)<\/p>/g, "$1");
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function formatInline(value: string): string {
|
||||
return escapeHtml(value)
|
||||
.replace(/`([^`]+)`/g, "<code>$1</code>")
|
||||
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
||||
}
|
||||
|
||||
function isTableSeparator(line: string): boolean {
|
||||
const cells = line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|");
|
||||
return cells.length > 1 && cells.every((cell) => /^:?-{3,}:?$/.test(cell.trim()));
|
||||
}
|
||||
|
||||
function splitTableRow(line: string): string[] {
|
||||
return line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim());
|
||||
}
|
||||
|
||||
function renderTable(lines: string[]): string {
|
||||
const headers = splitTableRow(lines[0] ?? "");
|
||||
const rows = lines.slice(2).map(splitTableRow);
|
||||
const head = headers.map((cell) => `<th>${formatInline(cell)}</th>`).join("");
|
||||
const body = rows
|
||||
.map((row) => `<tr>${row.map((cell) => `<td>${formatInline(cell)}</td>`).join("")}</tr>`)
|
||||
.join("");
|
||||
return `<div class="markdown-table-wrap"><table><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table></div>`;
|
||||
}
|
||||
|
||||
function renderList(lines: string[]): string {
|
||||
const items = lines.map((line) => line.replace(/^\s*[-*]\s+/, ""));
|
||||
return `<ul>${items.map((item) => `<li>${formatInline(item)}</li>`).join("")}</ul>`;
|
||||
}
|
||||
|
||||
function renderParagraph(lines: string[]): string {
|
||||
return `<p>${lines.map((line) => formatInline(line.trim())).join("<br>")}</p>`;
|
||||
}
|
||||
|
||||
export function markdownToHtml(md: string): string {
|
||||
const lines = md.replace(/\r\n/g, "\n").split("\n");
|
||||
const blocks: string[] = [];
|
||||
let index = 0;
|
||||
|
||||
while (index < lines.length) {
|
||||
const line = lines[index] ?? "";
|
||||
const trimmed = line.trim();
|
||||
|
||||
if (!trimmed) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("```")) {
|
||||
const codeLines: string[] = [];
|
||||
index += 1;
|
||||
while (index < lines.length && !(lines[index] ?? "").trim().startsWith("```")) {
|
||||
codeLines.push(lines[index] ?? "");
|
||||
index += 1;
|
||||
}
|
||||
index += 1;
|
||||
blocks.push(`<pre><code>${escapeHtml(codeLines.join("\n"))}</code></pre>`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const heading = /^(#{1,4})\s+(.+)$/.exec(trimmed);
|
||||
if (heading) {
|
||||
const level = heading[1].length;
|
||||
blocks.push(`<h${level}>${formatInline(heading[2])}</h${level}>`);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (trimmed.includes("|") && index + 1 < lines.length && isTableSeparator(lines[index + 1] ?? "")) {
|
||||
const tableLines = [line, lines[index + 1] ?? ""];
|
||||
index += 2;
|
||||
while (index < lines.length && (lines[index] ?? "").trim().includes("|")) {
|
||||
tableLines.push(lines[index] ?? "");
|
||||
index += 1;
|
||||
}
|
||||
blocks.push(renderTable(tableLines));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/^\s*[-*]\s+/.test(line)) {
|
||||
const listLines = [line];
|
||||
index += 1;
|
||||
while (index < lines.length && /^\s*[-*]\s+/.test(lines[index] ?? "")) {
|
||||
listLines.push(lines[index] ?? "");
|
||||
index += 1;
|
||||
}
|
||||
blocks.push(renderList(listLines));
|
||||
continue;
|
||||
}
|
||||
|
||||
const paragraphLines = [line];
|
||||
index += 1;
|
||||
while (
|
||||
index < lines.length &&
|
||||
(lines[index] ?? "").trim() &&
|
||||
!/^(#{1,4})\s+/.test((lines[index] ?? "").trim()) &&
|
||||
!/^\s*[-*]\s+/.test(lines[index] ?? "") &&
|
||||
!((lines[index] ?? "").trim().includes("|") && index + 1 < lines.length && isTableSeparator(lines[index + 1] ?? ""))
|
||||
) {
|
||||
paragraphLines.push(lines[index] ?? "");
|
||||
index += 1;
|
||||
}
|
||||
blocks.push(renderParagraph(paragraphLines));
|
||||
}
|
||||
|
||||
return blocks.join("");
|
||||
}
|
||||
|
||||
export function formatMarkdown(text: string): string {
|
||||
let html = text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
|
||||
.replace(/^\s*[-*]\s+(.+)/gm, "<li>$1</li>")
|
||||
.replace(/\n/g, "<br>");
|
||||
// Wrap consecutive <li> items in <ul>
|
||||
html = html.replace(/(<li>.*?<\/li>(<br>)?)+/g, (match) => {
|
||||
return "<ul>" + match.replace(/<br>/g, "") + "</ul>";
|
||||
});
|
||||
return html;
|
||||
}
|
||||
return markdownToHtml(text);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user