feat(documents): use toast-ui markdown rich editor
This commit is contained in:
@@ -13,7 +13,6 @@ const props = defineProps<{
|
||||
room: string;
|
||||
placeholder?: string;
|
||||
plain?: boolean;
|
||||
markdown?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -40,158 +39,11 @@ function escapeHtml(value: string) {
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
function parseInlineMarkdown(value: string) {
|
||||
let output = escapeHtml(value);
|
||||
|
||||
output = output.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, label: string, href: string) => {
|
||||
const rawHref = String(href ?? "").trim();
|
||||
const safeHref = /^(https?:\/\/|mailto:|tel:|\/)/i.test(rawHref) ? rawHref : "";
|
||||
if (!safeHref) return label;
|
||||
return `<a href="${escapeHtml(safeHref)}" target="_blank" rel="noopener noreferrer">${label}</a>`;
|
||||
});
|
||||
|
||||
output = output.replace(/`([^`]+)`/g, "<code>$1</code>");
|
||||
output = output.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
||||
output = output.replace(/__([^_]+)__/g, "<strong>$1</strong>");
|
||||
output = output.replace(/(^|[^*])\*([^*]+)\*(?!\*)/g, "$1<em>$2</em>");
|
||||
output = output.replace(/(^|[^_])_([^_]+)_(?!_)/g, "$1<em>$2</em>");
|
||||
output = output.replace(/~~([^~]+)~~/g, "<s>$1</s>");
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function markdownToHtml(value: string) {
|
||||
const lines = value.replaceAll("\r\n", "\n").split("\n");
|
||||
const chunks: string[] = [];
|
||||
const paragraphLines: string[] = [];
|
||||
const quoteLines: string[] = [];
|
||||
const listItems: string[] = [];
|
||||
let listType: "ul" | "ol" | null = null;
|
||||
let inCodeBlock = false;
|
||||
let codeLines: string[] = [];
|
||||
|
||||
const flushParagraph = () => {
|
||||
if (!paragraphLines.length) return;
|
||||
chunks.push(`<p>${paragraphLines.map((line) => parseInlineMarkdown(line)).join("<br />")}</p>`);
|
||||
paragraphLines.length = 0;
|
||||
};
|
||||
|
||||
const flushQuote = () => {
|
||||
if (!quoteLines.length) return;
|
||||
chunks.push(`<blockquote><p>${quoteLines.map((line) => parseInlineMarkdown(line)).join("<br />")}</p></blockquote>`);
|
||||
quoteLines.length = 0;
|
||||
};
|
||||
|
||||
const flushList = () => {
|
||||
if (!listType || !listItems.length) {
|
||||
listType = null;
|
||||
listItems.length = 0;
|
||||
return;
|
||||
}
|
||||
const items = listItems.map((item) => `<li>${parseInlineMarkdown(item)}</li>`).join("");
|
||||
chunks.push(`<${listType}>${items}</${listType}>`);
|
||||
listType = null;
|
||||
listItems.length = 0;
|
||||
};
|
||||
|
||||
const flushCode = () => {
|
||||
if (!inCodeBlock) return;
|
||||
chunks.push(`<pre><code>${escapeHtml(codeLines.join("\n"))}</code></pre>`);
|
||||
inCodeBlock = false;
|
||||
codeLines = [];
|
||||
};
|
||||
|
||||
for (const sourceLine of lines) {
|
||||
const trimmed = sourceLine.trim();
|
||||
|
||||
if (inCodeBlock) {
|
||||
if (trimmed.startsWith("```")) {
|
||||
flushCode();
|
||||
} else {
|
||||
codeLines.push(sourceLine);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("```")) {
|
||||
flushParagraph();
|
||||
flushQuote();
|
||||
flushList();
|
||||
inCodeBlock = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!trimmed) {
|
||||
flushParagraph();
|
||||
flushQuote();
|
||||
flushList();
|
||||
continue;
|
||||
}
|
||||
|
||||
const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
|
||||
if (headingMatch) {
|
||||
flushParagraph();
|
||||
flushQuote();
|
||||
flushList();
|
||||
const headingHashes = headingMatch[1] ?? "";
|
||||
const headingText = headingMatch[2] ?? "";
|
||||
const level = Math.min(6, Math.max(1, headingHashes.length || 1));
|
||||
chunks.push(`<h${level}>${parseInlineMarkdown(headingText)}</h${level}>`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/^(-{3,}|\*{3,}|_{3,})$/.test(trimmed)) {
|
||||
flushParagraph();
|
||||
flushQuote();
|
||||
flushList();
|
||||
chunks.push("<hr />");
|
||||
continue;
|
||||
}
|
||||
|
||||
const quoteMatch = sourceLine.match(/^\s*>\s?(.*)$/);
|
||||
if (quoteMatch) {
|
||||
flushParagraph();
|
||||
flushList();
|
||||
quoteLines.push(quoteMatch[1] ?? "");
|
||||
continue;
|
||||
}
|
||||
flushQuote();
|
||||
|
||||
const unorderedMatch = sourceLine.match(/^\s*[-*+]\s+(.+)$/);
|
||||
const orderedMatch = sourceLine.match(/^\s*\d+\.\s+(.+)$/);
|
||||
if (unorderedMatch || orderedMatch) {
|
||||
flushParagraph();
|
||||
const nextType: "ul" | "ol" = orderedMatch ? "ol" : "ul";
|
||||
if (listType && listType !== nextType) {
|
||||
flushList();
|
||||
}
|
||||
listType = nextType;
|
||||
listItems.push((orderedMatch?.[1] ?? unorderedMatch?.[1] ?? "").trim());
|
||||
continue;
|
||||
}
|
||||
|
||||
flushList();
|
||||
paragraphLines.push(sourceLine.trimEnd());
|
||||
}
|
||||
|
||||
flushParagraph();
|
||||
flushQuote();
|
||||
flushList();
|
||||
flushCode();
|
||||
|
||||
if (!chunks.length) return "<p></p>";
|
||||
return chunks.join("");
|
||||
}
|
||||
|
||||
function normalizeInitialContent(value: string) {
|
||||
const input = value.trim();
|
||||
if (!input) return "<p></p>";
|
||||
if (input.includes("<") && input.includes(">")) return value;
|
||||
|
||||
if (props.markdown) {
|
||||
return markdownToHtml(value);
|
||||
}
|
||||
|
||||
const blocks = value
|
||||
.replaceAll("\r\n", "\n")
|
||||
.split(/\n\n+/)
|
||||
|
||||
Reference in New Issue
Block a user