@bikdotai/bik-component-library 0.0.811-beta.0 → 0.0.811-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/cjs/editor/BikEditor.js +1 -1
  2. package/dist/cjs/editor/BikEditor.js.map +1 -1
  3. package/dist/cjs/editor/BikEditor.styles.js +22 -12
  4. package/dist/cjs/editor/BikEditor.styles.js.map +1 -1
  5. package/dist/cjs/editor/BikEditor.types.js.map +1 -1
  6. package/dist/cjs/editor/BikEditor.utils.js +1 -1
  7. package/dist/cjs/editor/BikEditor.utils.js.map +1 -1
  8. package/dist/cjs/editor/extensions/buildExtensions.js +1 -1
  9. package/dist/cjs/editor/extensions/buildExtensions.js.map +1 -1
  10. package/dist/cjs/editor/extensions/plainClipboard/PasteNormalizationExtension.js +1 -1
  11. package/dist/cjs/editor/extensions/plainClipboard/PasteNormalizationExtension.js.map +1 -1
  12. package/dist/cjs/editor/extensions/plainClipboard/pasteUtils.js +1 -1
  13. package/dist/cjs/editor/extensions/plainClipboard/pasteUtils.js.map +1 -1
  14. package/dist/cjs/src/editor/BikEditor.styles.d.ts +1 -1
  15. package/dist/cjs/src/editor/BikEditor.types.d.ts +0 -2
  16. package/dist/cjs/src/editor/BikEditor.utils.d.ts +14 -8
  17. package/dist/cjs/src/editor/extensions/buildExtensions.d.ts +1 -2
  18. package/dist/cjs/src/editor/extensions/plainClipboard/PasteNormalizationExtension.d.ts +22 -4
  19. package/dist/cjs/src/editor/extensions/plainClipboard/pasteUtils.d.ts +28 -16
  20. package/dist/esm/editor/BikEditor.js +1 -1
  21. package/dist/esm/editor/BikEditor.js.map +1 -1
  22. package/dist/esm/editor/BikEditor.styles.js +22 -12
  23. package/dist/esm/editor/BikEditor.styles.js.map +1 -1
  24. package/dist/esm/editor/BikEditor.types.js.map +1 -1
  25. package/dist/esm/editor/BikEditor.utils.js +1 -1
  26. package/dist/esm/editor/BikEditor.utils.js.map +1 -1
  27. package/dist/esm/editor/extensions/buildExtensions.js +1 -1
  28. package/dist/esm/editor/extensions/buildExtensions.js.map +1 -1
  29. package/dist/esm/editor/extensions/plainClipboard/PasteNormalizationExtension.js +1 -1
  30. package/dist/esm/editor/extensions/plainClipboard/PasteNormalizationExtension.js.map +1 -1
  31. package/dist/esm/editor/extensions/plainClipboard/pasteUtils.js +1 -1
  32. package/dist/esm/editor/extensions/plainClipboard/pasteUtils.js.map +1 -1
  33. package/dist/esm/src/editor/BikEditor.styles.d.ts +1 -1
  34. package/dist/esm/src/editor/BikEditor.types.d.ts +0 -2
  35. package/dist/esm/src/editor/BikEditor.utils.d.ts +14 -8
  36. package/dist/esm/src/editor/extensions/buildExtensions.d.ts +1 -2
  37. package/dist/esm/src/editor/extensions/plainClipboard/PasteNormalizationExtension.d.ts +22 -4
  38. package/dist/esm/src/editor/extensions/plainClipboard/pasteUtils.d.ts +28 -16
  39. package/package.json +2 -1
@@ -1 +1 @@
1
- {"version":3,"file":"BikEditor.utils.js","sources":["../../../src/editor/BikEditor.utils.ts"],"sourcesContent":["import { Editor } from '@tiptap/core';\nimport { DOMParser, DOMSerializer } from 'prosemirror-model';\nimport { EditorSnapshot, FormatState } from './BikEditor.types';\nimport { normalizeHardBreaks } from './extensions/plainClipboard/pasteUtils';\n\n// ---------------------------------------------------------------------------\n// HTML normalisation\n// ---------------------------------------------------------------------------\n\n/**\n * Normalise HTML before passing it to TipTap.\n *\n * 1. Strip all `<span>` tags first (keeps inner content). This exposes\n * bare `<br>` inside otherwise-empty paragraphs like Quill's\n * `<p><span class=\"ql-cursor\"><br></span></p>`.\n * 2. Then convert `<p><br></p>` → `<p></p>`. ProseMirror parses `<p></p>`\n * as an empty paragraph node and adds its own DOM `<br>` for cursor\n * positioning — one blank line, correct height. Leaving the `<br>`\n * would create a `hard_break` child AND the cursor `<br>`, producing\n * double-height blank lines.\n */\nexport function normalizeHtml(html: string): string {\n\treturn html\n\t\t.replace(/<\\/?span[^>]*>/gi, '')\n\t\t.replace(/(<p[^>]*>)\\s*<br\\s*\\/?>\\s*(<\\/p>)/gi, '$1$2');\n}\n\n/**\n * Make editor HTML portable across email clients and chat renderers.\n *\n * 1. Double a trailing `<br>` in content paragraphs so the blank line is\n * visible. A single trailing `<br>` before `</p>` is swallowed by most\n * email clients; `<br><br>` renders as a visible blank line (matches\n * Gmail's output).\n * 2. Convert empty `<p></p>` to `<p><br></p>` — email clients collapse\n * zero-height paragraphs without a `<br>`.\n */\nexport function toPortableHtml(html: string): string {\n\tconst div = document.createElement('div');\n\tdiv.innerHTML = html;\n\tconst blocks = div.querySelectorAll('p');\n\tfor (const p of Array.from(blocks)) {\n\t\tif (p.childNodes.length === 0) {\n\t\t\tp.appendChild(document.createElement('br'));\n\t\t\tcontinue;\n\t\t}\n\t\tconst last = p.lastChild;\n\t\tif (last instanceof HTMLBRElement && p.childNodes.length > 1) {\n\t\t\tp.appendChild(document.createElement('br'));\n\t\t}\n\t}\n\treturn div.innerHTML;\n}\n\n// ---------------------------------------------------------------------------\n// Inline content insertion\n// ---------------------------------------------------------------------------\n\n/**\n * Insert HTML at the current cursor position, merging the first paragraph\n * inline while preserving the block structure of subsequent paragraphs/lists.\n *\n * Uses `parseSlice` which returns an open-ended Slice — the first block merges\n * at the cursor, middle blocks stay intact, and the last block merges with any\n * text that follows the cursor.\n */\nexport function insertInlineHtml(editor: Editor, html: string): void {\n\tconst normalised = normalizeHtml(html);\n\tconst dom = document.createElement('div');\n\tdom.innerHTML = normalised;\n\tconst slice = DOMParser.fromSchema(editor.schema).parseSlice(dom);\n\n\tif (slice.size === 0) return;\n\n\tconst { from, to } = editor.state.selection;\n\tconst tr = editor.state.tr.replaceRange(from, to, slice);\n\teditor.view.dispatch(tr);\n}\n\n// ---------------------------------------------------------------------------\n// Section divider HTML\n// ---------------------------------------------------------------------------\n\nexport const SECTION_DIVIDER_HTML = (id: string) =>\n\t`<div data-section-divider=\"${id}\" style=\"display:none;height:0;line-height:0;overflow:hidden;\"></div>`;\n\n/**\n * Build initial HTML for an editor with one or more named sections below the body.\n * Each section is separated by an invisible divider node.\n *\n * @example\n * buildSectionedContent('<p>Body</p>', [\n * { id: 'signature', content: '<p>Alice</p>' },\n * { id: 'forwarded', content: '<p>--- fwd ---</p>' },\n * ])\n */\nexport function buildSectionedContent(\n\tbody: string,\n\tsections: Array<{ id: string; content: string }>,\n): string {\n\tlet html = body;\n\tfor (const s of sections) {\n\t\thtml += SECTION_DIVIDER_HTML(s.id) + s.content;\n\t}\n\treturn html;\n}\n\n// ---------------------------------------------------------------------------\n// Section extraction\n// ---------------------------------------------------------------------------\n\n/**\n * Parse the editor HTML into a Map of sectionId → raw HTML string.\n * The special key `'body'` always holds the content before the first divider.\n * Map iteration order matches document order.\n */\nexport function extractAllSections(editor: Editor): Map<string, string> {\n\treturn extractAllSectionsFromHtml(editor.getHTML());\n}\n\n/**\n * Extract just the body portion (everything before the first section divider)\n * from an HTML string produced by BikEditor. Safe to call on plain HTML too —\n * returns the full string when no dividers are present.\n */\nexport function getBodyHtml(html: string): string {\n\tif (!html) return '';\n\tconst sections = extractAllSectionsFromHtml(html);\n\treturn sections.get('body') ?? html;\n}\n\n/**\n * Extract everything from the first section divider onward (dividers + their\n * content). Returns an empty string when there are no sections.\n */\nexport function getSectionsHtml(html: string): string {\n\tif (!html) return '';\n\tconst body = getBodyHtml(html);\n\treturn html.substring(body.length);\n}\n\nexport function extractAllSectionsFromHtml(html: string): Map<string, string> {\n\tconst sections = new Map<string, string>();\n\tconst dividerRegex =\n\t\t/<div[^>]*data-section-divider=\"([^\"]*)[^>]*>\\s*<\\/div>/g;\n\tconst dividers: Array<{ id: string; index: number; endIndex: number }> = [];\n\n\tlet match: RegExpExecArray | null;\n\twhile ((match = dividerRegex.exec(html)) !== null) {\n\t\tdividers.push({\n\t\t\tid: match[1],\n\t\t\tindex: match.index,\n\t\t\tendIndex: match.index + match[0].length,\n\t\t});\n\t}\n\n\tif (dividers.length === 0) {\n\t\tsections.set('body', html);\n\t\treturn sections;\n\t}\n\n\tsections.set('body', html.slice(0, dividers[0].index));\n\tfor (let i = 0; i < dividers.length; i++) {\n\t\tconst start = dividers[i].endIndex;\n\t\tconst end = i + 1 < dividers.length ? dividers[i + 1].index : html.length;\n\t\tsections.set(dividers[i].id, html.slice(start, end));\n\t}\n\n\treturn sections;\n}\n\n/** Returns BikEditorContent for a single named section (or 'body'). */\nexport function extractSectionContent(\n\teditor: Editor,\n\tid: string,\n): EditorSnapshot {\n\tconst startPos = findSectionStartPos(editor, id);\n\tif (startPos === -1)\n\t\treturn { html: '', text: '', isEmpty: true, characterCount: 0 };\n\tconst endPos = findSectionEndPos(editor, id);\n\tconst slice = editor.state.doc.slice(startPos, endPos);\n\tconst container = document.createElement('div');\n\tconst serializer = DOMSerializer.fromSchema(editor.schema);\n\tcontainer.appendChild(serializer.serializeFragment(slice.content));\n\tconst html = toPortableHtml(container.innerHTML);\n\tconst text = container.textContent ?? '';\n\treturn { html, text, isEmpty: !text.trim(), characterCount: text.length };\n}\n\n/**\n * Parse an HTML string into ProseMirror nodes using the editor's schema.\n */\nfunction parseHtmlToNodes(editor: Editor, html: string) {\n\tconst container = document.createElement('div');\n\tcontainer.innerHTML = html;\n\treturn DOMParser.fromSchema(editor.schema).parse(container).content;\n}\n\n/**\n * Replace the content of a single named section without touching others.\n * Uses a ProseMirror transaction to replace only the target range,\n * preserving undo history and other sections' state.\n * If the section doesn't exist yet, it is appended at the end of the document.\n */\nexport function setSectionContentInEditor(\n\teditor: Editor,\n\tid: string,\n\thtml: string,\n): void {\n\tconst startPos = findSectionStartPos(editor, id);\n\tif (startPos === -1) {\n\t\tconst endPos = editor.state.doc.content.size;\n\t\tconst dividerHtml = SECTION_DIVIDER_HTML(id) + html;\n\t\teditor.commands.insertContentAt(endPos, dividerHtml, {\n\t\t\tupdateSelection: false,\n\t\t});\n\t\treturn;\n\t}\n\tconst endPos = findSectionEndPos(editor, id);\n\tconst fragment = parseHtmlToNodes(editor, html);\n\tconst { tr } = editor.state;\n\t// For body, replace from position 0 (before the first paragraph's opening\n\t// boundary) so ProseMirror swaps entire block nodes cleanly. Position 1\n\t// (inside the paragraph) would force ProseMirror to close the open paragraph\n\t// first, creating a ghost empty <p></p>.\n\tconst replaceFrom = id === 'body' ? 0 : startPos;\n\ttr.replaceWith(replaceFrom, endPos, fragment);\n\ttr.setMeta('addToHistory', false);\n\teditor.view.dispatch(tr);\n}\n\n// ---------------------------------------------------------------------------\n// Convenience shorthands (body = first section)\n// ---------------------------------------------------------------------------\n\n/** Extract BikEditorContent for just the body (before the first divider). */\nexport function extractBodyContent(editor: Editor): EditorSnapshot {\n\treturn extractSectionContent(editor, 'body');\n}\n\n// ---------------------------------------------------------------------------\n// Active formats\n// ---------------------------------------------------------------------------\n\nexport function extractActiveFormats(editor: Editor): FormatState {\n\treturn {\n\t\tbold: editor.isActive('bold'),\n\t\titalic: editor.isActive('italic'),\n\t\tunderline: editor.isActive('underline'),\n\t\tstrike: editor.isActive('strike'),\n\t\tbulletList: editor.isActive('bulletList'),\n\t\torderedList: editor.isActive('orderedList'),\n\t\tblockquote: editor.isActive('blockquote'),\n\t\tcodeBlock: editor.isActive('codeBlock'),\n\t\tlink: (() => {\n\t\t\tif (editor.isActive('link')) {\n\t\t\t\treturn { href: editor.getAttributes('link')['href'] ?? '' };\n\t\t\t}\n\t\t\tconst { $from } = editor.state.selection;\n\t\t\tif ($from.pos > 0) {\n\t\t\t\tconst before = $from.doc.resolve($from.pos);\n\t\t\t\tconst linkMark = before.marks().find((m) => m.type.name === 'link');\n\t\t\t\tif (!linkMark) {\n\t\t\t\t\tconst nodeBefore = $from.nodeBefore;\n\t\t\t\t\tconst markOnPrev = nodeBefore?.marks.find(\n\t\t\t\t\t\t(m) => m.type.name === 'link',\n\t\t\t\t\t);\n\t\t\t\t\tif (markOnPrev) {\n\t\t\t\t\t\treturn { href: markOnPrev.attrs['href'] ?? '' };\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\treturn { href: linkMark.attrs['href'] ?? '' };\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn null;\n\t\t})(),\n\t\ttextAlign: editor.isActive({ textAlign: 'center' })\n\t\t\t? 'center'\n\t\t\t: editor.isActive({ textAlign: 'right' })\n\t\t\t? 'right'\n\t\t\t: editor.isActive({ textAlign: 'justify' })\n\t\t\t? 'justify'\n\t\t\t: editor.isActive({ textAlign: 'left' })\n\t\t\t? 'left'\n\t\t\t: null,\n\t\tfontFamily: editor.getAttributes('textStyle')['fontFamily'] ?? null,\n\t\tfontSize: editor.getAttributes('textStyle')['fontSize'] ?? null,\n\t\tcolor: editor.getAttributes('textStyle')['color'] ?? null,\n\t\thighlight: editor.isActive('highlight')\n\t\t\t? editor.getAttributes('highlight')['color'] ?? 'default'\n\t\t\t: null,\n\t\tsuperscript: editor.isActive('superscript'),\n\t\tsubscript: editor.isActive('subscript'),\n\t};\n}\n\nexport function extractContent(editor: Editor): EditorSnapshot {\n\tconst html = toPortableHtml(editor.getHTML());\n\tconst normalized = normalizeHardBreaks(editor.state.doc.content);\n\tconst tempDoc = editor.schema.node('doc', null, normalized);\n\tconst text = tempDoc.textBetween(0, tempDoc.content.size, '\\n', '\\n');\n\treturn {\n\t\thtml,\n\t\ttext,\n\t\tisEmpty: editor.isEmpty,\n\t\tcharacterCount: text.length,\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Section position helpers (used by insertAtSectionStart/End and appendBodyContent)\n// ---------------------------------------------------------------------------\n\n/**\n * Returns the ProseMirror position immediately after a section's opening divider\n * (i.e. the start of the section's own content).\n * For `'body'`, returns 1 (beginning of the document).\n * Returns -1 if the named section's divider is not found.\n */\nexport function findSectionStartPos(editor: Editor, sectionId: string): number {\n\tif (sectionId === 'body') return 1;\n\tlet startPos = -1;\n\teditor.state.doc.descendants((node, pos) => {\n\t\tif (startPos !== -1) return false;\n\t\tif (\n\t\t\tnode.type.name === 'sectionDivider' &&\n\t\t\tnode.attrs['sectionId'] === sectionId\n\t\t) {\n\t\t\tstartPos = pos + node.nodeSize; // position right after the divider\n\t\t}\n\t});\n\treturn startPos;\n}\n\n/**\n * Returns the ProseMirror position at the end of a named section's content:\n * - For `'body'`: position of the first section divider, or end of document.\n * - For a named section: position of the next divider, or end of document.\n */\nexport function findSectionEndPos(editor: Editor, sectionId: string): number {\n\tconst doc = editor.state.doc;\n\tconst docSize = doc.content.size;\n\n\tif (sectionId === 'body') {\n\t\tlet firstDividerPos = -1;\n\t\tdoc.descendants((node, pos) => {\n\t\t\tif (firstDividerPos !== -1) return false;\n\t\t\tif (node.type.name === 'sectionDivider') {\n\t\t\t\tfirstDividerPos = pos;\n\t\t\t}\n\t\t});\n\t\treturn firstDividerPos !== -1 ? firstDividerPos : docSize;\n\t}\n\n\tlet passedSection = false;\n\tlet nextDividerPos = -1;\n\tdoc.descendants((node, pos) => {\n\t\tif (nextDividerPos !== -1) return false;\n\t\tif (!passedSection) {\n\t\t\tif (\n\t\t\t\tnode.type.name === 'sectionDivider' &&\n\t\t\t\tnode.attrs['sectionId'] === sectionId\n\t\t\t) {\n\t\t\t\tpassedSection = true;\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\t\tif (node.type.name === 'sectionDivider') {\n\t\t\tnextDividerPos = pos;\n\t\t}\n\t});\n\treturn nextDividerPos !== -1 ? nextDividerPos : docSize;\n}\n"],"names":["normalizeHtml","html","replace","toPortableHtml","div","document","createElement","innerHTML","blocks","querySelectorAll","p","Array","from","childNodes","length","appendChild","lastChild","HTMLBRElement","SECTION_DIVIDER_HTML","id","getBodyHtml","_a","extractAllSectionsFromHtml","get","sections","Map","dividerRegex","dividers","match","exec","push","index","endIndex","set","slice","i","start","end","extractSectionContent","editor","startPos","findSectionStartPos","text","isEmpty","characterCount","endPos","findSectionEndPos","state","doc","container","serializer","DOMSerializer","fromSchema","schema","serializeFragment","content","textContent","trim","sectionId","descendants","node","pos","type","name","attrs","nodeSize","docSize","size","firstDividerPos","passedSection","nextDividerPos","body","s","bold","isActive","italic","underline","strike","bulletList","orderedList","blockquote","codeBlock","link","href","getAttributes","$from","selection","linkMark","resolve","marks","find","m","_c","nodeBefore","markOnPrev","_b","textAlign","fontFamily","fontSize","color","highlight","_d","superscript","subscript","getHTML","normalized","normalizeHardBreaks","tempDoc","textBetween","substring","normalised","dom","DOMParser","parseSlice","to","tr","replaceRange","view","dispatch","dividerHtml","commands","insertContentAt","updateSelection","fragment","parse","parseHtmlToNodes","replaceFrom","replaceWith","setMeta"],"mappings":"4LAqBM,SAAUA,EAAcC,GAC7B,OAAOA,EACLC,QAAQ,mBAAoB,IAC5BA,QAAQ,sCAAuC,OAClD,CAYM,SAAUC,EAAeF,GAC9B,MAAMG,EAAMC,SAASC,cAAc,OACnCF,EAAIG,UAAYN,EAChB,MAAMO,EAASJ,EAAIK,iBAAiB,KACpC,IAAK,MAAMC,KAAKC,MAAMC,KAAKJ,GAAS,CACnC,GAA4B,IAAxBE,EAAEG,WAAWC,OAAc,CAC9BJ,EAAEK,YAAYV,SAASC,cAAc,OACrC,QACA,CACYI,EAAEM,qBACKC,eAAiBP,EAAEG,WAAWC,OAAS,GAC1DJ,EAAEK,YAAYV,SAASC,cAAc,MAEtC,CACD,OAAOF,EAAIG,SACZ,OA+BaW,EAAwBC,GACpC,8BAA8BA,yEAyCzB,SAAUC,EAAYnB,SAC3B,IAAKA,EAAM,MAAO,GAElB,OAA+B,QAAxBoB,EADUC,EAA2BrB,GAC5BsB,IAAI,eAAW,IAAAF,EAAAA,EAAApB,CAChC,CAYM,SAAUqB,EAA2BrB,GAC1C,MAAMuB,EAAW,IAAIC,IACfC,EACL,0DACKC,EAAmE,GAEzE,IAAIC,EACJ,KAA6C,QAArCA,EAAQF,EAAaG,KAAK5B,KACjC0B,EAASG,KAAK,CACbX,GAAIS,EAAM,GACVG,MAAOH,EAAMG,MACbC,SAAUJ,EAAMG,MAAQH,EAAM,GAAGd,SAInC,GAAwB,IAApBa,EAASb,OAEZ,OADAU,EAASS,IAAI,OAAQhC,GACduB,EAGRA,EAASS,IAAI,OAAQhC,EAAKiC,MAAM,EAAGP,EAAS,GAAGI,QAC/C,IAAK,IAAII,EAAI,EAAGA,EAAIR,EAASb,OAAQqB,IAAK,CACzC,MAAMC,EAAQT,EAASQ,GAAGH,SACpBK,EAAMF,EAAI,EAAIR,EAASb,OAASa,EAASQ,EAAI,GAAGJ,MAAQ9B,EAAKa,OACnEU,EAASS,IAAIN,EAASQ,GAAGhB,GAAIlB,EAAKiC,MAAME,EAAOC,GAC/C,CAED,OAAOb,CACR,CAGgB,SAAAc,EACfC,EACApB,SAEA,MAAMqB,EAAWC,EAAoBF,EAAQpB,GAC7C,IAAkB,IAAdqB,EACH,MAAO,CAAEvC,KAAM,GAAIyC,KAAM,GAAIC,SAAS,EAAMC,eAAgB,GAC7D,MAAMC,EAASC,EAAkBP,EAAQpB,GACnCe,EAAQK,EAAOQ,MAAMC,IAAId,MAAMM,EAAUK,GACzCI,EAAY5C,SAASC,cAAc,OACnC4C,EAAaC,EAAaA,cAACC,WAAWb,EAAOc,QACnDJ,EAAUlC,YAAYmC,EAAWI,kBAAkBpB,EAAMqB,UACzD,MAAMtD,EAAOE,EAAe8C,EAAU1C,WAChCmC,EAA4B,QAArBrB,EAAA4B,EAAUO,mBAAW,IAAAnC,EAAAA,EAAI,GACtC,MAAO,CAAEpB,OAAMyC,OAAMC,SAAUD,EAAKe,OAAQb,eAAgBF,EAAK5B,OAClE,CAoIgB,SAAA2B,EAAoBF,EAAgBmB,GACnD,GAAkB,SAAdA,EAAsB,OAAO,EACjC,IAAIlB,GAAY,EAUhB,OATAD,EAAOQ,MAAMC,IAAIW,aAAY,CAACC,EAAMC,KACnC,IAAkB,IAAdrB,EAAiB,OAAO,EAER,mBAAnBoB,EAAKE,KAAKC,MACVH,EAAKI,MAAiB,YAAMN,IAE5BlB,EAAWqB,EAAMD,EAAKK,SACtB,IAEKzB,CACR,CAOgB,SAAAM,EAAkBP,EAAgBmB,GACjD,MAAMV,EAAMT,EAAOQ,MAAMC,IACnBkB,EAAUlB,EAAIO,QAAQY,KAE5B,GAAkB,SAAdT,EAAsB,CACzB,IAAIU,GAAmB,EAOvB,OANApB,EAAIW,aAAY,CAACC,EAAMC,KACtB,IAAyB,IAArBO,EAAwB,OAAO,EACZ,mBAAnBR,EAAKE,KAAKC,OACbK,EAAkBP,EAClB,KAE0B,IAArBO,EAAyBA,EAAkBF,CAClD,CAED,IAAIG,GAAgB,EAChBC,GAAkB,EAgBtB,OAfAtB,EAAIW,aAAY,CAACC,EAAMC,KACE,IAApBS,IACCD,OASkB,mBAAnBT,EAAKE,KAAKC,OACbO,EAAiBT,KARG,mBAAnBD,EAAKE,KAAKC,MACVH,EAAKI,MAAiB,YAAMN,IAE5BW,GAAgB,IAEV,OAMkB,IAApBC,EAAwBA,EAAiBJ,CACjD,8DApRgB,SACfK,EACA/C,GAEA,IAAIvB,EAAOsE,EACX,IAAK,MAAMC,KAAKhD,EACfvB,GAAQiB,EAAqBsD,EAAErD,IAAMqD,EAAEjB,QAExC,OAAOtD,CACR,+BA2IM,SAA+BsC,eACpC,MAAO,CACNkC,KAAMlC,EAAOmC,SAAS,QACtBC,OAAQpC,EAAOmC,SAAS,UACxBE,UAAWrC,EAAOmC,SAAS,aAC3BG,OAAQtC,EAAOmC,SAAS,UACxBI,WAAYvC,EAAOmC,SAAS,cAC5BK,YAAaxC,EAAOmC,SAAS,eAC7BM,WAAYzC,EAAOmC,SAAS,cAC5BO,UAAW1C,EAAOmC,SAAS,aAC3BQ,KAAM,gBACL,GAAI3C,EAAOmC,SAAS,QACnB,MAAO,CAAES,KAA8C,QAAxC9D,EAAAkB,EAAO6C,cAAc,QAAc,YAAK,IAAA/D,EAAAA,EAAA,IAExD,MAAMgE,MAAEA,GAAU9C,EAAOQ,MAAMuC,UAC/B,GAAID,EAAMxB,IAAM,EAAG,CAClB,MACM0B,EADSF,EAAMrC,IAAIwC,QAAQH,EAAMxB,KACf4B,QAAQC,MAAMC,GAAsB,SAAhBA,EAAE7B,KAAKC,OACnD,GAAKwB,EASJ,MAAO,CAAEJ,KAAgC,QAA1BS,EAAAL,EAASvB,MAAY,YAAK,IAAA4B,EAAAA,EAAA,IAT3B,CACd,MAAMC,EAAaR,EAAMQ,WACnBC,EAAaD,eAAAA,EAAYJ,MAAMC,MACnCC,GAAsB,SAAhBA,EAAE7B,KAAKC,OAEf,GAAI+B,EACH,MAAO,CAAEX,KAAkC,QAA5BY,EAAAD,EAAW9B,MAAY,YAAK,IAAA+B,EAAAA,EAAA,GAE5C,CAGD,CACD,OAAO,IACP,EArBK,GAsBNC,UAAWzD,EAAOmC,SAAS,CAAEsB,UAAW,WACrC,SACAzD,EAAOmC,SAAS,CAAEsB,UAAW,UAC7B,QACAzD,EAAOmC,SAAS,CAAEsB,UAAW,YAC7B,UACAzD,EAAOmC,SAAS,CAAEsB,UAAW,SAC7B,OACA,KACHC,WAA2D,QAA/C5E,EAAAkB,EAAO6C,cAAc,aAAyB,kBAAC,IAAA/D,EAAAA,EAAI,KAC/D6E,SAAuD,QAA7CH,EAAAxD,EAAO6C,cAAc,aAAuB,gBAAC,IAAAW,EAAAA,EAAI,KAC3DI,MAAiD,QAA1CP,EAAArD,EAAO6C,cAAc,aAAoB,aAAC,IAAAQ,EAAAA,EAAI,KACrDQ,UAAW7D,EAAOmC,SAAS,aACkB,QAA1C2B,EAAA9D,EAAO6C,cAAc,aAAoB,aAAC,IAAAiB,EAAAA,EAAI,UAC9C,KACHC,YAAa/D,EAAOmC,SAAS,eAC7B6B,UAAWhE,EAAOmC,SAAS,aAE7B,kEA1DM,SAA6BnC,GAClC,OAAOD,EAAsBC,EAAQ,OACtC,yBA0DM,SAAyBA,GAC9B,MAAMtC,EAAOE,EAAeoC,EAAOiE,WAC7BC,EAAaC,EAAAA,oBAAoBnE,EAAOQ,MAAMC,IAAIO,SAClDoD,EAAUpE,EAAOc,OAAOO,KAAK,MAAO,KAAM6C,GAC1C/D,EAAOiE,EAAQC,YAAY,EAAGD,EAAQpD,QAAQY,KAAM,KAAM,MAChE,MAAO,CACNlE,OACAyC,OACAC,QAASJ,EAAOI,QAChBC,eAAgBF,EAAK5B,OAEvB,0IA5KM,SAA0Bb,GAC/B,IAAKA,EAAM,MAAO,GAClB,MAAMsE,EAAOnD,EAAYnB,GACzB,OAAOA,EAAK4G,UAAUtC,EAAKzD,OAC5B,2BAzEgB,SAAiByB,EAAgBtC,GAChD,MAAM6G,EAAa9G,EAAcC,GAC3B8G,EAAM1G,SAASC,cAAc,OACnCyG,EAAIxG,UAAYuG,EAChB,MAAM5E,EAAQ8E,EAASA,UAAC5D,WAAWb,EAAOc,QAAQ4D,WAAWF,GAE7D,GAAmB,IAAf7E,EAAMiC,KAAY,OAEtB,MAAMvD,KAAEA,EAAIsG,GAAEA,GAAO3E,EAAOQ,MAAMuC,UAC5B6B,EAAK5E,EAAOQ,MAAMoE,GAAGC,aAAaxG,EAAMsG,EAAIhF,GAClDK,EAAO8E,KAAKC,SAASH,EACtB,qEAgIC5E,EACApB,EACAlB,GAEA,MAAMuC,EAAWC,EAAoBF,EAAQpB,GAC7C,IAAkB,IAAdqB,EAAiB,CACpB,MAAMK,EAASN,EAAOQ,MAAMC,IAAIO,QAAQY,KAClCoD,EAAcrG,EAAqBC,GAAMlB,EAI/C,YAHAsC,EAAOiF,SAASC,gBAAgB5E,EAAQ0E,EAAa,CACpDG,iBAAiB,GAGlB,CACD,MAAM7E,EAASC,EAAkBP,EAAQpB,GACnCwG,EA3BP,SAA0BpF,EAAgBtC,GACzC,MAAMgD,EAAY5C,SAASC,cAAc,OAEzC,OADA2C,EAAU1C,UAAYN,EACf+G,EAASA,UAAC5D,WAAWb,EAAOc,QAAQuE,MAAM3E,GAAWM,OAC7D,CAuBkBsE,CAAiBtF,EAAQtC,IACpCkH,GAAEA,GAAO5E,EAAOQ,MAKhB+E,EAAqB,SAAP3G,EAAgB,EAAIqB,EACxC2E,EAAGY,YAAYD,EAAajF,EAAQ8E,GACpCR,EAAGa,QAAQ,gBAAgB,GAC3BzF,EAAO8E,KAAKC,SAASH,EACtB"}
1
+ {"version":3,"file":"BikEditor.utils.js","sources":["../../../src/editor/BikEditor.utils.ts"],"sourcesContent":["import { Editor } from '@tiptap/core';\nimport { DOMParser, DOMSerializer } from 'prosemirror-model';\nimport { EditorSnapshot, FormatState } from './BikEditor.types';\nimport { normalizeHardBreaks } from './extensions/plainClipboard/pasteUtils';\n\n// ---------------------------------------------------------------------------\n// HTML normalisation\n// ---------------------------------------------------------------------------\n\n/**\n * Normalise HTML before passing it to TipTap.\n *\n * 1. Unwrap style-less `<span>` wrappers (plain spans, and editor-cursor spans\n * like Quill's `<span class=\"ql-cursor\">`). Spans that carry inline styling\n * (`color` / `font-family` / `font-size` render as styled spans) or that\n * represent a `{{variable}}` token are PRESERVED — otherwise a setContent\n * round-trip would silently drop colour/font formatting that paste keeps.\n * 2. Collapse placeholder paragraphs (`<p><br></p>`) to `<p></p>`. ProseMirror\n * parses `<p></p>` as an empty paragraph and adds its own DOM `<br>` for the\n * cursor — one blank line, correct height. Leaving the source `<br>` would\n * create a `hard_break` child AND the cursor `<br>` → double-height blank line.\n *\n * Uses the DOM (regex cannot selectively pair span tags). `normalizeHtml` runs\n * in the component body for `initialContent`, so it is guarded for SSR: the\n * editor is created lazily on the client (`immediatelyRender: false`), meaning\n * the value is only parsed client-side and passing through on the server is safe.\n */\nexport function normalizeHtml(html: string): string {\n\tif (!html || typeof document === 'undefined') return html;\n\n\tconst container = document.createElement('div');\n\tcontainer.innerHTML = html;\n\n\tcontainer.querySelectorAll('span').forEach((span) => {\n\t\tif (span.hasAttribute('style') || span.hasAttribute('data-variable'))\n\t\t\treturn;\n\t\tconst parent = span.parentNode;\n\t\tif (!parent) return;\n\t\twhile (span.firstChild) parent.insertBefore(span.firstChild, span);\n\t\tparent.removeChild(span);\n\t});\n\n\tcontainer.querySelectorAll('p').forEach((p) => {\n\t\tif (\n\t\t\tp.querySelectorAll('br').length === 1 &&\n\t\t\t(p.textContent ?? '').trim() === ''\n\t\t) {\n\t\t\tp.innerHTML = '';\n\t\t}\n\t});\n\n\treturn container.innerHTML;\n}\n\n/**\n * Make editor HTML portable across email clients and chat renderers.\n *\n * 1. Double a trailing `<br>` in content paragraphs so the blank line is\n * visible. A single trailing `<br>` before `</p>` is swallowed by most\n * email clients; `<br><br>` renders as a visible blank line (matches\n * Gmail's output).\n * 2. Convert empty `<p></p>` to `<p><br></p>` — email clients collapse\n * zero-height paragraphs without a `<br>`.\n */\nexport function toPortableHtml(html: string): string {\n\tconst div = document.createElement('div');\n\tdiv.innerHTML = html;\n\tconst blocks = div.querySelectorAll('p');\n\tfor (const p of Array.from(blocks)) {\n\t\tif (p.childNodes.length === 0) {\n\t\t\tp.appendChild(document.createElement('br'));\n\t\t\tcontinue;\n\t\t}\n\t\tconst last = p.lastChild;\n\t\tif (last instanceof HTMLBRElement && p.childNodes.length > 1) {\n\t\t\tp.appendChild(document.createElement('br'));\n\t\t}\n\t}\n\treturn div.innerHTML;\n}\n\n// ---------------------------------------------------------------------------\n// Inline content insertion\n// ---------------------------------------------------------------------------\n\n/**\n * Insert HTML at the current cursor position, merging the first paragraph\n * inline while preserving the block structure of subsequent paragraphs/lists.\n *\n * Uses `parseSlice` which returns an open-ended Slice — the first block merges\n * at the cursor, middle blocks stay intact, and the last block merges with any\n * text that follows the cursor.\n */\nexport function insertInlineHtml(editor: Editor, html: string): void {\n\tconst normalised = normalizeHtml(html);\n\tconst dom = document.createElement('div');\n\tdom.innerHTML = normalised;\n\tconst slice = DOMParser.fromSchema(editor.schema).parseSlice(dom);\n\n\tif (slice.size === 0) return;\n\n\tconst { from, to } = editor.state.selection;\n\tconst tr = editor.state.tr.replaceRange(from, to, slice);\n\teditor.view.dispatch(tr);\n}\n\n// ---------------------------------------------------------------------------\n// Section divider HTML\n// ---------------------------------------------------------------------------\n\nexport const SECTION_DIVIDER_HTML = (id: string) =>\n\t`<div data-section-divider=\"${id}\" style=\"display:none;height:0;line-height:0;overflow:hidden;\"></div>`;\n\n/**\n * Build initial HTML for an editor with one or more named sections below the body.\n * Each section is separated by an invisible divider node.\n *\n * @example\n * buildSectionedContent('<p>Body</p>', [\n * { id: 'signature', content: '<p>Alice</p>' },\n * { id: 'forwarded', content: '<p>--- fwd ---</p>' },\n * ])\n */\nexport function buildSectionedContent(\n\tbody: string,\n\tsections: Array<{ id: string; content: string }>,\n): string {\n\tlet html = body;\n\tfor (const s of sections) {\n\t\thtml += SECTION_DIVIDER_HTML(s.id) + s.content;\n\t}\n\treturn html;\n}\n\n// ---------------------------------------------------------------------------\n// Section extraction\n// ---------------------------------------------------------------------------\n\n/**\n * Parse the editor HTML into a Map of sectionId → raw HTML string.\n * The special key `'body'` always holds the content before the first divider.\n * Map iteration order matches document order.\n */\nexport function extractAllSections(editor: Editor): Map<string, string> {\n\treturn extractAllSectionsFromHtml(editor.getHTML());\n}\n\n/**\n * Extract just the body portion (everything before the first section divider)\n * from an HTML string produced by BikEditor. Safe to call on plain HTML too —\n * returns the full string when no dividers are present.\n */\nexport function getBodyHtml(html: string): string {\n\tif (!html) return '';\n\tconst sections = extractAllSectionsFromHtml(html);\n\treturn sections.get('body') ?? html;\n}\n\n/**\n * Extract everything from the first section divider onward (dividers + their\n * content). Returns an empty string when there are no sections.\n */\nexport function getSectionsHtml(html: string): string {\n\tif (!html) return '';\n\tconst body = getBodyHtml(html);\n\treturn html.substring(body.length);\n}\n\nexport function extractAllSectionsFromHtml(html: string): Map<string, string> {\n\tconst sections = new Map<string, string>();\n\tconst dividerRegex =\n\t\t/<div[^>]*data-section-divider=\"([^\"]*)[^>]*>\\s*<\\/div>/g;\n\tconst dividers: Array<{ id: string; index: number; endIndex: number }> = [];\n\n\tlet match: RegExpExecArray | null;\n\twhile ((match = dividerRegex.exec(html)) !== null) {\n\t\tdividers.push({\n\t\t\tid: match[1],\n\t\t\tindex: match.index,\n\t\t\tendIndex: match.index + match[0].length,\n\t\t});\n\t}\n\n\tif (dividers.length === 0) {\n\t\tsections.set('body', html);\n\t\treturn sections;\n\t}\n\n\tsections.set('body', html.slice(0, dividers[0].index));\n\tfor (let i = 0; i < dividers.length; i++) {\n\t\tconst start = dividers[i].endIndex;\n\t\tconst end = i + 1 < dividers.length ? dividers[i + 1].index : html.length;\n\t\tsections.set(dividers[i].id, html.slice(start, end));\n\t}\n\n\treturn sections;\n}\n\n/** Returns BikEditorContent for a single named section (or 'body'). */\nexport function extractSectionContent(\n\teditor: Editor,\n\tid: string,\n): EditorSnapshot {\n\tconst startPos = findSectionStartPos(editor, id);\n\tif (startPos === -1)\n\t\treturn { html: '', text: '', isEmpty: true, characterCount: 0 };\n\tconst endPos = findSectionEndPos(editor, id);\n\tconst slice = editor.state.doc.slice(startPos, endPos);\n\tconst container = document.createElement('div');\n\tconst serializer = DOMSerializer.fromSchema(editor.schema);\n\tcontainer.appendChild(serializer.serializeFragment(slice.content));\n\tconst html = toPortableHtml(container.innerHTML);\n\tconst text = container.textContent ?? '';\n\treturn { html, text, isEmpty: !text.trim(), characterCount: text.length };\n}\n\n/**\n * Parse an HTML string into ProseMirror nodes using the editor's schema.\n */\nfunction parseHtmlToNodes(editor: Editor, html: string) {\n\tconst container = document.createElement('div');\n\tcontainer.innerHTML = html;\n\treturn DOMParser.fromSchema(editor.schema).parse(container).content;\n}\n\n/**\n * Replace the content of a single named section without touching others.\n * Uses a ProseMirror transaction to replace only the target range,\n * preserving undo history and other sections' state.\n * If the section doesn't exist yet, it is appended at the end of the document.\n */\nexport function setSectionContentInEditor(\n\teditor: Editor,\n\tid: string,\n\thtml: string,\n): void {\n\tconst startPos = findSectionStartPos(editor, id);\n\tif (startPos === -1) {\n\t\tconst endPos = editor.state.doc.content.size;\n\t\tconst dividerHtml = SECTION_DIVIDER_HTML(id) + html;\n\t\teditor.commands.insertContentAt(endPos, dividerHtml, {\n\t\t\tupdateSelection: false,\n\t\t});\n\t\treturn;\n\t}\n\tconst endPos = findSectionEndPos(editor, id);\n\tconst fragment = parseHtmlToNodes(editor, html);\n\tconst { tr } = editor.state;\n\t// For body, replace from position 0 (before the first paragraph's opening\n\t// boundary) so ProseMirror swaps entire block nodes cleanly. Position 1\n\t// (inside the paragraph) would force ProseMirror to close the open paragraph\n\t// first, creating a ghost empty <p></p>.\n\tconst replaceFrom = id === 'body' ? 0 : startPos;\n\ttr.replaceWith(replaceFrom, endPos, fragment);\n\ttr.setMeta('addToHistory', false);\n\teditor.view.dispatch(tr);\n}\n\n// ---------------------------------------------------------------------------\n// Convenience shorthands (body = first section)\n// ---------------------------------------------------------------------------\n\n/** Extract BikEditorContent for just the body (before the first divider). */\nexport function extractBodyContent(editor: Editor): EditorSnapshot {\n\treturn extractSectionContent(editor, 'body');\n}\n\n// ---------------------------------------------------------------------------\n// Active formats\n// ---------------------------------------------------------------------------\n\nexport function extractActiveFormats(editor: Editor): FormatState {\n\treturn {\n\t\tbold: editor.isActive('bold'),\n\t\titalic: editor.isActive('italic'),\n\t\tunderline: editor.isActive('underline'),\n\t\tstrike: editor.isActive('strike'),\n\t\tbulletList: editor.isActive('bulletList'),\n\t\torderedList: editor.isActive('orderedList'),\n\t\tblockquote: editor.isActive('blockquote'),\n\t\tcodeBlock: editor.isActive('codeBlock'),\n\t\tlink: (() => {\n\t\t\tif (editor.isActive('link')) {\n\t\t\t\treturn { href: editor.getAttributes('link')['href'] ?? '' };\n\t\t\t}\n\t\t\tconst { $from } = editor.state.selection;\n\t\t\tif ($from.pos > 0) {\n\t\t\t\tconst before = $from.doc.resolve($from.pos);\n\t\t\t\tconst linkMark = before.marks().find((m) => m.type.name === 'link');\n\t\t\t\tif (!linkMark) {\n\t\t\t\t\tconst nodeBefore = $from.nodeBefore;\n\t\t\t\t\tconst markOnPrev = nodeBefore?.marks.find(\n\t\t\t\t\t\t(m) => m.type.name === 'link',\n\t\t\t\t\t);\n\t\t\t\t\tif (markOnPrev) {\n\t\t\t\t\t\treturn { href: markOnPrev.attrs['href'] ?? '' };\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\treturn { href: linkMark.attrs['href'] ?? '' };\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn null;\n\t\t})(),\n\t\ttextAlign: editor.isActive({ textAlign: 'center' })\n\t\t\t? 'center'\n\t\t\t: editor.isActive({ textAlign: 'right' })\n\t\t\t? 'right'\n\t\t\t: editor.isActive({ textAlign: 'justify' })\n\t\t\t? 'justify'\n\t\t\t: editor.isActive({ textAlign: 'left' })\n\t\t\t? 'left'\n\t\t\t: null,\n\t\tfontFamily: editor.getAttributes('textStyle')['fontFamily'] ?? null,\n\t\tfontSize: editor.getAttributes('textStyle')['fontSize'] ?? null,\n\t\tcolor: editor.getAttributes('textStyle')['color'] ?? null,\n\t\thighlight: editor.isActive('highlight')\n\t\t\t? editor.getAttributes('highlight')['color'] ?? 'default'\n\t\t\t: null,\n\t\tsuperscript: editor.isActive('superscript'),\n\t\tsubscript: editor.isActive('subscript'),\n\t};\n}\n\nexport function extractContent(editor: Editor): EditorSnapshot {\n\tconst html = toPortableHtml(editor.getHTML());\n\tconst normalized = normalizeHardBreaks(editor.state.doc.content);\n\tconst tempDoc = editor.schema.node('doc', null, normalized);\n\t// Paragraph boundary → blank line (\\n\\n), soft line break (hardBreak) → \\n.\n\t// Matches Gmail's plain-text output for the same content.\n\tconst text = tempDoc.textBetween(0, tempDoc.content.size, '\\n\\n', '\\n');\n\treturn {\n\t\thtml,\n\t\ttext,\n\t\tisEmpty: editor.isEmpty,\n\t\tcharacterCount: text.length,\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Section position helpers (used by insertAtSectionStart/End and appendBodyContent)\n// ---------------------------------------------------------------------------\n\n/**\n * Returns the ProseMirror position immediately after a section's opening divider\n * (i.e. the start of the section's own content).\n * For `'body'`, returns 1 (beginning of the document).\n * Returns -1 if the named section's divider is not found.\n */\nexport function findSectionStartPos(editor: Editor, sectionId: string): number {\n\tif (sectionId === 'body') return 1;\n\tlet startPos = -1;\n\teditor.state.doc.descendants((node, pos) => {\n\t\tif (startPos !== -1) return false;\n\t\tif (\n\t\t\tnode.type.name === 'sectionDivider' &&\n\t\t\tnode.attrs['sectionId'] === sectionId\n\t\t) {\n\t\t\tstartPos = pos + node.nodeSize; // position right after the divider\n\t\t}\n\t});\n\treturn startPos;\n}\n\n/**\n * Returns the ProseMirror position at the end of a named section's content:\n * - For `'body'`: position of the first section divider, or end of document.\n * - For a named section: position of the next divider, or end of document.\n */\nexport function findSectionEndPos(editor: Editor, sectionId: string): number {\n\tconst doc = editor.state.doc;\n\tconst docSize = doc.content.size;\n\n\tif (sectionId === 'body') {\n\t\tlet firstDividerPos = -1;\n\t\tdoc.descendants((node, pos) => {\n\t\t\tif (firstDividerPos !== -1) return false;\n\t\t\tif (node.type.name === 'sectionDivider') {\n\t\t\t\tfirstDividerPos = pos;\n\t\t\t}\n\t\t});\n\t\treturn firstDividerPos !== -1 ? firstDividerPos : docSize;\n\t}\n\n\tlet passedSection = false;\n\tlet nextDividerPos = -1;\n\tdoc.descendants((node, pos) => {\n\t\tif (nextDividerPos !== -1) return false;\n\t\tif (!passedSection) {\n\t\t\tif (\n\t\t\t\tnode.type.name === 'sectionDivider' &&\n\t\t\t\tnode.attrs['sectionId'] === sectionId\n\t\t\t) {\n\t\t\t\tpassedSection = true;\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\t\tif (node.type.name === 'sectionDivider') {\n\t\t\tnextDividerPos = pos;\n\t\t}\n\t});\n\treturn nextDividerPos !== -1 ? nextDividerPos : docSize;\n}\n"],"names":["normalizeHtml","html","document","container","createElement","innerHTML","querySelectorAll","forEach","span","hasAttribute","parent","parentNode","firstChild","insertBefore","removeChild","p","length","_a","textContent","trim","toPortableHtml","div","blocks","Array","from","childNodes","appendChild","lastChild","HTMLBRElement","SECTION_DIVIDER_HTML","id","getBodyHtml","extractAllSectionsFromHtml","get","sections","Map","dividerRegex","dividers","match","exec","push","index","endIndex","set","slice","i","start","end","extractSectionContent","editor","startPos","findSectionStartPos","text","isEmpty","characterCount","endPos","findSectionEndPos","state","doc","serializer","DOMSerializer","fromSchema","schema","serializeFragment","content","sectionId","descendants","node","pos","type","name","attrs","nodeSize","docSize","size","firstDividerPos","passedSection","nextDividerPos","body","s","bold","isActive","italic","underline","strike","bulletList","orderedList","blockquote","codeBlock","link","href","getAttributes","$from","selection","linkMark","resolve","marks","find","m","_c","nodeBefore","markOnPrev","_b","textAlign","fontFamily","fontSize","color","highlight","_d","superscript","subscript","getHTML","normalized","normalizeHardBreaks","tempDoc","textBetween","substring","normalised","dom","DOMParser","parseSlice","to","tr","replaceRange","view","dispatch","dividerHtml","commands","insertContentAt","updateSelection","fragment","parse","parseHtmlToNodes","replaceFrom","replaceWith","setMeta"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("../../node_modules/@tiptap/core/dist/index.js"),t=require("@tiptap/extension-character-count"),n=require("@tiptap/extension-color"),i=require("@tiptap/extension-font-family"),r=require("@tiptap/extension-highlight"),o=require("@tiptap/extension-image"),a=require("@tiptap/extension-link"),s=require("../../node_modules/@tiptap/extension-paragraph/dist/index.js"),d=require("@tiptap/extension-placeholder"),l=require("@tiptap/extension-subscript"),u=require("@tiptap/extension-superscript"),p=require("@tiptap/extension-text-align"),c=require("@tiptap/extension-text-style"),x=require("@tiptap/extension-underline"),m=require("@tiptap/starter-kit"),f=require("./FontSizeExtension.js"),h=require("./mention/MentionExtension.js"),g=require("./paste/PasteExtension.js"),S=require("./plainClipboard/PasteNormalizationExtension.js"),q=require("./sectionDivider/SectionDividerNode.js"),v=require("./sendShortcut/SendShortcutExtension.js"),E=require("./slashCommand/SlashCommandExtension.js"),b=require("./variable/VariableDecorationExtension.js");function M(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var k=M(t),j=M(n),C=M(i),P=M(r),y=M(o),D=M(a),T=M(d),w=M(l),z=M(u),A=M(p),I=M(x),N=M(m);exports.buildExtensions=function(t){var n,i,r,o,a,d,l,u,p;const x=null!==(i=null===(n=t.features)||void 0===n?void 0:n.richPaste)&&void 0!==i&&i,m=null!==(o=null===(r=t.features)||void 0===r?void 0:r.richTypography)&&void 0!==o&&o,M=null===(a=t.features)||void 0===a?void 0:a.allowedMarks,_=e=>!M||M.includes(e),H=s.default.extend({renderHTML(t){let{HTMLAttributes:n}=t;return["p",e.mergeAttributes(n,{style:"margin: 0px; padding: 0px;"}),0]}});return[...[N.default.configure({link:!1,underline:!1,paragraph:!x&&{},hardBreak:!1,bold:!!_("bold")&&{},italic:!!_("italic")&&{},strike:!!_("strike")&&{},code:!!_("code")&&{}}),e.Extension.create({name:"shiftEnterParagraph",addKeyboardShortcuts:()=>({"Shift-Enter":e=>{let{editor:t}=e;return t.commands.splitBlock()}})}),...x?[H]:[],..._("underline")?[I.default]:[],D.default.extend({addPasteRules:()=>[],addInputRules:()=>[]}).configure({openOnClick:!1,autolink:!1,linkOnPaste:!1,HTMLAttributes:{rel:"noopener noreferrer",class:"bik-link"}}),c.TextStyle,T.default.configure({placeholder:null!==(d=t.placeholder)&&void 0!==d?d:"Type a message..."}),...t.onPaste?[g.PasteExtension.configure({onPaste:t.onPaste})]:[],S.PasteNormalizationExtension.configure({preserveMarks:x}),v.SendShortcutExtension.configure({onSend:t.onSend,sendShortcut:t.sendShortcut,extraShortcuts:null!==(l=t.shortcuts)&&void 0!==l?l:[]}),b.VariableDecorationExtension,...t.maxCharacters?[k.default.configure({limit:t.maxCharacters})]:[],q.SectionDividerNode],...[...(null===(u=t.mentions)||void 0===u?void 0:u.agents)?[h.buildAgentMentionExtension(t.mentions.agents,t.onMentionSelected,t.renderMentionItem,t.renderMentionDropdown)]:[],...(null===(p=t.mentions)||void 0===p?void 0:p.teams)?[h.buildTeamMentionExtension(t.mentions.teams,t.onMentionSelected,t.renderMentionItem,t.renderMentionDropdown)]:[],...t.slashCommands?[E.buildSlashCommandExtension(t.slashCommands,t.onSlashCommandSelected,t.renderSlashCommandItem,t.renderSlashCommandDropdown)]:[]],...m?[j.default,P.default.configure({multicolor:!0}),C.default,f.FontSizeExtension,A.default.configure({types:["heading","paragraph"]}),w.default,z.default,y.default]:[j.default]]};
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("@tiptap/extension-character-count"),t=require("@tiptap/extension-color"),n=require("@tiptap/extension-font-family"),i=require("@tiptap/extension-hard-break"),r=require("@tiptap/extension-highlight"),o=require("@tiptap/extension-image"),a=require("@tiptap/extension-link"),s=require("../../node_modules/@tiptap/extension-paragraph/dist/index.js"),l=require("@tiptap/extension-placeholder"),d=require("@tiptap/extension-subscript"),u=require("@tiptap/extension-superscript"),p=require("@tiptap/extension-text-align"),c=require("@tiptap/extension-text-style"),m=require("@tiptap/extension-underline"),x=require("@tiptap/starter-kit"),f=require("./FontSizeExtension.js"),h=require("./mention/MentionExtension.js"),g=require("./paste/PasteExtension.js"),v=require("./plainClipboard/PasteNormalizationExtension.js"),S=require("./sectionDivider/SectionDividerNode.js"),q=require("./sendShortcut/SendShortcutExtension.js"),b=require("./slashCommand/SlashCommandExtension.js"),E=require("./variable/VariableDecorationExtension.js");function k(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var M=k(e),y=k(t),j=k(n),C=k(i),P=k(r),D=k(o),T=k(a),O=k(l),w=k(d),z=k(u),H=k(p),I=k(m),N=k(x);exports.buildExtensions=function(e){var t,n,i,r,o,a,l,d,u;const p=null!==(n=null===(t=e.features)||void 0===t?void 0:t.richPaste)&&void 0!==n&&n,m=null!==(r=null===(i=e.features)||void 0===i?void 0:i.richTypography)&&void 0!==r&&r,x=null===(o=e.features)||void 0===o?void 0:o.allowedMarks,k=e=>!x||x.includes(e),A=C.default.extend({addKeyboardShortcuts(){return{"Shift-Enter":()=>this.editor.commands.setHardBreak()}}}),B=e=>"0"===e||"0px"===e||"0pt"===e||"0em"===e,L=s.default.extend({addAttributes(){var e,t;return Object.assign(Object.assign({},null!==(t=null===(e=this.parent)||void 0===e?void 0:e.call(this))&&void 0!==t?t:{}),{margin:{default:null,parseHTML:e=>B(e.style.marginTop)&&B(e.style.marginBottom)?"0":null,renderHTML:e=>"0"===e.margin?{style:"margin: 0px"}:{}}})}});return[...[N.default.configure({link:!1,underline:!1,hardBreak:!1,paragraph:!1,bold:!!k("bold")&&{},italic:!!k("italic")&&{},strike:!!k("strike")&&{},code:!!k("code")&&{}}),L,A,...k("underline")?[I.default]:[],T.default.extend({addPasteRules:()=>[],addInputRules:()=>[]}).configure({openOnClick:!1,autolink:!1,linkOnPaste:!1,HTMLAttributes:{rel:"noopener noreferrer",class:"bik-link"}}),c.TextStyle,O.default.configure({placeholder:null!==(a=e.placeholder)&&void 0!==a?a:"Type a message..."}),...e.onPaste?[g.PasteExtension.configure({onPaste:e.onPaste})]:[],v.PasteNormalizationExtension.configure({preserveMarks:p,plainOnly:!p}),q.SendShortcutExtension.configure({onSend:e.onSend,sendShortcut:e.sendShortcut,extraShortcuts:null!==(l=e.shortcuts)&&void 0!==l?l:[]}),E.VariableDecorationExtension,...e.maxCharacters?[M.default.configure({limit:e.maxCharacters})]:[],S.SectionDividerNode],...[...(null===(d=e.mentions)||void 0===d?void 0:d.agents)?[h.buildAgentMentionExtension(e.mentions.agents,e.onMentionSelected,e.renderMentionItem,e.renderMentionDropdown)]:[],...(null===(u=e.mentions)||void 0===u?void 0:u.teams)?[h.buildTeamMentionExtension(e.mentions.teams,e.onMentionSelected,e.renderMentionItem,e.renderMentionDropdown)]:[],...e.slashCommands?[b.buildSlashCommandExtension(e.slashCommands,e.onSlashCommandSelected,e.renderSlashCommandItem,e.renderSlashCommandDropdown)]:[]],...m?[y.default,P.default.configure({multicolor:!0}),j.default,f.FontSizeExtension,H.default.configure({types:["heading","paragraph"]}),w.default,z.default,D.default]:[y.default]]};
2
2
  //# sourceMappingURL=buildExtensions.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"buildExtensions.js","sources":["../../../../src/editor/extensions/buildExtensions.ts"],"sourcesContent":["import { Extension, mergeAttributes } from '@tiptap/core';\nimport CharacterCount from '@tiptap/extension-character-count';\nimport Color from '@tiptap/extension-color';\nimport FontFamily from '@tiptap/extension-font-family';\nimport Highlight from '@tiptap/extension-highlight';\nimport Image from '@tiptap/extension-image';\nimport Link from '@tiptap/extension-link';\nimport Paragraph from '@tiptap/extension-paragraph';\nimport Placeholder from '@tiptap/extension-placeholder';\nimport Subscript from '@tiptap/extension-subscript';\nimport Superscript from '@tiptap/extension-superscript';\nimport TextAlign from '@tiptap/extension-text-align';\nimport { TextStyle } from '@tiptap/extension-text-style';\nimport Underline from '@tiptap/extension-underline';\nimport StarterKit from '@tiptap/starter-kit';\nimport type { ReactNode } from 'react';\nimport type {\n\tEditorFeatures,\n\tEditorSnapshot,\n\tKeyboardShortcut,\n\tMentionDropdownRenderProps,\n\tMentionItem,\n\tPasteData,\n\tSlashCommandDropdownRenderProps,\n\tSlashCommandItem,\n} from '../BikEditor.types';\nimport { FontSizeExtension } from './FontSizeExtension';\nimport {\n\tbuildAgentMentionExtension,\n\tbuildTeamMentionExtension,\n} from './mention/MentionExtension';\nimport { PasteExtension } from './paste/PasteExtension';\nimport { PasteNormalizationExtension } from './plainClipboard/PasteNormalizationExtension';\nimport { SectionDividerNode } from './sectionDivider/SectionDividerNode';\nimport { SendShortcutExtension } from './sendShortcut/SendShortcutExtension';\nimport { buildSlashCommandExtension } from './slashCommand/SlashCommandExtension';\nimport { VariableDecorationExtension } from './variable/VariableDecorationExtension';\n\ninterface ExtensionConfig {\n\tfeatures?: EditorFeatures;\n\tplaceholder?: string;\n\tmaxCharacters?: number;\n\thasSections?: boolean;\n\tmentions?: {\n\t\tagents?: { current: MentionItem[] };\n\t\tteams?: { current: MentionItem[] };\n\t};\n\tslashCommands?: { current: SlashCommandItem[] };\n\tonPaste?: (data: PasteData) => boolean | void;\n\tonSend?: (content: EditorSnapshot) => void;\n\tsendShortcut?:\n\t\t| { key: string; modifiers?: Array<'mod' | 'ctrl' | 'shift' | 'alt'> }\n\t\t| Array<{\n\t\t\t\tkey: string;\n\t\t\t\tmodifiers?: Array<'mod' | 'ctrl' | 'shift' | 'alt'>;\n\t\t }>;\n\tshortcuts?: KeyboardShortcut[];\n\tonMentionSelected?: (item: MentionItem, char: '@' | '#') => void;\n\tonSlashCommandSelected?: (command: SlashCommandItem) => void;\n\trenderMentionItem?: (item: MentionItem, isActive: boolean) => ReactNode;\n\trenderMentionDropdown?: (props: MentionDropdownRenderProps) => ReactNode;\n\trenderSlashCommandItem?: (\n\t\titem: SlashCommandItem,\n\t\tisActive: boolean,\n\t) => ReactNode;\n\trenderSlashCommandDropdown?: (\n\t\tprops: SlashCommandDropdownRenderProps,\n\t) => ReactNode;\n}\n\nexport function buildExtensions(config: ExtensionConfig) {\n\tconst hasRichPaste = config.features?.richPaste ?? false;\n\tconst hasRichTypography = config.features?.richTypography ?? false;\n\n\t// When allowedMarks is specified only those marks are registered in the schema.\n\t// This disables rendering, keyboard shortcuts AND paste-preservation for the\n\t// excluded marks — all three come for free when the extension is absent.\n\tconst allowedMarks = config.features?.allowedMarks;\n\tconst isMark = (m: 'bold' | 'italic' | 'strike' | 'underline' | 'code') =>\n\t\t!allowedMarks || allowedMarks.includes(m);\n\n\tconst emailParagraph = Paragraph.extend({\n\t\trenderHTML({ HTMLAttributes }) {\n\t\t\treturn [\n\t\t\t\t'p',\n\t\t\t\tmergeAttributes(HTMLAttributes, {\n\t\t\t\t\tstyle: 'margin: 0px; padding: 0px;',\n\t\t\t\t}),\n\t\t\t\t0,\n\t\t\t];\n\t\t},\n\t});\n\n\tconst base = [\n\t\t// Exclude Link and Underline from StarterKit — TipTap v3 bundles both.\n\t\t// Without this, two copies of each are registered and the StarterKit copy's\n\t\t// click handler calls window.open even when openOnClick: false is set.\n\t\tStarterKit.configure({\n\t\t\tlink: false,\n\t\t\tunderline: false,\n\t\t\tparagraph: hasRichPaste ? false : {},\n\t\t\thardBreak: false,\n\t\t\tbold: isMark('bold') ? {} : false,\n\t\t\titalic: isMark('italic') ? {} : false,\n\t\t\tstrike: isMark('strike') ? {} : false,\n\t\t\tcode: isMark('code') ? {} : false,\n\t\t}),\n\t\tExtension.create({\n\t\t\tname: 'shiftEnterParagraph',\n\t\t\taddKeyboardShortcuts() {\n\t\t\t\treturn {\n\t\t\t\t\t'Shift-Enter': ({ editor }) => editor.commands.splitBlock(),\n\t\t\t\t};\n\t\t\t},\n\t\t}),\n\t\t...(hasRichPaste ? [emailParagraph] : []),\n\t\t...(isMark('underline') ? [Underline] : []),\n\t\t// Extend Link to strip addPasteRules() and addInputRules() so pasting a\n\t\t// URL never creates a hyperlink regardless of autolink/linkOnPaste values.\n\t\tLink.extend({\n\t\t\taddPasteRules: () => [],\n\t\t\taddInputRules: () => [],\n\t\t}).configure({\n\t\t\topenOnClick: false,\n\t\t\tautolink: false,\n\t\t\tlinkOnPaste: false,\n\t\t\tHTMLAttributes: {\n\t\t\t\trel: 'noopener noreferrer',\n\t\t\t\tclass: 'bik-link',\n\t\t\t},\n\t\t}),\n\t\tTextStyle,\n\t\tPlaceholder.configure({\n\t\t\tplaceholder: config.placeholder ?? 'Type a message...',\n\t\t}),\n\t\t// Consumer paste callback runs first. If it returns true the event is\n\t\t// consumed and neither PlainClipboardExtension nor the editor's default\n\t\t// paste handling will run for that event.\n\t\t...(config.onPaste\n\t\t\t? [PasteExtension.configure({ onPaste: config.onPaste })]\n\t\t\t: []),\n\t\tPasteNormalizationExtension.configure({\n\t\t\tpreserveMarks: hasRichPaste,\n\t\t}),\n\t\tSendShortcutExtension.configure({\n\t\t\tonSend: config.onSend,\n\t\t\tsendShortcut: config.sendShortcut,\n\t\t\textraShortcuts: config.shortcuts ?? [],\n\t\t}),\n\t\tVariableDecorationExtension,\n\t\t...(config.maxCharacters\n\t\t\t? [CharacterCount.configure({ limit: config.maxCharacters })]\n\t\t\t: []),\n\t\t// Always register SectionDividerNode so the custom <div data-section-divider>\n\t\t// node is part of the schema in every editor instance. This means imperative\n\t\t// callers (setBodyAndSections / setSectionContent) work even when no\n\t\t// `sections` prop was provided at mount time.\n\t\tSectionDividerNode,\n\t];\n\n\tconst mentionExtensions = [\n\t\t...(config.mentions?.agents\n\t\t\t? [\n\t\t\t\t\tbuildAgentMentionExtension(\n\t\t\t\t\t\tconfig.mentions.agents,\n\t\t\t\t\t\tconfig.onMentionSelected,\n\t\t\t\t\t\tconfig.renderMentionItem,\n\t\t\t\t\t\tconfig.renderMentionDropdown,\n\t\t\t\t\t),\n\t\t\t ]\n\t\t\t: []),\n\t\t...(config.mentions?.teams\n\t\t\t? [\n\t\t\t\t\tbuildTeamMentionExtension(\n\t\t\t\t\t\tconfig.mentions.teams,\n\t\t\t\t\t\tconfig.onMentionSelected,\n\t\t\t\t\t\tconfig.renderMentionItem,\n\t\t\t\t\t\tconfig.renderMentionDropdown,\n\t\t\t\t\t),\n\t\t\t ]\n\t\t\t: []),\n\t\t...(config.slashCommands\n\t\t\t? [\n\t\t\t\t\tbuildSlashCommandExtension(\n\t\t\t\t\t\tconfig.slashCommands,\n\t\t\t\t\t\tconfig.onSlashCommandSelected,\n\t\t\t\t\t\tconfig.renderSlashCommandItem,\n\t\t\t\t\t\tconfig.renderSlashCommandDropdown,\n\t\t\t\t\t),\n\t\t\t ]\n\t\t\t: []),\n\t];\n\n\tconst richExtensions = hasRichTypography\n\t\t? [\n\t\t\t\tColor,\n\t\t\t\tHighlight.configure({ multicolor: true }),\n\t\t\t\tFontFamily,\n\t\t\t\tFontSizeExtension,\n\t\t\t\tTextAlign.configure({ types: ['heading', 'paragraph'] }),\n\t\t\t\tSubscript,\n\t\t\t\tSuperscript,\n\t\t\t\tImage,\n\t\t ]\n\t\t: [Color];\n\n\treturn [...base, ...mentionExtensions, ...richExtensions];\n}\n"],"names":["config","hasRichPaste","_b","_a","features","richPaste","hasRichTypography","_d","_c","richTypography","allowedMarks","_e","isMark","m","includes","emailParagraph","Paragraph","extend","renderHTML","_ref","HTMLAttributes","mergeAttributes","style","StarterKit","configure","link","underline","paragraph","hardBreak","bold","italic","strike","code","Extension","create","name","addKeyboardShortcuts","_ref2","editor","commands","splitBlock","Underline","Link","addPasteRules","addInputRules","openOnClick","autolink","linkOnPaste","rel","class","TextStyle","Placeholder","placeholder","_f","onPaste","PasteExtension","PasteNormalizationExtension","preserveMarks","SendShortcutExtension","onSend","sendShortcut","extraShortcuts","_g","shortcuts","VariableDecorationExtension","maxCharacters","CharacterCount","limit","SectionDividerNode","_h","mentions","agents","buildAgentMentionExtension","onMentionSelected","renderMentionItem","renderMentionDropdown","_j","teams","buildTeamMentionExtension","slashCommands","buildSlashCommandExtension","onSlashCommandSelected","renderSlashCommandItem","renderSlashCommandDropdown","Color","Highlight","multicolor","FontFamily","FontSizeExtension","TextAlign","types","Subscript","Superscript","Image"],"mappings":"2xCAsEM,SAA0BA,yBAC/B,MAAMC,EAA6C,QAA9BC,EAAiB,QAAjBC,EAAAH,EAAOI,gBAAU,IAAAD,OAAA,EAAAA,EAAAE,iBAAa,IAAAH,GAAAA,EAC7CI,EAAuD,QAAnCC,EAAiB,QAAjBC,EAAAR,EAAOI,gBAAU,IAAAI,OAAA,EAAAA,EAAAC,sBAAkB,IAAAF,GAAAA,EAKvDG,EAA8B,QAAfC,EAAAX,EAAOI,gBAAQ,IAAAO,OAAA,EAAAA,EAAED,aAChCE,EAAUC,IACdH,GAAgBA,EAAaI,SAASD,GAElCE,EAAiBC,EAAS,QAACC,OAAO,CACvCC,WAAUC,GAAmB,IAAlBC,eAAEA,GAAgBD,EAC5B,MAAO,CACN,IACAE,EAAeA,gBAACD,EAAgB,CAC/BE,MAAO,+BAER,EAEF,IAoHD,MAAO,IAjHM,CAIZC,EAAAA,QAAWC,UAAU,CACpBC,MAAM,EACNC,WAAW,EACXC,WAAW1B,GAAuB,CAAE,EACpC2B,WAAW,EACXC,OAAMjB,EAAO,SAAU,CAAE,EACzBkB,SAAQlB,EAAO,WAAY,CAAE,EAC7BmB,SAAQnB,EAAO,WAAY,CAAE,EAC7BoB,OAAMpB,EAAO,SAAU,CAAE,IAE1BqB,EAASA,UAACC,OAAO,CAChBC,KAAM,sBACNC,qBAAoBA,KACZ,CACN,cAAeC,IAAA,IAACC,OAAEA,GAAQD,EAAA,OAAKC,EAAOC,SAASC,YAAY,SAI1DvC,EAAe,CAACc,GAAkB,MAClCH,EAAO,aAAe,CAAC6B,EAAS,SAAI,GAGxCC,EAAAA,QAAKzB,OAAO,CACX0B,cAAeA,IAAM,GACrBC,cAAeA,IAAM,KACnBpB,UAAU,CACZqB,aAAa,EACbC,UAAU,EACVC,aAAa,EACb3B,eAAgB,CACf4B,IAAK,sBACLC,MAAO,cAGTC,EAAAA,UACAC,EAAW,QAAC3B,UAAU,CACrB4B,oBAAaC,EAAArD,EAAOoD,2BAAe,yBAKhCpD,EAAOsD,QACR,CAACC,EAAAA,eAAe/B,UAAU,CAAE8B,QAAStD,EAAOsD,WAC5C,GACHE,EAAAA,4BAA4BhC,UAAU,CACrCiC,cAAexD,IAEhByD,EAAqBA,sBAAClC,UAAU,CAC/BmC,OAAQ3D,EAAO2D,OACfC,aAAc5D,EAAO4D,aACrBC,uBAAgBC,EAAA9D,EAAO+D,yBAAa,KAErCC,EAA2BA,+BACvBhE,EAAOiE,cACR,CAACC,EAAc,QAAC1C,UAAU,CAAE2C,MAAOnE,EAAOiE,iBAC1C,GAKHG,EAAAA,uBAGyB,aACrBC,EAAArE,EAAOsE,+BAAUC,QAClB,CACAC,6BACCxE,EAAOsE,SAASC,OAChBvE,EAAOyE,kBACPzE,EAAO0E,kBACP1E,EAAO2E,wBAGR,eACCC,EAAA5E,EAAOsE,+BAAUO,OAClB,CACAC,EAAyBA,0BACxB9E,EAAOsE,SAASO,MAChB7E,EAAOyE,kBACPzE,EAAO0E,kBACP1E,EAAO2E,wBAGR,MACC3E,EAAO+E,cACR,CACAC,EAAAA,2BACChF,EAAO+E,cACP/E,EAAOiF,uBACPjF,EAAOkF,uBACPlF,EAAOmF,6BAGR,OAGmB7E,EACpB,CACA8E,EAAAA,QACAC,EAAAA,QAAU7D,UAAU,CAAE8D,YAAY,IAClCC,EAAU,QACVC,oBACAC,EAAAA,QAAUjE,UAAU,CAAEkE,MAAO,CAAC,UAAW,eACzCC,EAAS,QACTC,EAAW,QACXC,EAAK,SAEL,CAACT,EAAK,SAGV"}
1
+ {"version":3,"file":"buildExtensions.js","sources":["../../../../src/editor/extensions/buildExtensions.ts"],"sourcesContent":["import CharacterCount from '@tiptap/extension-character-count';\nimport Color from '@tiptap/extension-color';\nimport FontFamily from '@tiptap/extension-font-family';\nimport HardBreak from '@tiptap/extension-hard-break';\nimport Highlight from '@tiptap/extension-highlight';\nimport Image from '@tiptap/extension-image';\nimport Link from '@tiptap/extension-link';\nimport Paragraph from '@tiptap/extension-paragraph';\nimport Placeholder from '@tiptap/extension-placeholder';\nimport Subscript from '@tiptap/extension-subscript';\nimport Superscript from '@tiptap/extension-superscript';\nimport TextAlign from '@tiptap/extension-text-align';\nimport { TextStyle } from '@tiptap/extension-text-style';\nimport Underline from '@tiptap/extension-underline';\nimport StarterKit from '@tiptap/starter-kit';\nimport type { ReactNode } from 'react';\nimport type {\n\tEditorFeatures,\n\tEditorSnapshot,\n\tKeyboardShortcut,\n\tMentionDropdownRenderProps,\n\tMentionItem,\n\tPasteData,\n\tSlashCommandDropdownRenderProps,\n\tSlashCommandItem,\n} from '../BikEditor.types';\nimport { FontSizeExtension } from './FontSizeExtension';\nimport {\n\tbuildAgentMentionExtension,\n\tbuildTeamMentionExtension,\n} from './mention/MentionExtension';\nimport { PasteExtension } from './paste/PasteExtension';\nimport { PasteNormalizationExtension } from './plainClipboard/PasteNormalizationExtension';\nimport { SectionDividerNode } from './sectionDivider/SectionDividerNode';\nimport { SendShortcutExtension } from './sendShortcut/SendShortcutExtension';\nimport { buildSlashCommandExtension } from './slashCommand/SlashCommandExtension';\nimport { VariableDecorationExtension } from './variable/VariableDecorationExtension';\n\ninterface ExtensionConfig {\n\tfeatures?: EditorFeatures;\n\tplaceholder?: string;\n\tmaxCharacters?: number;\n\thasSections?: boolean;\n\tmentions?: {\n\t\tagents?: { current: MentionItem[] };\n\t\tteams?: { current: MentionItem[] };\n\t};\n\tslashCommands?: { current: SlashCommandItem[] };\n\tonPaste?: (data: PasteData) => boolean | void;\n\tonSend?: (content: EditorSnapshot) => void;\n\tsendShortcut?:\n\t\t| { key: string; modifiers?: Array<'mod' | 'ctrl' | 'shift' | 'alt'> }\n\t\t| Array<{\n\t\t\t\tkey: string;\n\t\t\t\tmodifiers?: Array<'mod' | 'ctrl' | 'shift' | 'alt'>;\n\t\t }>;\n\tshortcuts?: KeyboardShortcut[];\n\tonMentionSelected?: (item: MentionItem, char: '@' | '#') => void;\n\tonSlashCommandSelected?: (command: SlashCommandItem) => void;\n\trenderMentionItem?: (item: MentionItem, isActive: boolean) => ReactNode;\n\trenderMentionDropdown?: (props: MentionDropdownRenderProps) => ReactNode;\n\trenderSlashCommandItem?: (\n\t\titem: SlashCommandItem,\n\t\tisActive: boolean,\n\t) => ReactNode;\n\trenderSlashCommandDropdown?: (\n\t\tprops: SlashCommandDropdownRenderProps,\n\t) => ReactNode;\n}\n\nexport function buildExtensions(config: ExtensionConfig) {\n\tconst hasRichPaste = config.features?.richPaste ?? false;\n\tconst hasRichTypography = config.features?.richTypography ?? false;\n\n\t// When allowedMarks is specified only those marks are registered in the schema.\n\t// This disables rendering, keyboard shortcuts AND paste-preservation for the\n\t// excluded marks — all three come for free when the extension is absent.\n\tconst allowedMarks = config.features?.allowedMarks;\n\tconst isMark = (m: 'bold' | 'italic' | 'strike' | 'underline' | 'code') =>\n\t\t!allowedMarks || allowedMarks.includes(m);\n\n\t// Soft line break. Shift-Enter inserts a <br> inside the current block\n\t// (matches Gmail); plain Enter still splits the block into a new paragraph\n\t// via StarterKit's default. HardBreak is registered standalone — like Link\n\t// and Underline above — instead of through StarterKit, so we can bind ONLY\n\t// Shift-Enter here and leave Mod-Enter free for the send shortcut.\n\tconst softBreak = HardBreak.extend({\n\t\taddKeyboardShortcuts() {\n\t\t\treturn {\n\t\t\t\t'Shift-Enter': () => this.editor.commands.setHardBreak(),\n\t\t\t};\n\t\t},\n\t});\n\n\t// Paragraph that preserves a zero block-margin from the source (Gmail-style).\n\t// Most sources (ChatGPT, typed text) use plain <p> → no margin attr → the\n\t// editor/viewer default 1em margin provides the gap. Google-Docs-style\n\t// content uses margin:0 paragraphs and a separate blank-line paragraph for\n\t// spacing; we keep that margin:0 so the blank line — not stacked margins —\n\t// provides a single-line gap, exactly like Gmail. The paste pipeline also\n\t// sets margin:'0' on every paragraph when a paste contains blank-line\n\t// paragraphs (see applyParagraphSpacing), so spacing comes from EITHER the\n\t// margin OR the blank line, never both (no double gap).\n\tconst isZeroLen = (v: string) =>\n\t\tv === '0' || v === '0px' || v === '0pt' || v === '0em';\n\tconst ParagraphWithMargin = Paragraph.extend({\n\t\taddAttributes() {\n\t\t\treturn {\n\t\t\t\t...(this.parent?.() ?? {}),\n\t\t\t\tmargin: {\n\t\t\t\t\tdefault: null,\n\t\t\t\t\tparseHTML: (el: HTMLElement) =>\n\t\t\t\t\t\tisZeroLen(el.style.marginTop) && isZeroLen(el.style.marginBottom)\n\t\t\t\t\t\t\t? '0'\n\t\t\t\t\t\t\t: null,\n\t\t\t\t\trenderHTML: (attrs: { margin?: string | null }) =>\n\t\t\t\t\t\tattrs.margin === '0' ? { style: 'margin: 0px' } : {},\n\t\t\t\t},\n\t\t\t};\n\t\t},\n\t});\n\n\tconst base = [\n\t\t// Exclude Link and Underline from StarterKit — TipTap v3 bundles both.\n\t\t// Without this, two copies of each are registered and the StarterKit copy's\n\t\t// click handler calls window.open even when openOnClick: false is set.\n\t\tStarterKit.configure({\n\t\t\tlink: false,\n\t\t\tunderline: false,\n\t\t\thardBreak: false,\n\t\t\tparagraph: false,\n\t\t\tbold: isMark('bold') ? {} : false,\n\t\t\titalic: isMark('italic') ? {} : false,\n\t\t\tstrike: isMark('strike') ? {} : false,\n\t\t\tcode: isMark('code') ? {} : false,\n\t\t}),\n\t\tParagraphWithMargin,\n\t\tsoftBreak,\n\t\t...(isMark('underline') ? [Underline] : []),\n\t\t// Extend Link to strip addPasteRules() and addInputRules() so pasting a\n\t\t// URL never creates a hyperlink regardless of autolink/linkOnPaste values.\n\t\tLink.extend({\n\t\t\taddPasteRules: () => [],\n\t\t\taddInputRules: () => [],\n\t\t}).configure({\n\t\t\topenOnClick: false,\n\t\t\tautolink: false,\n\t\t\tlinkOnPaste: false,\n\t\t\tHTMLAttributes: {\n\t\t\t\trel: 'noopener noreferrer',\n\t\t\t\tclass: 'bik-link',\n\t\t\t},\n\t\t}),\n\t\tTextStyle,\n\t\tPlaceholder.configure({\n\t\t\tplaceholder: config.placeholder ?? 'Type a message...',\n\t\t}),\n\t\t// Consumer paste callback runs first. If it returns true the event is\n\t\t// consumed and neither PlainClipboardExtension nor the editor's default\n\t\t// paste handling will run for that event.\n\t\t...(config.onPaste\n\t\t\t? [PasteExtension.configure({ onPaste: config.onPaste })]\n\t\t\t: []),\n\t\tPasteNormalizationExtension.configure({\n\t\t\tpreserveMarks: hasRichPaste,\n\t\t\t// Only email + signature (richPaste) keep the rich HTML paste pipeline.\n\t\t\t// Every other channel (WhatsApp, IG, FB, comments) forces plain-text\n\t\t\t// paste for BOTH Cmd/Ctrl+V and Cmd/Ctrl+Shift+V — no formatting.\n\t\t\tplainOnly: !hasRichPaste,\n\t\t}),\n\t\tSendShortcutExtension.configure({\n\t\t\tonSend: config.onSend,\n\t\t\tsendShortcut: config.sendShortcut,\n\t\t\textraShortcuts: config.shortcuts ?? [],\n\t\t}),\n\t\tVariableDecorationExtension,\n\t\t...(config.maxCharacters\n\t\t\t? [CharacterCount.configure({ limit: config.maxCharacters })]\n\t\t\t: []),\n\t\t// Always register SectionDividerNode so the custom <div data-section-divider>\n\t\t// node is part of the schema in every editor instance. This means imperative\n\t\t// callers (setBodyAndSections / setSectionContent) work even when no\n\t\t// `sections` prop was provided at mount time.\n\t\tSectionDividerNode,\n\t];\n\n\tconst mentionExtensions = [\n\t\t...(config.mentions?.agents\n\t\t\t? [\n\t\t\t\t\tbuildAgentMentionExtension(\n\t\t\t\t\t\tconfig.mentions.agents,\n\t\t\t\t\t\tconfig.onMentionSelected,\n\t\t\t\t\t\tconfig.renderMentionItem,\n\t\t\t\t\t\tconfig.renderMentionDropdown,\n\t\t\t\t\t),\n\t\t\t ]\n\t\t\t: []),\n\t\t...(config.mentions?.teams\n\t\t\t? [\n\t\t\t\t\tbuildTeamMentionExtension(\n\t\t\t\t\t\tconfig.mentions.teams,\n\t\t\t\t\t\tconfig.onMentionSelected,\n\t\t\t\t\t\tconfig.renderMentionItem,\n\t\t\t\t\t\tconfig.renderMentionDropdown,\n\t\t\t\t\t),\n\t\t\t ]\n\t\t\t: []),\n\t\t...(config.slashCommands\n\t\t\t? [\n\t\t\t\t\tbuildSlashCommandExtension(\n\t\t\t\t\t\tconfig.slashCommands,\n\t\t\t\t\t\tconfig.onSlashCommandSelected,\n\t\t\t\t\t\tconfig.renderSlashCommandItem,\n\t\t\t\t\t\tconfig.renderSlashCommandDropdown,\n\t\t\t\t\t),\n\t\t\t ]\n\t\t\t: []),\n\t];\n\n\tconst richExtensions = hasRichTypography\n\t\t? [\n\t\t\t\tColor,\n\t\t\t\tHighlight.configure({ multicolor: true }),\n\t\t\t\tFontFamily,\n\t\t\t\tFontSizeExtension,\n\t\t\t\tTextAlign.configure({ types: ['heading', 'paragraph'] }),\n\t\t\t\tSubscript,\n\t\t\t\tSuperscript,\n\t\t\t\tImage,\n\t\t ]\n\t\t: [Color];\n\n\treturn [...base, ...mentionExtensions, ...richExtensions];\n}\n"],"names":["config","hasRichPaste","_b","_a","features","richPaste","hasRichTypography","_d","_c","richTypography","allowedMarks","_e","isMark","m","includes","softBreak","HardBreak","extend","addKeyboardShortcuts","Shift-Enter","this","editor","commands","setHardBreak","isZeroLen","v","ParagraphWithMargin","Paragraph","addAttributes","Object","assign","parent","call","margin","default","parseHTML","el","style","marginTop","marginBottom","renderHTML","attrs","StarterKit","configure","link","underline","hardBreak","paragraph","bold","italic","strike","code","Underline","Link","addPasteRules","addInputRules","openOnClick","autolink","linkOnPaste","HTMLAttributes","rel","class","TextStyle","Placeholder","placeholder","_f","onPaste","PasteExtension","PasteNormalizationExtension","preserveMarks","plainOnly","SendShortcutExtension","onSend","sendShortcut","extraShortcuts","_g","shortcuts","VariableDecorationExtension","maxCharacters","CharacterCount","limit","SectionDividerNode","_h","mentions","agents","buildAgentMentionExtension","onMentionSelected","renderMentionItem","renderMentionDropdown","_j","teams","buildTeamMentionExtension","slashCommands","buildSlashCommandExtension","onSlashCommandSelected","renderSlashCommandItem","renderSlashCommandDropdown","Color","Highlight","multicolor","FontFamily","FontSizeExtension","TextAlign","types","Subscript","Superscript","Image"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("../../../node_modules/@tiptap/core/dist/index.js"),t=require("@tiptap/pm/model"),r=require("@tiptap/pm/state"),a=require("@tiptap/pm/view"),n=require("./pasteUtils.js");const i=e.Extension.create({name:"pasteNormalization",addOptions:()=>({preserveMarks:!1}),addProseMirrorPlugins(){const e=this.options.preserveMarks;return[new r.Plugin({props:{decorations(e){const t=[];return e.doc.descendants(((e,r)=>{"paragraph"===e.type.name&&0===e.childCount&&t.push(a.Decoration.node(r,r+e.nodeSize,{class:"is-blank"}))})),a.DecorationSet.create(e.doc,t)},handlePaste(e,t){var r,a;const i=null===(r=t.clipboardData)||void 0===r?void 0:r.getData("text/html");if(null==i?void 0:i.includes("data-pm-slice"))return!1;const s=(i?n.extractTextFromHtml(i):void 0)||(null===(a=t.clipboardData)||void 0===a?void 0:a.getData("text/plain"));if(!s)return!1;const o=n.parseClipboardText(s,e.state.schema);return e.dispatch(e.state.tr.replaceSelection(o)),!0},clipboardTextParser:(e,t,r,a)=>n.parseClipboardText(e,a.state.schema),transformPastedHTML(e){if(e.includes("data-pm-slice"))return e;const t=document.createElement("div");return t.innerHTML=e,n.wrapInlineContent(t),n.cleanBlockBrs(t),n.stripTrailingBrs(t),t.innerHTML},transformPasted(r){let a=n.normalizeHardBreaks(r.content);return e||(a=n.stripRichMarks(a)),new t.Slice(a,r.openStart,r.openEnd)}}})]}});exports.PasteNormalizationExtension=i;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("../../../node_modules/@tiptap/core/dist/index.js"),t=require("@tiptap/pm/model"),r=require("@tiptap/pm/state"),n=require("./pasteUtils.js");const a=e.Extension.create({name:"pasteNormalization",addOptions:()=>({preserveMarks:!1,plainOnly:!1}),addProseMirrorPlugins(){const e=this.options.preserveMarks,a=this.options.plainOnly;let s;const i=e=>{var t;const r=e.firstChild;s=r&&"paragraph"===r.type.name?null!==(t=r.attrs.margin)&&void 0!==t?t:null:void 0};return[new r.Plugin({appendTransaction(e,t,r){var n;if(!e.some((e=>e.docChanged)))return s=void 0,null;const a=r.tr;let i=!1;if(void 0!==s){const o=s;let l=t.selection.from;for(const t of e)l=t.mapping.map(l);const p=r.doc.resolve(Math.max(0,Math.min(l,r.doc.content.size)));for(let e=p.depth;e>=1;e--){const t=p.node(e);if("paragraph"===t.type.name){(null!==(n=t.attrs.margin)&&void 0!==n?n:null)!==o&&(a.setNodeMarkup(p.before(e),void 0,Object.assign(Object.assign({},t.attrs),{margin:o})),i=!0);break}}}s=void 0;const o=a.doc;let l=null;return o.forEach(((e,t)=>{var r;if("paragraph"!==e.type.name)return void(l=null);let n=null!==(r=e.attrs.margin)&&void 0!==r?r:null;0===e.childCount&&"0"!==n&&"0"===l&&(a.setNodeMarkup(t,void 0,Object.assign(Object.assign({},e.attrs),{margin:"0"})),n="0",i=!0),l=n})),i?a.setMeta("addToHistory",!1):null},props:Object.assign(Object.assign({},a?{handlePaste(e,t){var r,a;const s=null!==(a=null===(r=t.clipboardData)||void 0===r?void 0:r.getData("text/plain"))&&void 0!==a?a:"";if(!s)return!1;const o=n.parseClipboardTextAsBreaks(s,e.state.schema);return i(o.content),e.dispatch(e.state.tr.replaceSelection(o)),!0}}:{}),{clipboardTextParser(e,t,r,a){const s=n.parseClipboardTextAsBreaks(e,a.state.schema);return i(s.content),s},transformPastedHTML(e){if(e.includes("data-pm-slice"))return e;const t=document.createElement("div");return t.innerHTML=e,n.wrapInlineContent(t),n.cleanBlockBrs(t),n.stripTrailingBrs(t),t.innerHTML},transformPasted(r){let a=n.normalizeHardBreaks(r.content);return e||(a=n.stripRichMarks(a)),a=n.mirrorParagraphMargins(a),i(a),new t.Slice(a,r.openStart,r.openEnd)}})})]}});exports.PasteNormalizationExtension=a;
2
2
  //# sourceMappingURL=PasteNormalizationExtension.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"PasteNormalizationExtension.js","sources":["../../../../../src/editor/extensions/plainClipboard/PasteNormalizationExtension.ts"],"sourcesContent":["import { Extension } from '@tiptap/core';\nimport { Slice } from '@tiptap/pm/model';\nimport { Plugin } from '@tiptap/pm/state';\nimport { Decoration, DecorationSet } from '@tiptap/pm/view';\nimport {\n\tcleanBlockBrs,\n\textractTextFromHtml,\n\tnormalizeHardBreaks,\n\tparseClipboardText,\n\tstripRichMarks,\n\tstripTrailingBrs,\n\twrapInlineContent,\n} from './pasteUtils';\n\n/**\n * Unified paste normalizer for all editor channels.\n *\n * Uses ProseMirror's recommended hooks instead of overriding handlePaste:\n * - clipboardTextParser: plain text → Slice (preserves blank lines)\n * - transformPastedHTML: HTML → HTML (cleans Google Docs / Word artifacts)\n * - transformPasted: Slice → Slice (normalizes hardBreaks, strips marks)\n *\n * Also decorates empty paragraphs with an `is-blank` CSS class so the\n * editor stylesheet can give blank lines precise height without extra margins.\n */\nexport const PasteNormalizationExtension = Extension.create({\n\tname: 'pasteNormalization',\n\taddOptions() {\n\t\treturn {\n\t\t\tpreserveMarks: false,\n\t\t};\n\t},\n\taddProseMirrorPlugins() {\n\t\tconst preserveMarks = this.options.preserveMarks;\n\t\treturn [\n\t\t\tnew Plugin({\n\t\t\t\tprops: {\n\t\t\t\t\tdecorations(state) {\n\t\t\t\t\t\tconst decorations: Decoration[] = [];\n\t\t\t\t\t\tstate.doc.descendants((node, pos) => {\n\t\t\t\t\t\t\tif (node.type.name === 'paragraph' && node.childCount === 0) {\n\t\t\t\t\t\t\t\tdecorations.push(\n\t\t\t\t\t\t\t\t\tDecoration.node(pos, pos + node.nodeSize, {\n\t\t\t\t\t\t\t\t\t\tclass: 'is-blank',\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t});\n\t\t\t\t\t\treturn DecorationSet.create(state.doc, decorations);\n\t\t\t\t\t},\n\n\t\t\t\t\thandlePaste(view, event) {\n\t\t\t\t\t\tconst html = event.clipboardData?.getData('text/html');\n\t\t\t\t\t\tif (html?.includes('data-pm-slice')) return false;\n\t\t\t\t\t\tconst text =\n\t\t\t\t\t\t\t(html ? extractTextFromHtml(html) : undefined) ||\n\t\t\t\t\t\t\tevent.clipboardData?.getData('text/plain');\n\t\t\t\t\t\tif (!text) return false;\n\t\t\t\t\t\tconst slice = parseClipboardText(text, view.state.schema);\n\t\t\t\t\t\tview.dispatch(view.state.tr.replaceSelection(slice));\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t},\n\n\t\t\t\t\tclipboardTextParser(text, _$context, _plain, view) {\n\t\t\t\t\t\treturn parseClipboardText(text, view.state.schema);\n\t\t\t\t\t},\n\n\t\t\t\t\ttransformPastedHTML(html) {\n\t\t\t\t\t\tif (html.includes('data-pm-slice')) return html;\n\t\t\t\t\t\tconst container = document.createElement('div');\n\t\t\t\t\t\tcontainer.innerHTML = html;\n\t\t\t\t\t\twrapInlineContent(container);\n\t\t\t\t\t\tcleanBlockBrs(container);\n\t\t\t\t\t\tstripTrailingBrs(container);\n\t\t\t\t\t\treturn container.innerHTML;\n\t\t\t\t\t},\n\n\t\t\t\t\ttransformPasted(slice) {\n\t\t\t\t\t\tlet content = normalizeHardBreaks(slice.content);\n\t\t\t\t\t\tif (!preserveMarks) {\n\t\t\t\t\t\t\tcontent = stripRichMarks(content);\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn new Slice(content, slice.openStart, slice.openEnd);\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}),\n\t\t];\n\t},\n});\n"],"names":["PasteNormalizationExtension","Extension","create","name","addOptions","preserveMarks","addProseMirrorPlugins","this","options","Plugin","props","decorations","state","doc","descendants","node","pos","type","childCount","push","Decoration","nodeSize","class","DecorationSet","handlePaste","view","event","html","_a","clipboardData","getData","includes","text","extractTextFromHtml","undefined","_b","slice","parseClipboardText","schema","dispatch","tr","replaceSelection","clipboardTextParser","_$context","_plain","transformPastedHTML","container","document","createElement","innerHTML","wrapInlineContent","cleanBlockBrs","stripTrailingBrs","transformPasted","content","normalizeHardBreaks","stripRichMarks","Slice","openStart","openEnd"],"mappings":"kQAyBaA,EAA8BC,EAASA,UAACC,OAAO,CAC3DC,KAAM,qBACNC,WAAUA,KACF,CACNC,eAAe,IAGjBC,wBACC,MAAMD,EAAgBE,KAAKC,QAAQH,cACnC,MAAO,CACN,IAAII,EAAAA,OAAO,CACVC,MAAO,CACNC,YAAYC,GACX,MAAMD,EAA4B,GAUlC,OATAC,EAAMC,IAAIC,aAAY,CAACC,EAAMC,KACL,cAAnBD,EAAKE,KAAKd,MAA4C,IAApBY,EAAKG,YAC1CP,EAAYQ,KACXC,aAAWL,KAAKC,EAAKA,EAAMD,EAAKM,SAAU,CACzCC,MAAO,aAGT,IAEKC,EAAaA,cAACrB,OAAOU,EAAMC,IAAKF,EACvC,EAEDa,YAAYC,EAAMC,WACjB,MAAMC,EAA0B,QAAnBC,EAAAF,EAAMG,qBAAa,IAAAD,OAAA,EAAAA,EAAEE,QAAQ,aAC1C,GAAIH,aAAI,EAAJA,EAAMI,SAAS,iBAAkB,OAAO,EAC5C,MAAMC,GACJL,EAAOM,EAAmBA,oBAACN,QAAQO,KACjB,QAAnBC,EAAAT,EAAMG,qBAAa,IAAAM,OAAA,EAAAA,EAAEL,QAAQ,eAC9B,IAAKE,EAAM,OAAO,EAClB,MAAMI,EAAQC,EAAAA,mBAAmBL,EAAMP,EAAKb,MAAM0B,QAElD,OADAb,EAAKc,SAASd,EAAKb,MAAM4B,GAAGC,iBAAiBL,KACtC,CACP,EAEDM,oBAAmBA,CAACV,EAAMW,EAAWC,EAAQnB,IACrCY,EAAkBA,mBAACL,EAAMP,EAAKb,MAAM0B,QAG5CO,oBAAoBlB,GACnB,GAAIA,EAAKI,SAAS,iBAAkB,OAAOJ,EAC3C,MAAMmB,EAAYC,SAASC,cAAc,OAKzC,OAJAF,EAAUG,UAAYtB,EACtBuB,EAAiBA,kBAACJ,GAClBK,EAAaA,cAACL,GACdM,EAAgBA,iBAACN,GACVA,EAAUG,SACjB,EAEDI,gBAAgBjB,GACf,IAAIkB,EAAUC,EAAAA,oBAAoBnB,EAAMkB,SAIxC,OAHKjD,IACJiD,EAAUE,EAAAA,eAAeF,IAEnB,IAAIG,EAAKA,MAACH,EAASlB,EAAMsB,UAAWtB,EAAMuB,QAClD,KAIJ"}
1
+ {"version":3,"file":"PasteNormalizationExtension.js","sources":["../../../../../src/editor/extensions/plainClipboard/PasteNormalizationExtension.ts"],"sourcesContent":["import { Extension } from '@tiptap/core';\nimport { Slice } from '@tiptap/pm/model';\nimport { Plugin } from '@tiptap/pm/state';\nimport {\n\tcleanBlockBrs,\n\tmirrorParagraphMargins,\n\tnormalizeHardBreaks,\n\tparseClipboardTextAsBreaks,\n\tstripRichMarks,\n\tstripTrailingBrs,\n\twrapInlineContent,\n} from './pasteUtils';\n\n/**\n * Unified paste normalizer for all editor channels.\n *\n * Uses ProseMirror's recommended hooks instead of overriding handlePaste:\n * - clipboardTextParser: plain text → Slice (one block, <br> per line)\n * - transformPastedHTML: HTML → HTML (cleans Google Docs / Word artifacts)\n * - transformPasted: Slice → Slice (normalize hardBreaks, strip marks,\n * mirror the source paragraph-margin convention)\n *\n * Margins MIRROR the source (like Gmail): they are not added structurally, so\n * ChatGPT-pasted `<p>` keeps its default margin (gaps) while Google-Docs and\n * plain/paste-without-formatting content stays margin:0 (tight).\n *\n * `appendTransaction` does two bounded touch-ups:\n * 1. Repairs the FIRST pasted paragraph — ProseMirror merges it into the\n * cursor's paragraph at paste time, dropping its margin attr; we record that\n * margin (in transformPasted / the plain handlePaste) and reapply it at the\n * paste position.\n * 2. Typed-Enter inheritance — splitBlock gives a paragraph created at a block\n * end DEFAULT attrs, so an empty paragraph that follows a margin:0 paragraph\n * is set to margin:0 too. Typing inside tight content stays tight; default\n * content stays default; non-empty (pasted) paragraphs are never touched.\n *\n * `plainOnly` (channels with no rich formatting — WhatsApp, IG, FB, comments)\n * forces EVERY paste through the plain-text path: even Cmd/Ctrl+V ignores the\n * clipboard HTML and pastes text only, since those channels don't support\n * formatting anyway. Email and the signature editor (richPaste) keep the full\n * HTML pipeline.\n */\nexport const PasteNormalizationExtension = Extension.create({\n\tname: 'pasteNormalization',\n\taddOptions() {\n\t\treturn {\n\t\t\tpreserveMarks: false,\n\t\t\tplainOnly: false,\n\t\t};\n\t},\n\taddProseMirrorPlugins() {\n\t\tconst preserveMarks = this.options.preserveMarks;\n\t\tconst plainOnly = this.options.plainOnly;\n\n\t\t// Margin of the first pasted paragraph, recorded at paste time so the\n\t\t// appendTransaction can restore it after ProseMirror merges that\n\t\t// paragraph into the destination (losing its attrs). `undefined` means\n\t\t// \"no paste pending\".\n\t\tlet pastedFirstMargin: string | null | undefined;\n\n\t\tconst recordFirstMargin = (content: { firstChild: any }) => {\n\t\t\tconst first = content.firstChild;\n\t\t\tpastedFirstMargin =\n\t\t\t\tfirst && first.type.name === 'paragraph'\n\t\t\t\t\t? first.attrs['margin'] ?? null\n\t\t\t\t\t: undefined;\n\t\t};\n\n\t\treturn [\n\t\t\tnew Plugin({\n\t\t\t\tappendTransaction(transactions, oldState, newState) {\n\t\t\t\t\tif (!transactions.some((t) => t.docChanged)) {\n\t\t\t\t\t\tpastedFirstMargin = undefined;\n\t\t\t\t\t\treturn null;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst tr = newState.tr;\n\t\t\t\t\tlet changed = false;\n\n\t\t\t\t\t// Job 1 — repair the first pasted paragraph. ProseMirror merges it\n\t\t\t\t\t// into the destination paragraph at paste time, dropping its margin\n\t\t\t\t\t// attr; reapply the recorded margin at the paste position.\n\t\t\t\t\tif (pastedFirstMargin !== undefined) {\n\t\t\t\t\t\tconst margin = pastedFirstMargin;\n\t\t\t\t\t\tlet pos = oldState.selection.from;\n\t\t\t\t\t\tfor (const t of transactions) pos = t.mapping.map(pos);\n\t\t\t\t\t\tconst $pos = newState.doc.resolve(\n\t\t\t\t\t\t\tMath.max(0, Math.min(pos, newState.doc.content.size)),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tfor (let d = $pos.depth; d >= 1; d--) {\n\t\t\t\t\t\t\tconst node = $pos.node(d);\n\t\t\t\t\t\t\tif (node.type.name === 'paragraph') {\n\t\t\t\t\t\t\t\tif ((node.attrs['margin'] ?? null) !== margin) {\n\t\t\t\t\t\t\t\t\ttr.setNodeMarkup($pos.before(d), undefined, {\n\t\t\t\t\t\t\t\t\t\t...node.attrs,\n\t\t\t\t\t\t\t\t\t\tmargin,\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\tchanged = true;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tpastedFirstMargin = undefined;\n\n\t\t\t\t\t// Job 2 — typed Enter inherits tightness. ProseMirror's splitBlock\n\t\t\t\t\t// gives a paragraph created at the end of a block DEFAULT attrs, so\n\t\t\t\t\t// pressing Enter in margin:0 content would otherwise produce a\n\t\t\t\t\t// default-margin (gap) paragraph. Make an EMPTY paragraph that\n\t\t\t\t\t// follows a margin:0 paragraph margin:0 too — so typing inside tight\n\t\t\t\t\t// (plain / Google-Docs) content stays tight, while default content\n\t\t\t\t\t// stays default. Non-empty paragraphs are never touched, so pasted\n\t\t\t\t\t// default-margin content is left alone.\n\t\t\t\t\tconst doc = tr.doc;\n\t\t\t\t\tlet prevMargin: string | null = null;\n\t\t\t\t\tdoc.forEach((node, pos) => {\n\t\t\t\t\t\tif (node.type.name !== 'paragraph') {\n\t\t\t\t\t\t\tprevMargin = null;\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tlet margin = (node.attrs['margin'] as string | null) ?? null;\n\t\t\t\t\t\tif (node.childCount === 0 && margin !== '0' && prevMargin === '0') {\n\t\t\t\t\t\t\ttr.setNodeMarkup(pos, undefined, { ...node.attrs, margin: '0' });\n\t\t\t\t\t\t\tmargin = '0';\n\t\t\t\t\t\t\tchanged = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tprevMargin = margin;\n\t\t\t\t\t});\n\n\t\t\t\t\treturn changed ? tr.setMeta('addToHistory', false) : null;\n\t\t\t\t},\n\t\t\t\tprops: {\n\t\t\t\t\t// plainOnly channels: ignore clipboard HTML entirely — every\n\t\t\t\t\t// paste (Cmd/Ctrl+V and Cmd/Ctrl+Shift+V) becomes plain text.\n\t\t\t\t\t...(plainOnly\n\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\thandlePaste(view, event) {\n\t\t\t\t\t\t\t\t\tconst text = event.clipboardData?.getData('text/plain') ?? '';\n\t\t\t\t\t\t\t\t\tif (!text) return false; // images / non-text: let others handle\n\t\t\t\t\t\t\t\t\tconst slice = parseClipboardTextAsBreaks(\n\t\t\t\t\t\t\t\t\t\ttext,\n\t\t\t\t\t\t\t\t\t\tview.state.schema,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\trecordFirstMargin(slice.content);\n\t\t\t\t\t\t\t\t\tview.dispatch(view.state.tr.replaceSelection(slice));\n\t\t\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t }\n\t\t\t\t\t\t: {}),\n\n\t\t\t\t\t// Plain-text paste (Cmd/Ctrl+Shift+V, which strips HTML): one\n\t\t\t\t\t// block with <br> per line and <br><br> per blank line — matches\n\t\t\t\t\t// Gmail's paste-without-formatting output exactly.\n\t\t\t\t\tclipboardTextParser(text, _$context, _plain, view) {\n\t\t\t\t\t\tconst slice = parseClipboardTextAsBreaks(text, view.state.schema);\n\t\t\t\t\t\trecordFirstMargin(slice.content);\n\t\t\t\t\t\treturn slice;\n\t\t\t\t\t},\n\n\t\t\t\t\ttransformPastedHTML(html) {\n\t\t\t\t\t\tif (html.includes('data-pm-slice')) return html;\n\t\t\t\t\t\tconst container = document.createElement('div');\n\t\t\t\t\t\tcontainer.innerHTML = html;\n\t\t\t\t\t\twrapInlineContent(container);\n\t\t\t\t\t\tcleanBlockBrs(container);\n\t\t\t\t\t\tstripTrailingBrs(container);\n\t\t\t\t\t\treturn container.innerHTML;\n\t\t\t\t\t},\n\n\t\t\t\t\ttransformPasted(slice) {\n\t\t\t\t\t\tlet content = normalizeHardBreaks(slice.content);\n\t\t\t\t\t\tif (!preserveMarks) {\n\t\t\t\t\t\t\tcontent = stripRichMarks(content);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcontent = mirrorParagraphMargins(content);\n\t\t\t\t\t\trecordFirstMargin(content);\n\t\t\t\t\t\treturn new Slice(content, slice.openStart, slice.openEnd);\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}),\n\t\t];\n\t},\n});\n"],"names":["PasteNormalizationExtension","Extension","create","name","addOptions","preserveMarks","plainOnly","addProseMirrorPlugins","this","options","pastedFirstMargin","recordFirstMargin","content","first","firstChild","type","attrs","_a","undefined","Plugin","appendTransaction","transactions","oldState","newState","some","t","docChanged","tr","changed","margin","pos","selection","from","mapping","map","$pos","doc","resolve","Math","max","min","size","d","depth","node","setNodeMarkup","before","prevMargin","forEach","childCount","Object","assign","setMeta","props","handlePaste","view","event","text","_b","clipboardData","getData","slice","parseClipboardTextAsBreaks","state","schema","dispatch","replaceSelection","clipboardTextParser","_$context","_plain","transformPastedHTML","html","includes","container","document","createElement","innerHTML","wrapInlineContent","cleanBlockBrs","stripTrailingBrs","transformPasted","normalizeHardBreaks","stripRichMarks","mirrorParagraphMargins","Slice","openStart","openEnd"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("@tiptap/pm/model");const t=new Set(["P","LI","H1","H2","H3","H4","H5","H6","TD","TH","PRE"]),r="p,div,h1,h2,h3,h4,h5,h6,ul,ol,li,blockquote,table,pre",n=new Set(["bold","italic","strike","underline","code"]);function o(e){if(e.querySelector(r))return;const t=Array.from(e.childNodes),n=[[]];for(let e=0;e<t.length;e++){const r=t[e];r instanceof HTMLBRElement&&t[e+1]instanceof HTMLBRElement?(n.push([]),e++):n[n.length-1].push(r)}for(;e.firstChild;)e.firstChild.remove();for(const t of n){const r=document.createElement("p");for(const e of t)r.appendChild(e);e.appendChild(r)}}function l(e){const r=Array.from(e.querySelectorAll("br"));for(const n of r){let r=!1,o=n.parentElement;for(;o&&o!==e;){if(t.has(o.tagName)){r=!0;break}o=o.parentElement}r||n.parentElement.replaceChild(document.createElement("p"),n)}}function a(e){for(const t of["p","div"])for(const r of Array.from(e.querySelectorAll(t))){const e=r.lastElementChild;"BR"===(null==e?void 0:e.tagName)&&r.childNodes.length>1&&e.remove()}}exports.BLOCK_SELECTOR=r,exports.TEXTBLOCK_TAGS=t,exports.cleanBlockBrs=l,exports.extractTextFromHtml=function(e){const t=document.createElement("div");t.innerHTML=e,o(t),l(t),a(t);const r="p,h1,h2,h3,h4,h5,h6,li,pre,td,th",n=Array.from(t.querySelectorAll(r));if(0===n.length)return;return n.filter((e=>!e.querySelector(r))).map((e=>e.textContent||"")).join("\n")},exports.normalizeHardBreaks=function t(r){const n=[];return r.forEach((r=>{var o,l;if("paragraph"===r.type.name){if(1===r.childCount&&"hardBreak"===(null===(o=r.firstChild)||void 0===o?void 0:o.type.name))return void n.push(r.type.create(r.attrs));if(r.childCount>1&&"hardBreak"===(null===(l=r.lastChild)||void 0===l?void 0:l.type.name)){const t=[];return r.content.forEach(((e,n,o)=>{o<r.childCount-1&&t.push(e)})),void n.push(r.copy(e.Fragment.from(t)))}}r.isBlock&&!r.isLeaf?n.push(r.copy(t(r.content))):n.push(r)})),e.Fragment.from(n)},exports.parseClipboardText=function(t,r){const n=t.split("\n").map((e=>r.node("paragraph",null,e?[r.text(e)]:[])));return new e.Slice(e.Fragment.fromArray(n),1,1)},exports.stripRichMarks=function t(r){const o=[];return r.forEach((e=>{if(e.isText){const t=e.marks.filter((e=>n.has(e.type.name)));o.push(t.length===e.marks.length?e:e.mark(t))}else o.push(e.copy(t(e.content)))})),e.Fragment.from(o)},exports.stripTrailingBrs=a,exports.wrapInlineContent=o;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("@tiptap/pm/model");const r=new Set(["P","LI","H1","H2","H3","H4","H5","H6","TD","TH","PRE"]),t="p,div,h1,h2,h3,h4,h5,h6,ul,ol,li,blockquote,table,pre",n=new Set(["bold","italic","strike","underline","code"]);function o(e){return e.replace(/\r\n?/g,"\n")}exports.BLOCK_SELECTOR=t,exports.TEXTBLOCK_TAGS=r,exports.cleanBlockBrs=function(e){const t=Array.from(e.querySelectorAll("br"));for(const n of t){let t=!1,o=n.parentElement;for(;o&&o!==e;){if(r.has(o.tagName)){t=!0;break}o=o.parentElement}t||n.parentElement.replaceChild(document.createElement("p"),n)}},exports.mirrorParagraphMargins=function(r){let t=!1;if(r.forEach((e=>{"paragraph"===e.type.name&&"0"===e.attrs.margin&&(t=!0)})),!t)return r;const n=[];return r.forEach((e=>{"paragraph"===e.type.name&&"0"!==e.attrs.margin?n.push(e.type.create(Object.assign(Object.assign({},e.attrs),{margin:"0"}),e.content,e.marks)):n.push(e)})),e.Fragment.from(n)},exports.normalizeHardBreaks=function r(t){const n=[];return t.forEach((e=>{var t;"paragraph"!==e.type.name||1!==e.childCount||"hardBreak"!==(null===(t=e.firstChild)||void 0===t?void 0:t.type.name)?e.isBlock&&!e.isLeaf?n.push(e.copy(r(e.content))):n.push(e):n.push(e.type.create(e.attrs))})),e.Fragment.from(n)},exports.normalizeNewlines=o,exports.parseClipboardTextAsBreaks=function(r,t){const n=e=>t.node("paragraph",{margin:"0"},e),a=o(r).replace(/\n+$/,"");if(!a)return new e.Slice(e.Fragment.from(n([])),1,1);const s=t.nodes.hardBreak,i=a.split("\n"),l=[];return i.forEach(((e,r)=>{e&&l.push(t.text(e)),r<i.length-1&&s&&l.push(s.create())})),new e.Slice(e.Fragment.from(n(l)),1,1)},exports.stripRichMarks=function r(t){const o=[];return t.forEach((e=>{if(e.isText){const r=e.marks.filter((e=>n.has(e.type.name)));o.push(r.length===e.marks.length?e:e.mark(r))}else o.push(e.copy(r(e.content)))})),e.Fragment.from(o)},exports.stripTrailingBrs=function(e){for(const r of["p","div"])for(const t of Array.from(e.querySelectorAll(r))){const e=t.lastElementChild;"BR"===(null==e?void 0:e.tagName)&&t.childNodes.length>1&&e.remove()}},exports.wrapInlineContent=function(e){if(e.querySelector(t))return;const r=Array.from(e.childNodes),n=[[]];for(let e=0;e<r.length;e++){const t=r[e];t instanceof HTMLBRElement&&r[e+1]instanceof HTMLBRElement?(n.push([]),e++):n[n.length-1].push(t)}for(;e.firstChild;)e.firstChild.remove();for(const r of n){const t=document.createElement("p");for(const e of r)t.appendChild(e);e.appendChild(t)}};
2
2
  //# sourceMappingURL=pasteUtils.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"pasteUtils.js","sources":["../../../../../src/editor/extensions/plainClipboard/pasteUtils.ts"],"sourcesContent":["import { Fragment, Node as PMNode, Schema, Slice } from '@tiptap/pm/model';\n\nexport const TEXTBLOCK_TAGS = new Set([\n\t'P',\n\t'LI',\n\t'H1',\n\t'H2',\n\t'H3',\n\t'H4',\n\t'H5',\n\t'H6',\n\t'TD',\n\t'TH',\n\t'PRE',\n]);\n\nexport const BLOCK_SELECTOR =\n\t'p,div,h1,h2,h3,h4,h5,h6,ul,ol,li,blockquote,table,pre';\n\nconst BASIC_MARKS = new Set(['bold', 'italic', 'strike', 'underline', 'code']);\n\n/**\n * When pasted HTML is entirely inline (no block elements), split at\n * `<br><br>` boundaries into `<p>` elements. Single `<br>` stays as a\n * line break inside the paragraph.\n */\nexport function wrapInlineContent(container: HTMLElement): void {\n\tif (container.querySelector(BLOCK_SELECTOR)) return;\n\n\tconst children = Array.from(container.childNodes);\n\tconst groups: Node[][] = [[]];\n\n\tfor (let i = 0; i < children.length; i++) {\n\t\tconst node = children[i];\n\t\tif (\n\t\t\tnode instanceof HTMLBRElement &&\n\t\t\tchildren[i + 1] instanceof HTMLBRElement\n\t\t) {\n\t\t\tgroups.push([]);\n\t\t\ti++;\n\t\t\tcontinue;\n\t\t}\n\t\tgroups[groups.length - 1].push(node);\n\t}\n\n\twhile (container.firstChild) container.firstChild.remove();\n\n\tfor (const group of groups) {\n\t\tconst p = document.createElement('p');\n\t\tfor (const node of group) p.appendChild(node);\n\t\tcontainer.appendChild(p);\n\t}\n}\n\n/**\n * Replace `<br>` elements that sit at block level (not inside a textblock\n * like `<p>`, `<li>`, etc.) with empty `<p></p>` elements. Google Docs\n * puts standalone `<br>` tags between paragraphs to represent blank lines.\n */\nexport function cleanBlockBrs(container: HTMLElement): void {\n\tconst brs = Array.from(container.querySelectorAll('br'));\n\tfor (const br of brs) {\n\t\tlet insideTextblock = false;\n\t\tlet el = br.parentElement;\n\t\twhile (el && el !== container) {\n\t\t\tif (TEXTBLOCK_TAGS.has(el.tagName)) {\n\t\t\t\tinsideTextblock = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tel = el.parentElement;\n\t\t}\n\t\tif (!insideTextblock) {\n\t\t\tbr.parentElement!.replaceChild(document.createElement('p'), br);\n\t\t}\n\t}\n}\n\n/**\n * Strip trailing `<br>` from content paragraphs in the DOM.\n * Google Docs adds a `<br>` at the end of non-empty paragraphs as a cursor\n * placeholder. Without this, ProseMirror creates a trailing hardBreak that\n * adds a visual blank line at the bottom of the paragraph.\n */\nexport function stripTrailingBrs(container: HTMLElement): void {\n\tfor (const tag of ['p', 'div']) {\n\t\tfor (const el of Array.from(container.querySelectorAll(tag))) {\n\t\t\tconst last = el.lastElementChild;\n\t\t\tif (last?.tagName === 'BR' && el.childNodes.length > 1) {\n\t\t\t\tlast.remove();\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Convert paragraphs containing only a hardBreak into truly empty paragraphs.\n * Google Docs represents blank lines as `<p><br></p>` — ProseMirror parses\n * the `<br>` as a hardBreak node, which renders double-height. Converting to\n * an empty paragraph (childCount 0) gives a single-height blank line.\n *\n * Does NOT collapse consecutive empties — that would destroy user intent.\n */\nexport function normalizeHardBreaks(fragment: Fragment): Fragment {\n\tconst nodes: PMNode[] = [];\n\tfragment.forEach((node) => {\n\t\tif (node.type.name === 'paragraph') {\n\t\t\tif (node.childCount === 1 && node.firstChild?.type.name === 'hardBreak') {\n\t\t\t\tnodes.push(node.type.create(node.attrs));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (node.childCount > 1 && node.lastChild?.type.name === 'hardBreak') {\n\t\t\t\tconst children: PMNode[] = [];\n\t\t\t\tnode.content.forEach((child, _offset, index) => {\n\t\t\t\t\tif (index < node.childCount - 1) children.push(child);\n\t\t\t\t});\n\t\t\t\tnodes.push(node.copy(Fragment.from(children)));\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tif (node.isBlock && !node.isLeaf) {\n\t\t\tnodes.push(node.copy(normalizeHardBreaks(node.content)));\n\t\t} else {\n\t\t\tnodes.push(node);\n\t\t}\n\t});\n\treturn Fragment.from(nodes);\n}\n\n/**\n * Strip \"rich\" marks (link, textStyle, highlight, etc.) while keeping\n * basic marks (bold, italic, strike, underline, code) that messaging\n * channels can represent.\n */\nexport function stripRichMarks(fragment: Fragment): Fragment {\n\tconst nodes: PMNode[] = [];\n\tfragment.forEach((node) => {\n\t\tif (node.isText) {\n\t\t\tconst kept = node.marks.filter((m) => BASIC_MARKS.has(m.type.name));\n\t\t\tnodes.push(kept.length === node.marks.length ? node : node.mark(kept));\n\t\t} else {\n\t\t\tnodes.push(node.copy(stripRichMarks(node.content)));\n\t\t}\n\t});\n\treturn Fragment.from(nodes);\n}\n\n/**\n * Extract plain text from clipboard HTML, using the DOM structure for\n * paragraph boundaries instead of relying on `text/plain` (which differs\n * between Mac and Windows for the same source, e.g. Google Docs).\n *\n * Reuses the existing HTML normalizers so `<br>` between blocks becomes an\n * empty `<p>`, then pulls `textContent` from each leaf block element.\n * Returns `undefined` when no block structure is found so the caller can\n * fall back to `text/plain`.\n */\nexport function extractTextFromHtml(html: string): string | undefined {\n\tconst container = document.createElement('div');\n\tcontainer.innerHTML = html;\n\n\twrapInlineContent(container);\n\tcleanBlockBrs(container);\n\tstripTrailingBrs(container);\n\n\tconst selector = 'p,h1,h2,h3,h4,h5,h6,li,pre,td,th';\n\tconst blocks = Array.from(container.querySelectorAll(selector));\n\n\tif (blocks.length === 0) return undefined;\n\n\tconst leafBlocks = blocks.filter((el) => !el.querySelector(selector));\n\n\treturn leafBlocks.map((el) => el.textContent || '').join('\\n');\n}\n\n/**\n * Parse plain clipboard text into a ProseMirror Slice, preserving blank\n * lines as empty paragraphs. This replaces ProseMirror's default text\n * parser which uses `\\n+` and loses all blank lines.\n *\n * Best for channels with compact paragraph gaps (e.g. WhatsApp) where\n * blank lines need structural representation as empty paragraphs.\n */\nexport function parseClipboardText(text: string, schema: Schema): Slice {\n\tconst paragraphs = text\n\t\t.split('\\n')\n\t\t.map((line) =>\n\t\t\tschema.node('paragraph', null, line ? [schema.text(line)] : []),\n\t\t);\n\treturn new Slice(Fragment.fromArray(paragraphs), 1, 1);\n}\n\n/**\n * Parse plain clipboard text into a single paragraph with hardBreaks for\n * every `\\n`. Produces the same structure as Gmail's Cmd+Shift+V output:\n * one block element with `<br>` for line breaks and `<br><br>` for blank\n * lines. This gives identical html and text output to Gmail.\n */\nexport function parseClipboardTextAsBreaks(\n\ttext: string,\n\tschema: Schema,\n): Slice {\n\tconst trimmed = text.replace(/\\n+$/, '');\n\tif (!trimmed) {\n\t\treturn new Slice(Fragment.from(schema.node('paragraph', null, [])), 1, 1);\n\t}\n\tconst hardBreak = schema.nodes['hardBreak'];\n\tconst lines = trimmed.split('\\n');\n\tconst children: PMNode[] = [];\n\tlines.forEach((line, i) => {\n\t\tif (line) children.push(schema.text(line));\n\t\tif (i < lines.length - 1 && hardBreak) {\n\t\t\tchildren.push(hardBreak.create());\n\t\t}\n\t});\n\treturn new Slice(\n\t\tFragment.from(\n\t\t\tschema.node('paragraph', null, children.length ? children : []),\n\t\t),\n\t\t1,\n\t\t1,\n\t);\n}\n"],"names":["TEXTBLOCK_TAGS","Set","BLOCK_SELECTOR","BASIC_MARKS","wrapInlineContent","container","querySelector","children","Array","from","childNodes","groups","i","length","node","HTMLBRElement","push","firstChild","remove","group","p","document","createElement","appendChild","cleanBlockBrs","brs","querySelectorAll","br","insideTextblock","el","parentElement","has","tagName","replaceChild","stripTrailingBrs","tag","last","lastElementChild","html","innerHTML","selector","blocks","filter","map","textContent","join","normalizeHardBreaks","fragment","nodes","forEach","type","name","childCount","_a","create","attrs","_b","lastChild","content","child","_offset","index","copy","Fragment","isBlock","isLeaf","text","schema","paragraphs","split","line","Slice","fromArray","stripRichMarks","isText","kept","marks","m","mark"],"mappings":"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"}
1
+ {"version":3,"file":"pasteUtils.js","sources":["../../../../../src/editor/extensions/plainClipboard/pasteUtils.ts"],"sourcesContent":["import { Fragment, Node as PMNode, Schema, Slice } from '@tiptap/pm/model';\n\nexport const TEXTBLOCK_TAGS = new Set([\n\t'P',\n\t'LI',\n\t'H1',\n\t'H2',\n\t'H3',\n\t'H4',\n\t'H5',\n\t'H6',\n\t'TD',\n\t'TH',\n\t'PRE',\n]);\n\nexport const BLOCK_SELECTOR =\n\t'p,div,h1,h2,h3,h4,h5,h6,ul,ol,li,blockquote,table,pre';\n\nconst BASIC_MARKS = new Set(['bold', 'italic', 'strike', 'underline', 'code']);\n\n/** Normalise clipboard newlines: Windows CRLF and lone CR → LF. */\nexport function normalizeNewlines(text: string): string {\n\treturn text.replace(/\\r\\n?/g, '\\n');\n}\n\n/**\n * When pasted HTML is entirely inline (no block elements), split at\n * `<br><br>` boundaries into `<p>` elements. Single `<br>` stays as a\n * line break inside the paragraph.\n */\nexport function wrapInlineContent(container: HTMLElement): void {\n\tif (container.querySelector(BLOCK_SELECTOR)) return;\n\n\tconst children = Array.from(container.childNodes);\n\tconst groups: Node[][] = [[]];\n\n\tfor (let i = 0; i < children.length; i++) {\n\t\tconst node = children[i];\n\t\tif (\n\t\t\tnode instanceof HTMLBRElement &&\n\t\t\tchildren[i + 1] instanceof HTMLBRElement\n\t\t) {\n\t\t\tgroups.push([]);\n\t\t\ti++;\n\t\t\tcontinue;\n\t\t}\n\t\tgroups[groups.length - 1].push(node);\n\t}\n\n\twhile (container.firstChild) container.firstChild.remove();\n\n\tfor (const group of groups) {\n\t\tconst p = document.createElement('p');\n\t\tfor (const node of group) p.appendChild(node);\n\t\tcontainer.appendChild(p);\n\t}\n}\n\n/**\n * Replace `<br>` elements that sit at block level (not inside a textblock\n * like `<p>`, `<li>`, etc.) with empty `<p></p>` elements. Google Docs\n * puts standalone `<br>` tags between paragraphs to represent blank lines.\n */\nexport function cleanBlockBrs(container: HTMLElement): void {\n\tconst brs = Array.from(container.querySelectorAll('br'));\n\tfor (const br of brs) {\n\t\tlet insideTextblock = false;\n\t\tlet el = br.parentElement;\n\t\twhile (el && el !== container) {\n\t\t\tif (TEXTBLOCK_TAGS.has(el.tagName)) {\n\t\t\t\tinsideTextblock = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tel = el.parentElement;\n\t\t}\n\t\tif (!insideTextblock) {\n\t\t\tbr.parentElement!.replaceChild(document.createElement('p'), br);\n\t\t}\n\t}\n}\n\n/**\n * Strip trailing `<br>` from content paragraphs in the DOM.\n * Google Docs adds a `<br>` at the end of non-empty paragraphs as a cursor\n * placeholder. Without this, ProseMirror creates a trailing hardBreak that\n * adds a visual blank line at the bottom of the paragraph.\n */\nexport function stripTrailingBrs(container: HTMLElement): void {\n\tfor (const tag of ['p', 'div']) {\n\t\tfor (const el of Array.from(container.querySelectorAll(tag))) {\n\t\t\tconst last = el.lastElementChild;\n\t\t\tif (last?.tagName === 'BR' && el.childNodes.length > 1) {\n\t\t\t\tlast.remove();\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Convert a paragraph whose ONLY child is a hardBreak (`<p><br></p>`) into a\n * truly empty paragraph. ProseMirror parses the `<br>` as a hardBreak node AND\n * adds its own cursor `<br>`, rendering double-height; an empty paragraph\n * (childCount 0) gives a single-height blank line.\n *\n * Does NOT strip trailing hardBreaks from content paragraphs, and does NOT\n * collapse consecutive empties — those `<br>`s/blank lines are intentional\n * (e.g. shift+enter / enter runs from Google Docs) and Gmail preserves them.\n */\nexport function normalizeHardBreaks(fragment: Fragment): Fragment {\n\tconst nodes: PMNode[] = [];\n\tfragment.forEach((node) => {\n\t\tif (node.type.name === 'paragraph') {\n\t\t\tif (node.childCount === 1 && node.firstChild?.type.name === 'hardBreak') {\n\t\t\t\tnodes.push(node.type.create(node.attrs));\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tif (node.isBlock && !node.isLeaf) {\n\t\t\tnodes.push(node.copy(normalizeHardBreaks(node.content)));\n\t\t} else {\n\t\t\tnodes.push(node);\n\t\t}\n\t});\n\treturn Fragment.from(nodes);\n}\n\n/**\n * Mirror the source paragraph-margin convention across a pasted block.\n *\n * Gmail decides spacing from the SOURCE: it keeps default margins for content\n * pasted from ChatGPT/plain `<p>`, and `margin:0` for Google-Docs-style content\n * (whose paragraphs carry `margin:0`). We do the same. A paragraph's margin is\n * already preserved from the source (the Paragraph node's `margin` attribute),\n * so the only gap is the synthesized blank-line paragraphs (created from a\n * Google-Docs block-level `<br>`), which have no source margin. If the pasted\n * block is margin:0-style (any paragraph carries margin:0), apply margin:0 to\n * every paragraph of THIS paste — so the blank lines match. Pastes with no\n * margin:0 paragraph (ChatGPT, plain text) are left untouched → default margins.\n * Scoped to the pasted slice, so it never tightens existing/typed content.\n */\nexport function mirrorParagraphMargins(fragment: Fragment): Fragment {\n\tlet isMarginZeroBlock = false;\n\tfragment.forEach((node) => {\n\t\tif (node.type.name === 'paragraph' && node.attrs['margin'] === '0') {\n\t\t\tisMarginZeroBlock = true;\n\t\t}\n\t});\n\tif (!isMarginZeroBlock) return fragment;\n\n\tconst nodes: PMNode[] = [];\n\tfragment.forEach((node) => {\n\t\tif (node.type.name === 'paragraph' && node.attrs['margin'] !== '0') {\n\t\t\tnodes.push(\n\t\t\t\tnode.type.create(\n\t\t\t\t\t{ ...node.attrs, margin: '0' },\n\t\t\t\t\tnode.content,\n\t\t\t\t\tnode.marks,\n\t\t\t\t),\n\t\t\t);\n\t\t} else {\n\t\t\tnodes.push(node);\n\t\t}\n\t});\n\treturn Fragment.from(nodes);\n}\n\n/**\n * Strip \"rich\" marks (link, textStyle, highlight, etc.) while keeping\n * basic marks (bold, italic, strike, underline, code) that messaging\n * channels can represent.\n */\nexport function stripRichMarks(fragment: Fragment): Fragment {\n\tconst nodes: PMNode[] = [];\n\tfragment.forEach((node) => {\n\t\tif (node.isText) {\n\t\t\tconst kept = node.marks.filter((m) => BASIC_MARKS.has(m.type.name));\n\t\t\tnodes.push(kept.length === node.marks.length ? node : node.mark(kept));\n\t\t} else {\n\t\t\tnodes.push(node.copy(stripRichMarks(node.content)));\n\t\t}\n\t});\n\treturn Fragment.from(nodes);\n}\n\n/**\n * Parse plain clipboard text into a ProseMirror Slice, preserving blank\n * lines as empty paragraphs. This replaces ProseMirror's default text\n * parser which uses `\\n+` and loses all blank lines.\n *\n * Best for channels with compact paragraph gaps (e.g. WhatsApp) where\n * blank lines need structural representation as empty paragraphs.\n */\nexport function parseClipboardText(text: string, schema: Schema): Slice {\n\t// Normalise Windows CRLF (and lone CR) to LF first — otherwise the stray\n\t// \\r survives splitting and turns blank lines into whitespace paragraphs.\n\tconst paragraphs = normalizeNewlines(text)\n\t\t.split('\\n')\n\t\t.map((line) =>\n\t\t\tschema.node('paragraph', null, line ? [schema.text(line)] : []),\n\t\t);\n\treturn new Slice(Fragment.fromArray(paragraphs), 1, 1);\n}\n\n/**\n * Parse plain clipboard text into a single paragraph with hardBreaks for\n * every `\\n`. Produces the same structure as Gmail's Cmd+Shift+V output:\n * one block element with `<br>` for line breaks and `<br><br>` for blank\n * lines. This gives identical html and text output to Gmail.\n *\n * The paragraph is tight (`margin: '0'`) — plain/paste-without-formatting is\n * Gmail's `<div>` (no margin), and typed Enter inherits this margin, so\n * splitting plain-pasted text stays tight like Gmail (not default-margin gaps).\n */\nexport function parseClipboardTextAsBreaks(\n\ttext: string,\n\tschema: Schema,\n): Slice {\n\tconst para = (children: PMNode[]) =>\n\t\tschema.node('paragraph', { margin: '0' }, children);\n\t// Normalise Windows CRLF (and lone CR) to LF, then drop trailing blank\n\t// lines so a copied source's terminal newline doesn't add an empty line.\n\tconst trimmed = normalizeNewlines(text).replace(/\\n+$/, '');\n\tif (!trimmed) {\n\t\treturn new Slice(Fragment.from(para([])), 1, 1);\n\t}\n\tconst hardBreak = schema.nodes['hardBreak'];\n\tconst lines = trimmed.split('\\n');\n\tconst children: PMNode[] = [];\n\tlines.forEach((line, i) => {\n\t\tif (line) children.push(schema.text(line));\n\t\tif (i < lines.length - 1 && hardBreak) {\n\t\t\tchildren.push(hardBreak.create());\n\t\t}\n\t});\n\treturn new Slice(Fragment.from(para(children)), 1, 1);\n}\n"],"names":["TEXTBLOCK_TAGS","Set","BLOCK_SELECTOR","BASIC_MARKS","normalizeNewlines","text","replace","container","brs","Array","from","querySelectorAll","br","insideTextblock","el","parentElement","has","tagName","replaceChild","document","createElement","fragment","isMarginZeroBlock","forEach","node","type","name","attrs","nodes","push","create","Object","assign","margin","content","marks","Fragment","normalizeHardBreaks","childCount","_a","firstChild","isBlock","isLeaf","copy","schema","para","children","trimmed","Slice","hardBreak","lines","split","line","i","length","stripRichMarks","isText","kept","filter","m","mark","tag","last","lastElementChild","childNodes","remove","querySelector","groups","HTMLBRElement","group","p","appendChild"],"mappings":"sGAEaA,MAAAA,EAAiB,IAAIC,IAAI,CACrC,IACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,KACA,QAGYC,EACZ,wDAEKC,EAAc,IAAIF,IAAI,CAAC,OAAQ,SAAU,SAAU,YAAa,SAGhE,SAAUG,EAAkBC,GACjC,OAAOA,EAAKC,QAAQ,SAAU,KAC/B,yEAwCM,SAAwBC,GAC7B,MAAMC,EAAMC,MAAMC,KAAKH,EAAUI,iBAAiB,OAClD,IAAK,MAAMC,KAAMJ,EAAK,CACrB,IAAIK,GAAkB,EAClBC,EAAKF,EAAGG,cACZ,KAAOD,GAAMA,IAAOP,GAAW,CAC9B,GAAIP,EAAegB,IAAIF,EAAGG,SAAU,CACnCJ,GAAkB,EAClB,KACA,CACDC,EAAKA,EAAGC,aACR,CACIF,GACJD,EAAGG,cAAeG,aAAaC,SAASC,cAAc,KAAMR,EAE7D,CACF,iCA6DM,SAAiCS,GACtC,IAAIC,GAAoB,EAMxB,GALAD,EAASE,SAASC,IACM,cAAnBA,EAAKC,KAAKC,MAAiD,MAAzBF,EAAKG,MAAc,SACxDL,GAAoB,EACpB,KAEGA,EAAmB,OAAOD,EAE/B,MAAMO,EAAkB,GAcxB,OAbAP,EAASE,SAASC,IACM,cAAnBA,EAAKC,KAAKC,MAAiD,MAAzBF,EAAKG,MAAc,OACxDC,EAAMC,KACLL,EAAKC,KAAKK,OAAMC,OAAAC,OAAAD,OAAAC,OAAA,CAAA,EACVR,EAAKG,OAAK,CAAEM,OAAQ,MACzBT,EAAKU,QACLV,EAAKW,QAIPP,EAAMC,KAAKL,EACX,IAEKY,EAAQA,SAAC1B,KAAKkB,EACtB,8BAxDM,SAAUS,EAAoBhB,GACnC,MAAMO,EAAkB,GAcxB,OAbAP,EAASE,SAASC,UACM,cAAnBA,EAAKC,KAAKC,MACW,IAApBF,EAAKc,YAAmD,eAAd,QAAjBC,EAAAf,EAAKgB,kBAAY,IAAAD,OAAA,EAAAA,EAAAd,KAAKC,MAKhDF,EAAKiB,UAAYjB,EAAKkB,OACzBd,EAAMC,KAAKL,EAAKmB,KAAKN,EAAoBb,EAAKU,WAE9CN,EAAMC,KAAKL,GAPVI,EAAMC,KAAKL,EAAKC,KAAKK,OAAON,EAAKG,OAQlC,IAEKS,EAAQA,SAAC1B,KAAKkB,EACtB,iEAyFgB,SACfvB,EACAuC,GAEA,MAAMC,EAAQC,GACbF,EAAOpB,KAAK,YAAa,CAAES,OAAQ,KAAOa,GAGrCC,EAAU3C,EAAkBC,GAAMC,QAAQ,OAAQ,IACxD,IAAKyC,EACJ,OAAO,IAAIC,EAAAA,MAAMZ,EAAQA,SAAC1B,KAAKmC,EAAK,KAAM,EAAG,GAE9C,MAAMI,EAAYL,EAAOhB,MAAiB,UACpCsB,EAAQH,EAAQI,MAAM,MACtBL,EAAqB,GAO3B,OANAI,EAAM3B,SAAQ,CAAC6B,EAAMC,KAChBD,GAAMN,EAASjB,KAAKe,EAAOvC,KAAK+C,IAChCC,EAAIH,EAAMI,OAAS,GAAKL,GAC3BH,EAASjB,KAAKoB,EAAUnB,SACxB,IAEK,IAAIkB,EAAAA,MAAMZ,EAAQA,SAAC1B,KAAKmC,EAAKC,IAAY,EAAG,EACpD,yBAhEM,SAAUS,EAAelC,GAC9B,MAAMO,EAAkB,GASxB,OARAP,EAASE,SAASC,IACjB,GAAIA,EAAKgC,OAAQ,CAChB,MAAMC,EAAOjC,EAAKW,MAAMuB,QAAQC,GAAMxD,EAAYa,IAAI2C,EAAElC,KAAKC,QAC7DE,EAAMC,KAAK4B,EAAKH,SAAW9B,EAAKW,MAAMmB,OAAS9B,EAAOA,EAAKoC,KAAKH,GAChE,MACA7B,EAAMC,KAAKL,EAAKmB,KAAKY,EAAe/B,EAAKU,UACzC,IAEKE,EAAQA,SAAC1B,KAAKkB,EACtB,2BA/FM,SAA2BrB,GAChC,IAAK,MAAMsD,IAAO,CAAC,IAAK,OACvB,IAAK,MAAM/C,KAAML,MAAMC,KAAKH,EAAUI,iBAAiBkD,IAAO,CAC7D,MAAMC,EAAOhD,EAAGiD,iBACM,QAAlBD,aAAA,EAAAA,EAAM7C,UAAoBH,EAAGkD,WAAWV,OAAS,GACpDQ,EAAKG,QAEN,CAEH,4BAlEM,SAA4B1D,GACjC,GAAIA,EAAU2D,cAAchE,GAAiB,OAE7C,MAAM4C,EAAWrC,MAAMC,KAAKH,EAAUyD,YAChCG,EAAmB,CAAC,IAE1B,IAAK,IAAId,EAAI,EAAGA,EAAIP,EAASQ,OAAQD,IAAK,CACzC,MAAM7B,EAAOsB,EAASO,GAErB7B,aAAgB4C,eAChBtB,EAASO,EAAI,aAAce,eAE3BD,EAAOtC,KAAK,IACZwB,KAGDc,EAAOA,EAAOb,OAAS,GAAGzB,KAAKL,EAC/B,CAED,KAAOjB,EAAUiC,YAAYjC,EAAUiC,WAAWyB,SAElD,IAAK,MAAMI,KAASF,EAAQ,CAC3B,MAAMG,EAAInD,SAASC,cAAc,KACjC,IAAK,MAAMI,KAAQ6C,EAAOC,EAAEC,YAAY/C,GACxCjB,EAAUgE,YAAYD,EACtB,CACF"}
@@ -1,5 +1,5 @@
1
1
  export declare const BikEditorShell: import("styled-components").StyledComponent<"div", any, {
2
2
  minHeight?: string | undefined;
3
3
  maxHeight?: string | undefined;
4
- paragraphGap?: string | undefined;
4
+ tightParagraphs?: boolean | undefined;
5
5
  }, never>;
@@ -728,6 +728,4 @@ export interface BikEditorProps {
728
728
  minHeight?: string;
729
729
  /** Maximum height. The editor scrolls internally when exceeded. CSS value, e.g. `'400px'`. */
730
730
  maxHeight?: string;
731
- /** Gap between consecutive paragraphs. CSS value, defaults to `'4px'`. */
732
- paragraphGap?: string;
733
731
  }
@@ -3,14 +3,20 @@ import { EditorSnapshot, FormatState } from './BikEditor.types';
3
3
  /**
4
4
  * Normalise HTML before passing it to TipTap.
5
5
  *
6
- * 1. Strip all `<span>` tags first (keeps inner content). This exposes
7
- * bare `<br>` inside otherwise-empty paragraphs like Quill's
8
- * `<p><span class="ql-cursor"><br></span></p>`.
9
- * 2. Then convert `<p><br></p>` `<p></p>`. ProseMirror parses `<p></p>`
10
- * as an empty paragraph node and adds its own DOM `<br>` for cursor
11
- * positioning one blank line, correct height. Leaving the `<br>`
12
- * would create a `hard_break` child AND the cursor `<br>`, producing
13
- * double-height blank lines.
6
+ * 1. Unwrap style-less `<span>` wrappers (plain spans, and editor-cursor spans
7
+ * like Quill's `<span class="ql-cursor">`). Spans that carry inline styling
8
+ * (`color` / `font-family` / `font-size` render as styled spans) or that
9
+ * represent a `{{variable}}` token are PRESERVED otherwise a setContent
10
+ * round-trip would silently drop colour/font formatting that paste keeps.
11
+ * 2. Collapse placeholder paragraphs (`<p><br></p>`) to `<p></p>`. ProseMirror
12
+ * parses `<p></p>` as an empty paragraph and adds its own DOM `<br>` for the
13
+ * cursor — one blank line, correct height. Leaving the source `<br>` would
14
+ * create a `hard_break` child AND the cursor `<br>` → double-height blank line.
15
+ *
16
+ * Uses the DOM (regex cannot selectively pair span tags). `normalizeHtml` runs
17
+ * in the component body for `initialContent`, so it is guarded for SSR: the
18
+ * editor is created lazily on the client (`immediatelyRender: false`), meaning
19
+ * the value is only parsed client-side and passing through on the server is safe.
14
20
  */
15
21
  export declare function normalizeHtml(html: string): string;
16
22
  /**
@@ -1,4 +1,3 @@
1
- import { Extension } from '@tiptap/core';
2
1
  import type { ReactNode } from 'react';
3
2
  import type { EditorFeatures, EditorSnapshot, KeyboardShortcut, MentionDropdownRenderProps, MentionItem, PasteData, SlashCommandDropdownRenderProps, SlashCommandItem } from '../BikEditor.types';
4
3
  interface ExtensionConfig {
@@ -34,5 +33,5 @@ interface ExtensionConfig {
34
33
  renderSlashCommandItem?: (item: SlashCommandItem, isActive: boolean) => ReactNode;
35
34
  renderSlashCommandDropdown?: (props: SlashCommandDropdownRenderProps) => ReactNode;
36
35
  }
37
- export declare function buildExtensions(config: ExtensionConfig): (import("@tiptap/core").Node<import("@tiptap/extension-paragraph").ParagraphOptions, any> | Extension<import("@tiptap/starter-kit").StarterKitOptions, any> | Extension<any, any> | import("@tiptap/core").Mark<import("@tiptap/extension-underline").UnderlineOptions, any> | import("@tiptap/core").Mark<import("@tiptap/extension-link").LinkOptions, any> | import("@tiptap/core").Mark<import("@tiptap/extension-text-style").TextStyleOptions, any> | Extension<import("@tiptap/extensions").PlaceholderOptions, any> | Extension<import("@tiptap/extensions").CharacterCountOptions, import("@tiptap/extensions").CharacterCountStorage> | import("@tiptap/core").Node<any, any> | import("@tiptap/core").Node<import("@tiptap/extension-mention").MentionOptions<any, import("@tiptap/extension-mention").MentionNodeAttrs>, any> | Extension<import("@tiptap/extension-text-style").ColorOptions, any> | import("@tiptap/core").Mark<import("@tiptap/extension-highlight").HighlightOptions, any> | Extension<import("@tiptap/extension-text-align").TextAlignOptions, any> | import("@tiptap/core").Node<import("@tiptap/extension-image").ImageOptions, any>)[];
36
+ export declare function buildExtensions(config: ExtensionConfig): (import("@tiptap/core").Node<import("@tiptap/extension-hard-break").HardBreakOptions, any> | import("@tiptap/core").Node<import("@tiptap/extension-paragraph").ParagraphOptions, any> | import("@tiptap/core").Extension<import("@tiptap/starter-kit").StarterKitOptions, any> | import("@tiptap/core").Mark<import("@tiptap/extension-underline").UnderlineOptions, any> | import("@tiptap/core").Mark<import("@tiptap/extension-link").LinkOptions, any> | import("@tiptap/core").Mark<import("@tiptap/extension-text-style").TextStyleOptions, any> | import("@tiptap/core").Extension<import("@tiptap/extensions").PlaceholderOptions, any> | import("@tiptap/core").Extension<any, any> | import("@tiptap/core").Extension<import("@tiptap/extensions").CharacterCountOptions, import("@tiptap/extensions").CharacterCountStorage> | import("@tiptap/core").Node<any, any> | import("@tiptap/core").Node<import("@tiptap/extension-mention").MentionOptions<any, import("@tiptap/extension-mention").MentionNodeAttrs>, any> | import("@tiptap/core").Extension<import("@tiptap/extension-text-style").ColorOptions, any> | import("@tiptap/core").Mark<import("@tiptap/extension-highlight").HighlightOptions, any> | import("@tiptap/core").Extension<import("@tiptap/extension-text-align").TextAlignOptions, any> | import("@tiptap/core").Node<import("@tiptap/extension-image").ImageOptions, any>)[];
38
37
  export {};
@@ -3,11 +3,29 @@ import { Extension } from '@tiptap/core';
3
3
  * Unified paste normalizer for all editor channels.
4
4
  *
5
5
  * Uses ProseMirror's recommended hooks instead of overriding handlePaste:
6
- * - clipboardTextParser: plain text → Slice (preserves blank lines)
6
+ * - clipboardTextParser: plain text → Slice (one block, <br> per line)
7
7
  * - transformPastedHTML: HTML → HTML (cleans Google Docs / Word artifacts)
8
- * - transformPasted: Slice → Slice (normalizes hardBreaks, strips marks)
8
+ * - transformPasted: Slice → Slice (normalize hardBreaks, strip marks,
9
+ * mirror the source paragraph-margin convention)
9
10
  *
10
- * Also decorates empty paragraphs with an `is-blank` CSS class so the
11
- * editor stylesheet can give blank lines precise height without extra margins.
11
+ * Margins MIRROR the source (like Gmail): they are not added structurally, so
12
+ * ChatGPT-pasted `<p>` keeps its default margin (gaps) while Google-Docs and
13
+ * plain/paste-without-formatting content stays margin:0 (tight).
14
+ *
15
+ * `appendTransaction` does two bounded touch-ups:
16
+ * 1. Repairs the FIRST pasted paragraph — ProseMirror merges it into the
17
+ * cursor's paragraph at paste time, dropping its margin attr; we record that
18
+ * margin (in transformPasted / the plain handlePaste) and reapply it at the
19
+ * paste position.
20
+ * 2. Typed-Enter inheritance — splitBlock gives a paragraph created at a block
21
+ * end DEFAULT attrs, so an empty paragraph that follows a margin:0 paragraph
22
+ * is set to margin:0 too. Typing inside tight content stays tight; default
23
+ * content stays default; non-empty (pasted) paragraphs are never touched.
24
+ *
25
+ * `plainOnly` (channels with no rich formatting — WhatsApp, IG, FB, comments)
26
+ * forces EVERY paste through the plain-text path: even Cmd/Ctrl+V ignores the
27
+ * clipboard HTML and pastes text only, since those channels don't support
28
+ * formatting anyway. Email and the signature editor (richPaste) keep the full
29
+ * HTML pipeline.
12
30
  */
13
31
  export declare const PasteNormalizationExtension: Extension<any, any>;
@@ -1,6 +1,8 @@
1
1
  import { Fragment, Schema, Slice } from '@tiptap/pm/model';
2
2
  export declare const TEXTBLOCK_TAGS: Set<string>;
3
3
  export declare const BLOCK_SELECTOR = "p,div,h1,h2,h3,h4,h5,h6,ul,ol,li,blockquote,table,pre";
4
+ /** Normalise clipboard newlines: Windows CRLF and lone CR → LF. */
5
+ export declare function normalizeNewlines(text: string): string;
4
6
  /**
5
7
  * When pasted HTML is entirely inline (no block elements), split at
6
8
  * `<br><br>` boundaries into `<p>` elements. Single `<br>` stays as a
@@ -21,31 +23,37 @@ export declare function cleanBlockBrs(container: HTMLElement): void;
21
23
  */
22
24
  export declare function stripTrailingBrs(container: HTMLElement): void;
23
25
  /**
24
- * Convert paragraphs containing only a hardBreak into truly empty paragraphs.
25
- * Google Docs represents blank lines as `<p><br></p>` ProseMirror parses
26
- * the `<br>` as a hardBreak node, which renders double-height. Converting to
27
- * an empty paragraph (childCount 0) gives a single-height blank line.
26
+ * Convert a paragraph whose ONLY child is a hardBreak (`<p><br></p>`) into a
27
+ * truly empty paragraph. ProseMirror parses the `<br>` as a hardBreak node AND
28
+ * adds its own cursor `<br>`, rendering double-height; an empty paragraph
29
+ * (childCount 0) gives a single-height blank line.
28
30
  *
29
- * Does NOT collapse consecutive empties that would destroy user intent.
31
+ * Does NOT strip trailing hardBreaks from content paragraphs, and does NOT
32
+ * collapse consecutive empties — those `<br>`s/blank lines are intentional
33
+ * (e.g. shift+enter / enter runs from Google Docs) and Gmail preserves them.
30
34
  */
31
35
  export declare function normalizeHardBreaks(fragment: Fragment): Fragment;
36
+ /**
37
+ * Mirror the source paragraph-margin convention across a pasted block.
38
+ *
39
+ * Gmail decides spacing from the SOURCE: it keeps default margins for content
40
+ * pasted from ChatGPT/plain `<p>`, and `margin:0` for Google-Docs-style content
41
+ * (whose paragraphs carry `margin:0`). We do the same. A paragraph's margin is
42
+ * already preserved from the source (the Paragraph node's `margin` attribute),
43
+ * so the only gap is the synthesized blank-line paragraphs (created from a
44
+ * Google-Docs block-level `<br>`), which have no source margin. If the pasted
45
+ * block is margin:0-style (any paragraph carries margin:0), apply margin:0 to
46
+ * every paragraph of THIS paste — so the blank lines match. Pastes with no
47
+ * margin:0 paragraph (ChatGPT, plain text) are left untouched → default margins.
48
+ * Scoped to the pasted slice, so it never tightens existing/typed content.
49
+ */
50
+ export declare function mirrorParagraphMargins(fragment: Fragment): Fragment;
32
51
  /**
33
52
  * Strip "rich" marks (link, textStyle, highlight, etc.) while keeping
34
53
  * basic marks (bold, italic, strike, underline, code) that messaging
35
54
  * channels can represent.
36
55
  */
37
56
  export declare function stripRichMarks(fragment: Fragment): Fragment;
38
- /**
39
- * Extract plain text from clipboard HTML, using the DOM structure for
40
- * paragraph boundaries instead of relying on `text/plain` (which differs
41
- * between Mac and Windows for the same source, e.g. Google Docs).
42
- *
43
- * Reuses the existing HTML normalizers so `<br>` between blocks becomes an
44
- * empty `<p>`, then pulls `textContent` from each leaf block element.
45
- * Returns `undefined` when no block structure is found so the caller can
46
- * fall back to `text/plain`.
47
- */
48
- export declare function extractTextFromHtml(html: string): string | undefined;
49
57
  /**
50
58
  * Parse plain clipboard text into a ProseMirror Slice, preserving blank
51
59
  * lines as empty paragraphs. This replaces ProseMirror's default text
@@ -60,5 +68,9 @@ export declare function parseClipboardText(text: string, schema: Schema): Slice;
60
68
  * every `\n`. Produces the same structure as Gmail's Cmd+Shift+V output:
61
69
  * one block element with `<br>` for line breaks and `<br><br>` for blank
62
70
  * lines. This gives identical html and text output to Gmail.
71
+ *
72
+ * The paragraph is tight (`margin: '0'`) — plain/paste-without-formatting is
73
+ * Gmail's `<div>` (no margin), and typed Enter inherits this margin, so
74
+ * splitting plain-pasted text stays tight like Gmail (not default-margin gaps).
63
75
  */
64
76
  export declare function parseClipboardTextAsBreaks(text: string, schema: Schema): Slice;