|
437 | 437 | if (!editor || !editor.view) return; |
438 | 438 | text = text.replaceAll('\n\n', '\n'); |
439 | 439 |
|
440 | | - // reset the editor content |
441 | | - editor.commands.clearContent(); |
442 | | -
|
443 | | - const { state, view } = editor; |
444 | | - const { schema, tr } = state; |
445 | | -
|
446 | | - // Build a paragraph node from a line of text, reconstructing any |
447 | | - // serialized mention syntax (e.g. <@model>, <$skill|Label>) into |
448 | | - // proper TipTap Mention nodes via DOMParser. |
449 | | - const toParagraph = (line: string) => { |
450 | | - if (!line) return schema.nodes.paragraph.create(); |
451 | | - if (/<[@#$][\w.\-:/]+(?:\|[^>]*)?>/.test(line)) { |
452 | | - const html = line.replace( |
453 | | - /<([@#$])([\w.\-:/]+)(?:\|([^>]*))?>/g, |
454 | | - (_, ch, id, label) => { |
455 | | - const display = label?.length ? label : id; |
456 | | - return `<span class="mention" data-type="mention" data-id="${id}" data-mention-suggestion-char="${ch}">${ch}${display}</span>`; |
457 | | - } |
458 | 440 | ); |
459 | | - const el = document.createElement('p'); |
460 | | - el.innerHTML = html; |
461 | | - return DOMParser.fromSchema(schema).parse(el, { |
462 | | - topNode: schema.nodes.paragraph |
463 | | - }); |
464 | | - } |
465 | | - return schema.nodes.paragraph.create({}, schema.text(line)); |
466 | | - }; |
467 | | -
|
468 | | - if (text.includes('\n')) { |
469 | | - // Multiple lines: make paragraphs |
470 | | - const lines = text.split('\n'); |
471 | | - const nodes = lines.map(toParagraph); |
472 | | - // Create a document fragment containing all parsed paragraphs |
473 | | - const fragment = Fragment.fromArray(nodes); |
474 | | - // Replace current selection with these paragraphs |
475 | | - tr.replaceSelectionWith(fragment, false /* don't select new */); |
476 | | - view.dispatch(tr); |
477 | | - } else if (text === '') { |
478 | | - // Empty: replace with empty paragraph using tr |
| 441 | + if (text === '') { |
479 | 442 | editor.commands.clearContent(); |
480 | 443 | } else { |
481 | | - // Single line: create paragraph with text |
482 | | - tr.replaceSelectionWith(toParagraph(text), false); |
483 | | - view.dispatch(tr); |
| 444 | + // Regex to find serialized mention tags: <@id>, <#id>, <$id|label> |
| 445 | + const mentionReG = /<([@#$])([\w.\-:/]+)(?:\|([^>]*))?>/g; |
| 446 | +
|
| 447 | + // Convert each line to a <p>, replacing mention tags with proper |
| 448 | + // TipTap mention spans that the editor's DOMParser will recognise. |
| 449 | + const lines = text.split('\n'); |
| 450 | + const htmlContent = lines |
| 451 | + .map((line) => { |
| 452 | + if (!line) return '<p></p>'; |
| 453 | + // Escape HTML entities in the line FIRST so we don't corrupt |
| 454 | + // user text that happens to contain < or >, then re-inject |
| 455 | + // the mention spans. |
| 456 | + const escaped = line |
| 457 | + .replace(/&/g, '&') |
| 458 | + .replace(/</g, '<') |
| 459 | + .replace(/>/g, '>'); |
| 460 | + // Now replace the escaped mention patterns back into real spans |
| 461 | + const withMentions = escaped.replace( |
| 462 | + /<([@#$])([\w.\-:/]+)(?:\|([^&]*?))?>/g, |
| 463 | + (_, ch, id, label) => { |
| 464 | + const display = label?.length ? label : id; |
| 465 | + return `<span class="mention" data-type="mention" data-id="${id}" data-label="${display}" data-mention-suggestion-char="${ch}">${ch}${display}</span>`; |
| 466 | + } |
| 467 | + ); |
| 468 | + return `<p>${withMentions}</p>`; |
| 469 | + }) |
| 470 | + .join(''); |
| 471 | +
|
| 472 | + editor.commands.setContent(htmlContent); |
484 | 473 | } |
485 | 474 |
|
486 | 475 | selectNextTemplate(editor.view.state, editor.view.dispatch); |
|
0 commit comments