diff --git a/frontend/app/components/ContactCollaborativeEditor.client.vue b/frontend/app/components/ContactCollaborativeEditor.client.vue
index af1ad41..c53eddb 100644
--- a/frontend/app/components/ContactCollaborativeEditor.client.vue
+++ b/frontend/app/components/ContactCollaborativeEditor.client.vue
@@ -13,6 +13,7 @@ const props = defineProps<{
room: string;
placeholder?: string;
plain?: boolean;
+ markdown?: boolean;
}>();
const emit = defineEmits<{
@@ -39,11 +40,158 @@ 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 `${label}`;
+ });
+
+ output = output.replace(/`([^`]+)`/g, "$1");
+ output = output.replace(/\*\*([^*]+)\*\*/g, "$1");
+ output = output.replace(/__([^_]+)__/g, "$1");
+ output = output.replace(/(^|[^*])\*([^*]+)\*(?!\*)/g, "$1$2");
+ output = output.replace(/(^|[^_])_([^_]+)_(?!_)/g, "$1$2");
+ output = output.replace(/~~([^~]+)~~/g, "$1");
+
+ 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(`
${paragraphLines.map((line) => parseInlineMarkdown(line)).join("
")}
`); + quoteLines.length = 0; + }; + + const flushList = () => { + if (!listType || !listItems.length) { + listType = null; + listItems.length = 0; + return; + } + const items = listItems.map((item) => `${quoteLines.map((line) => parseInlineMarkdown(line)).join("
")}
${escapeHtml(codeLines.join("\n"))}`);
+ 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(`