diff --git a/src/lib/components/common/RichTextInput.svelte b/src/lib/components/common/RichTextInput.svelte index 2e062dad19..580a8304c9 100644 --- a/src/lib/components/common/RichTextInput.svelte +++ b/src/lib/components/common/RichTextInput.svelte @@ -437,50 +437,39 @@ if (!editor || !editor.view) return; text = text.replaceAll('\n\n', '\n'); - // reset the editor content - editor.commands.clearContent(); - - const { state, view } = editor; - const { schema, tr } = state; - - // Build a paragraph node from a line of text, reconstructing any - // serialized mention syntax (e.g. <@model>, <$skill|Label>) into - // proper TipTap Mention nodes via DOMParser. - const toParagraph = (line: string) => { - if (!line) return schema.nodes.paragraph.create(); - if (/<[@#$][\w.\-:/]+(?:\|[^>]*)?>/.test(line)) { - const html = line.replace( - /<([@#$])([\w.\-:/]+)(?:\|([^>]*))?>/g, - (_, ch, id, label) => { - const display = label?.length ? label : id; - return `${ch}${display}`; - } ); - const el = document.createElement('p'); - el.innerHTML = html; - return DOMParser.fromSchema(schema).parse(el, { - topNode: schema.nodes.paragraph - }); - } - return schema.nodes.paragraph.create({}, schema.text(line)); - }; - - if (text.includes('\n')) { - // Multiple lines: make paragraphs - const lines = text.split('\n'); - const nodes = lines.map(toParagraph); - // Create a document fragment containing all parsed paragraphs - const fragment = Fragment.fromArray(nodes); - // Replace current selection with these paragraphs - tr.replaceSelectionWith(fragment, false /* don't select new */); - view.dispatch(tr); - } else if (text === '') { - // Empty: replace with empty paragraph using tr + if (text === '') { editor.commands.clearContent(); } else { - // Single line: create paragraph with text - tr.replaceSelectionWith(toParagraph(text), false); - view.dispatch(tr); + // Regex to find serialized mention tags: <@id>, <#id>, <$id|label> + const mentionReG = /<([@#$])([\w.\-:/]+)(?:\|([^>]*))?>/g; + + // Convert each line to a

, replacing mention tags with proper + // TipTap mention spans that the editor's DOMParser will recognise. + const lines = text.split('\n'); + const htmlContent = lines + .map((line) => { + if (!line) return '

'; + // Escape HTML entities in the line FIRST so we don't corrupt + // user text that happens to contain < or >, then re-inject + // the mention spans. + const escaped = line + .replace(/&/g, '&') + .replace(//g, '>'); + // Now replace the escaped mention patterns back into real spans + const withMentions = escaped.replace( + /<([@#$])([\w.\-:/]+)(?:\|([^&]*?))?>/g, + (_, ch, id, label) => { + const display = label?.length ? label : id; + return `${ch}${display}`; + } + ); + return `

${withMentions}

`; + }) + .join(''); + + editor.commands.setContent(htmlContent); } selectNextTemplate(editor.view.state, editor.view.dispatch);