@bikdotai/bik-component-library 0.0.809-beta.12 → 0.0.809-beta.13

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.
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("../../../node_modules/@tiptap/core/dist/index.js"),r=require("@tiptap/pm/model"),t=require("@tiptap/pm/state"),a=require("@tiptap/pm/view"),s=require("./pasteUtils.js");const n=e.Extension.create({name:"pasteNormalization",addOptions:()=>({preserveMarks:!1}),addProseMirrorPlugins(){const e=this.options.preserveMarks;return[new t.Plugin({props:{decorations(e){const r=[];return e.doc.descendants(((e,t)=>{"paragraph"===e.type.name&&0===e.childCount&&r.push(a.Decoration.node(t,t+e.nodeSize,{class:"is-blank"}))})),a.DecorationSet.create(e.doc,r)},clipboardTextParser:(r,t,a,n)=>(e?s.parseClipboardTextAsParagraphs:s.parseClipboardText)(r,n.state.schema),transformPastedHTML(e){if(e.includes("data-pm-slice"))return e;const r=document.createElement("div");return r.innerHTML=e,s.cleanBlockBrs(r),s.stripTrailingBrs(r),r.innerHTML},transformPasted(t){let a=s.normalizeHardBreaks(t.content);return e||(a=s.stripRichMarks(a)),new r.Slice(a,t.openStart,t.openEnd)}}})]}});exports.PasteNormalizationExtension=n;
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e=require("../../../node_modules/@tiptap/core/dist/index.js"),r=require("@tiptap/pm/model"),t=require("@tiptap/pm/state"),s=require("@tiptap/pm/view"),a=require("./pasteUtils.js");const n=e.Extension.create({name:"pasteNormalization",addOptions:()=>({preserveMarks:!1}),addProseMirrorPlugins(){const e=this.options.preserveMarks;return[new t.Plugin({props:{decorations(e){const r=[];return e.doc.descendants(((e,t)=>{"paragraph"===e.type.name&&0===e.childCount&&r.push(s.Decoration.node(t,t+e.nodeSize,{class:"is-blank"}))})),s.DecorationSet.create(e.doc,r)},clipboardTextParser:(r,t,s,n)=>(e?a.parseClipboardTextAsBreaks:a.parseClipboardText)(r,n.state.schema),transformPastedHTML(e){if(e.includes("data-pm-slice"))return e;const r=document.createElement("div");return r.innerHTML=e,a.cleanBlockBrs(r),a.stripTrailingBrs(r),r.innerHTML},transformPasted(t){let s=a.normalizeHardBreaks(t.content);return e||(s=a.stripRichMarks(s)),new r.Slice(s,t.openStart,t.openEnd)}}})]}});exports.PasteNormalizationExtension=n;
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\tnormalizeHardBreaks,\n\tparseClipboardText,\n\tparseClipboardTextAsParagraphs,\n\tstripRichMarks,\n\tstripTrailingBrs,\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\tclipboardTextParser(text, _$context, _plain, view) {\n\t\t\t\t\t\tconst parse = preserveMarks\n\t\t\t\t\t\t\t? parseClipboardTextAsParagraphs\n\t\t\t\t\t\t\t: parseClipboardText;\n\t\t\t\t\t\treturn parse(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\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","clipboardTextParser","text","_$context","_plain","view","parseClipboardTextAsParagraphs","parseClipboardText","schema","transformPastedHTML","html","includes","container","document","createElement","innerHTML","cleanBlockBrs","stripTrailingBrs","transformPasted","slice","content","normalizeHardBreaks","stripRichMarks","Slice","openStart","openEnd"],"mappings":"kQAwBaA,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,oBAAmBA,CAACC,EAAMC,EAAWC,EAAQC,KAC9BvB,EACXwB,EAA8BA,+BAC9BC,sBACUL,EAAMG,EAAKhB,MAAMmB,QAG/BC,oBAAoBC,GACnB,GAAIA,EAAKC,SAAS,iBAAkB,OAAOD,EAC3C,MAAME,EAAYC,SAASC,cAAc,OAIzC,OAHAF,EAAUG,UAAYL,EACtBM,EAAaA,cAACJ,GACdK,EAAgBA,iBAACL,GACVA,EAAUG,SACjB,EAEDG,gBAAgBC,GACf,IAAIC,EAAUC,EAAAA,oBAAoBF,EAAMC,SAIxC,OAHKtC,IACJsC,EAAUE,EAAAA,eAAeF,IAEnB,IAAIG,EAAKA,MAACH,EAASD,EAAMK,UAAWL,EAAMM,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 { Decoration, DecorationSet } from '@tiptap/pm/view';\nimport {\n\tcleanBlockBrs,\n\tnormalizeHardBreaks,\n\tparseClipboardText,\n\tparseClipboardTextAsBreaks,\n\tstripRichMarks,\n\tstripTrailingBrs,\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\tclipboardTextParser(text, _$context, _plain, view) {\n\t\t\t\t\t\tconst parse = preserveMarks\n\t\t\t\t\t\t\t? parseClipboardTextAsBreaks\n\t\t\t\t\t\t\t: parseClipboardText;\n\t\t\t\t\t\treturn parse(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\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","clipboardTextParser","text","_$context","_plain","view","parseClipboardTextAsBreaks","parseClipboardText","schema","transformPastedHTML","html","includes","container","document","createElement","innerHTML","cleanBlockBrs","stripTrailingBrs","transformPasted","slice","content","normalizeHardBreaks","stripRichMarks","Slice","openStart","openEnd"],"mappings":"kQAwBaA,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,oBAAmBA,CAACC,EAAMC,EAAWC,EAAQC,KAC9BvB,EACXwB,EAA0BA,2BAC1BC,sBACUL,EAAMG,EAAKhB,MAAMmB,QAG/BC,oBAAoBC,GACnB,GAAIA,EAAKC,SAAS,iBAAkB,OAAOD,EAC3C,MAAME,EAAYC,SAASC,cAAc,OAIzC,OAHAF,EAAUG,UAAYL,EACtBM,EAAaA,cAACJ,GACdK,EAAgBA,iBAACL,GACVA,EAAUG,SACjB,EAEDG,gBAAgBC,GACf,IAAIC,EAAUC,EAAAA,oBAAoBF,EAAMC,SAIxC,OAHKtC,IACJsC,EAAUE,EAAAA,eAAeF,IAEnB,IAAIG,EAAKA,MAACH,EAASD,EAAMK,UAAWL,EAAMM,QAClD,KAIJ"}
@@ -1,2 +1,2 @@
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=new Set(["bold","italic","strike","underline","code"]);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.normalizeHardBreaks=function r(t){const n=[];return t.forEach((t=>{var o,a;if("paragraph"===t.type.name){if(1===t.childCount&&"hardBreak"===(null===(o=t.firstChild)||void 0===o?void 0:o.type.name))return void n.push(t.type.create(t.attrs));if(t.childCount>1&&"hardBreak"===(null===(a=t.lastChild)||void 0===a?void 0:a.type.name)){const r=[];return t.content.forEach(((e,n,o)=>{o<t.childCount-1&&r.push(e)})),void n.push(t.copy(e.Fragment.from(r)))}}t.isBlock&&!t.isLeaf?n.push(t.copy(r(t.content))):n.push(t)})),e.Fragment.from(n)},exports.parseClipboardText=function(r,t){const n=r.split("\n").map((e=>t.node("paragraph",null,e?[t.text(e)]:[])));return new e.Slice(e.Fragment.fromArray(n),1,1)},exports.parseClipboardTextAsParagraphs=function(r,t){const n=r.replace(/\n+$/,"").split(/\n\n+/),o=t.nodes.hardBreak,a=n.map((e=>{const r=e.split("\n"),n=[];return r.forEach(((e,a)=>{e&&n.push(t.text(e)),a<r.length-1&&o&&n.push(o.create())})),t.node("paragraph",null,n.length?n:[])}));return new e.Slice(e.Fragment.fromArray(a),1,1)},exports.stripRichMarks=function r(n){const o=[];return n.forEach((e=>{if(e.isText){const r=e.marks.filter((e=>t.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()}};
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=new Set(["bold","italic","strike","underline","code"]);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.normalizeHardBreaks=function r(t){const n=[];return t.forEach((t=>{var o,a;if("paragraph"===t.type.name){if(1===t.childCount&&"hardBreak"===(null===(o=t.firstChild)||void 0===o?void 0:o.type.name))return void n.push(t.type.create(t.attrs));if(t.childCount>1&&"hardBreak"===(null===(a=t.lastChild)||void 0===a?void 0:a.type.name)){const r=[];return t.content.forEach(((e,n,o)=>{o<t.childCount-1&&r.push(e)})),void n.push(t.copy(e.Fragment.from(r)))}}t.isBlock&&!t.isLeaf?n.push(t.copy(r(t.content))):n.push(t)})),e.Fragment.from(n)},exports.parseClipboardText=function(r,t){const n=r.split("\n").map((e=>t.node("paragraph",null,e?[t.text(e)]:[])));return new e.Slice(e.Fragment.fromArray(n),1,1)},exports.parseClipboardTextAsBreaks=function(r,t){const n=r.replace(/\n+$/,"");if(!n)return new e.Slice(e.Fragment.from(t.node("paragraph",null,[])),1,1);const o=t.nodes.hardBreak,a=n.split("\n"),l=[];return a.forEach(((e,r)=>{e&&l.push(t.text(e)),r<a.length-1&&o&&l.push(o.create())})),new e.Slice(e.Fragment.from(t.node("paragraph",null,l.length?l:[])),1,1)},exports.stripRichMarks=function r(n){const o=[];return n.forEach((e=>{if(e.isText){const r=e.marks.filter((e=>t.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()}};
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 * 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 * 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 using double-newline as paragraph separator\n * and single newline as a line break (<br>) within a paragraph. Produces\n * the same structure as Gmail's Cmd+V output.\n *\n * Best for channels with CSS-based paragraph gaps (e.g. email) where the\n * margin between `<p>` tags already provides visual separation.\n */\nexport function parseClipboardTextAsParagraphs(\n\ttext: string,\n\tschema: Schema,\n): Slice {\n\tconst trimmed = text.replace(/\\n+$/, '');\n\tconst blocks = trimmed.split(/\\n\\n+/);\n\tconst hardBreak = schema.nodes['hardBreak'];\n\tconst paragraphs = blocks.map((block) => {\n\t\tconst lines = block.split('\\n');\n\t\tconst children: PMNode[] = [];\n\t\tlines.forEach((line, i) => {\n\t\t\tif (line) children.push(schema.text(line));\n\t\t\tif (i < lines.length - 1 && hardBreak) {\n\t\t\t\tchildren.push(hardBreak.create());\n\t\t\t}\n\t\t});\n\t\treturn schema.node('paragraph', null, children.length ? children : []);\n\t});\n\treturn new Slice(Fragment.fromArray(paragraphs), 1, 1);\n}\n"],"names":["TEXTBLOCK_TAGS","Set","BASIC_MARKS","container","brs","Array","from","querySelectorAll","br","insideTextblock","el","parentElement","has","tagName","replaceChild","document","createElement","normalizeHardBreaks","fragment","nodes","forEach","node","type","name","childCount","_a","firstChild","push","create","attrs","_b","lastChild","children","content","child","_offset","index","copy","Fragment","isBlock","isLeaf","text","schema","paragraphs","split","map","line","Slice","fromArray","blocks","replace","hardBreak","block","lines","i","length","stripRichMarks","isText","kept","marks","filter","m","mark","tag","last","lastElementChild","childNodes","remove"],"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/**\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 * 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","BASIC_MARKS","container","brs","Array","from","querySelectorAll","br","insideTextblock","el","parentElement","has","tagName","replaceChild","document","createElement","normalizeHardBreaks","fragment","nodes","forEach","node","type","name","childCount","_a","firstChild","push","create","attrs","_b","lastChild","children","content","child","_offset","index","copy","Fragment","isBlock","isLeaf","text","schema","paragraphs","split","map","line","Slice","fromArray","trimmed","replace","hardBreak","lines","i","length","stripRichMarks","isText","kept","marks","filter","m","mark","tag","last","lastElementChild","childNodes","remove"],"mappings":"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"}
@@ -39,11 +39,9 @@ export declare function stripRichMarks(fragment: Fragment): Fragment;
39
39
  */
40
40
  export declare function parseClipboardText(text: string, schema: Schema): Slice;
41
41
  /**
42
- * Parse plain clipboard text using double-newline as paragraph separator
43
- * and single newline as a line break (<br>) within a paragraph. Produces
44
- * the same structure as Gmail's Cmd+V output.
45
- *
46
- * Best for channels with CSS-based paragraph gaps (e.g. email) where the
47
- * margin between `<p>` tags already provides visual separation.
42
+ * Parse plain clipboard text into a single paragraph with hardBreaks for
43
+ * every `\n`. Produces the same structure as Gmail's Cmd+Shift+V output:
44
+ * one block element with `<br>` for line breaks and `<br><br>` for blank
45
+ * lines. This gives identical html and text output to Gmail.
48
46
  */
49
- export declare function parseClipboardTextAsParagraphs(text: string, schema: Schema): Slice;
47
+ export declare function parseClipboardTextAsBreaks(text: string, schema: Schema): Slice;
@@ -1,2 +1,2 @@
1
- import{Extension as e}from"../../../node_modules/@tiptap/core/dist/index.js";import{Slice as t}from"@tiptap/pm/model";import{Plugin as r}from"@tiptap/pm/state";import{Decoration as o,DecorationSet as n}from"@tiptap/pm/view";import{cleanBlockBrs as s,stripTrailingBrs as a,normalizeHardBreaks as p,stripRichMarks as i,parseClipboardTextAsParagraphs as d,parseClipboardText as m}from"./pasteUtils.js";const c=e.create({name:"pasteNormalization",addOptions:()=>({preserveMarks:!1}),addProseMirrorPlugins(){const e=this.options.preserveMarks;return[new r({props:{decorations(e){const t=[];return e.doc.descendants(((e,r)=>{"paragraph"===e.type.name&&0===e.childCount&&t.push(o.node(r,r+e.nodeSize,{class:"is-blank"}))})),n.create(e.doc,t)},clipboardTextParser:(t,r,o,n)=>(e?d:m)(t,n.state.schema),transformPastedHTML(e){if(e.includes("data-pm-slice"))return e;const t=document.createElement("div");return t.innerHTML=e,s(t),a(t),t.innerHTML},transformPasted(r){let o=p(r.content);return e||(o=i(o)),new t(o,r.openStart,r.openEnd)}}})]}});export{c as PasteNormalizationExtension};
1
+ import{Extension as e}from"../../../node_modules/@tiptap/core/dist/index.js";import{Slice as t}from"@tiptap/pm/model";import{Plugin as r}from"@tiptap/pm/state";import{Decoration as o,DecorationSet as n}from"@tiptap/pm/view";import{cleanBlockBrs as s,stripTrailingBrs as a,normalizeHardBreaks as p,stripRichMarks as i,parseClipboardTextAsBreaks as d,parseClipboardText as m}from"./pasteUtils.js";const c=e.create({name:"pasteNormalization",addOptions:()=>({preserveMarks:!1}),addProseMirrorPlugins(){const e=this.options.preserveMarks;return[new r({props:{decorations(e){const t=[];return e.doc.descendants(((e,r)=>{"paragraph"===e.type.name&&0===e.childCount&&t.push(o.node(r,r+e.nodeSize,{class:"is-blank"}))})),n.create(e.doc,t)},clipboardTextParser:(t,r,o,n)=>(e?d:m)(t,n.state.schema),transformPastedHTML(e){if(e.includes("data-pm-slice"))return e;const t=document.createElement("div");return t.innerHTML=e,s(t),a(t),t.innerHTML},transformPasted(r){let o=p(r.content);return e||(o=i(o)),new t(o,r.openStart,r.openEnd)}}})]}});export{c as PasteNormalizationExtension};
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\tnormalizeHardBreaks,\n\tparseClipboardText,\n\tparseClipboardTextAsParagraphs,\n\tstripRichMarks,\n\tstripTrailingBrs,\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\tclipboardTextParser(text, _$context, _plain, view) {\n\t\t\t\t\t\tconst parse = preserveMarks\n\t\t\t\t\t\t\t? parseClipboardTextAsParagraphs\n\t\t\t\t\t\t\t: parseClipboardText;\n\t\t\t\t\t\treturn parse(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\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","clipboardTextParser","text","_$context","_plain","view","parseClipboardTextAsParagraphs","parseClipboardText","schema","transformPastedHTML","html","includes","container","document","createElement","innerHTML","cleanBlockBrs","stripTrailingBrs","transformPasted","slice","content","normalizeHardBreaks","stripRichMarks","Slice","openStart","openEnd"],"mappings":"qZAwBaA,EAA8BC,EAAUC,OAAO,CAC3DC,KAAM,qBACNC,WAAUA,KACF,CACNC,eAAe,IAGjBC,wBACC,MAAMD,EAAgBE,KAAKC,QAAQH,cACnC,MAAO,CACN,IAAII,EAAO,CACVC,MAAO,CACNC,YAAYC,GACX,MAAMD,EAA4B,GAUlC,OATAC,EAAMC,IAAIC,aAAY,CAACC,EAAMC,KACL,cAAnBD,EAAKE,KAAKd,MAA4C,IAApBY,EAAKG,YAC1CP,EAAYQ,KACXC,EAAWL,KAAKC,EAAKA,EAAMD,EAAKM,SAAU,CACzCC,MAAO,aAGT,IAEKC,EAAcrB,OAAOU,EAAMC,IAAKF,EACvC,EAEDa,oBAAmBA,CAACC,EAAMC,EAAWC,EAAQC,KAC9BvB,EACXwB,EACAC,GACUL,EAAMG,EAAKhB,MAAMmB,QAG/BC,oBAAoBC,GACnB,GAAIA,EAAKC,SAAS,iBAAkB,OAAOD,EAC3C,MAAME,EAAYC,SAASC,cAAc,OAIzC,OAHAF,EAAUG,UAAYL,EACtBM,EAAcJ,GACdK,EAAiBL,GACVA,EAAUG,SACjB,EAEDG,gBAAgBC,GACf,IAAIC,EAAUC,EAAoBF,EAAMC,SAIxC,OAHKtC,IACJsC,EAAUE,EAAeF,IAEnB,IAAIG,EAAMH,EAASD,EAAMK,UAAWL,EAAMM,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 { Decoration, DecorationSet } from '@tiptap/pm/view';\nimport {\n\tcleanBlockBrs,\n\tnormalizeHardBreaks,\n\tparseClipboardText,\n\tparseClipboardTextAsBreaks,\n\tstripRichMarks,\n\tstripTrailingBrs,\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\tclipboardTextParser(text, _$context, _plain, view) {\n\t\t\t\t\t\tconst parse = preserveMarks\n\t\t\t\t\t\t\t? parseClipboardTextAsBreaks\n\t\t\t\t\t\t\t: parseClipboardText;\n\t\t\t\t\t\treturn parse(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\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","clipboardTextParser","text","_$context","_plain","view","parseClipboardTextAsBreaks","parseClipboardText","schema","transformPastedHTML","html","includes","container","document","createElement","innerHTML","cleanBlockBrs","stripTrailingBrs","transformPasted","slice","content","normalizeHardBreaks","stripRichMarks","Slice","openStart","openEnd"],"mappings":"iZAwBaA,EAA8BC,EAAUC,OAAO,CAC3DC,KAAM,qBACNC,WAAUA,KACF,CACNC,eAAe,IAGjBC,wBACC,MAAMD,EAAgBE,KAAKC,QAAQH,cACnC,MAAO,CACN,IAAII,EAAO,CACVC,MAAO,CACNC,YAAYC,GACX,MAAMD,EAA4B,GAUlC,OATAC,EAAMC,IAAIC,aAAY,CAACC,EAAMC,KACL,cAAnBD,EAAKE,KAAKd,MAA4C,IAApBY,EAAKG,YAC1CP,EAAYQ,KACXC,EAAWL,KAAKC,EAAKA,EAAMD,EAAKM,SAAU,CACzCC,MAAO,aAGT,IAEKC,EAAcrB,OAAOU,EAAMC,IAAKF,EACvC,EAEDa,oBAAmBA,CAACC,EAAMC,EAAWC,EAAQC,KAC9BvB,EACXwB,EACAC,GACUL,EAAMG,EAAKhB,MAAMmB,QAG/BC,oBAAoBC,GACnB,GAAIA,EAAKC,SAAS,iBAAkB,OAAOD,EAC3C,MAAME,EAAYC,SAASC,cAAc,OAIzC,OAHAF,EAAUG,UAAYL,EACtBM,EAAcJ,GACdK,EAAiBL,GACVA,EAAUG,SACjB,EAEDG,gBAAgBC,GACf,IAAIC,EAAUC,EAAoBF,EAAMC,SAIxC,OAHKtC,IACJsC,EAAUE,EAAeF,IAEnB,IAAIG,EAAMH,EAASD,EAAMK,UAAWL,EAAMM,QAClD,KAIJ"}
@@ -1,2 +1,2 @@
1
- import{Fragment as e,Slice as t}from"@tiptap/pm/model";const n=new Set(["P","LI","H1","H2","H3","H4","H5","H6","TD","TH","PRE"]),r=new Set(["bold","italic","strike","underline","code"]);function o(e){const t=Array.from(e.querySelectorAll("br"));for(const r of t){let t=!1,o=r.parentElement;for(;o&&o!==e;){if(n.has(o.tagName)){t=!0;break}o=o.parentElement}t||r.parentElement.replaceChild(document.createElement("p"),r)}}function a(e){for(const t of["p","div"])for(const n of Array.from(e.querySelectorAll(t))){const e=n.lastElementChild;"BR"===(null==e?void 0:e.tagName)&&n.childNodes.length>1&&e.remove()}}function l(t){const n=[];return t.forEach((t=>{var r,o;if("paragraph"===t.type.name){if(1===t.childCount&&"hardBreak"===(null===(r=t.firstChild)||void 0===r?void 0:r.type.name))return void n.push(t.type.create(t.attrs));if(t.childCount>1&&"hardBreak"===(null===(o=t.lastChild)||void 0===o?void 0:o.type.name)){const r=[];return t.content.forEach(((e,n,o)=>{o<t.childCount-1&&r.push(e)})),void n.push(t.copy(e.from(r)))}}t.isBlock&&!t.isLeaf?n.push(t.copy(l(t.content))):n.push(t)})),e.from(n)}function c(t){const n=[];return t.forEach((e=>{if(e.isText){const t=e.marks.filter((e=>r.has(e.type.name)));n.push(t.length===e.marks.length?e:e.mark(t))}else n.push(e.copy(c(e.content)))})),e.from(n)}function i(n,r){const o=n.split("\n").map((e=>r.node("paragraph",null,e?[r.text(e)]:[])));return new t(e.fromArray(o),1,1)}function p(n,r){const o=n.replace(/\n+$/,"").split(/\n\n+/),a=r.nodes.hardBreak,l=o.map((e=>{const t=e.split("\n"),n=[];return t.forEach(((e,o)=>{e&&n.push(r.text(e)),o<t.length-1&&a&&n.push(a.create())})),r.node("paragraph",null,n.length?n:[])}));return new t(e.fromArray(l),1,1)}export{n as TEXTBLOCK_TAGS,o as cleanBlockBrs,l as normalizeHardBreaks,i as parseClipboardText,p as parseClipboardTextAsParagraphs,c as stripRichMarks,a as stripTrailingBrs};
1
+ import{Fragment as e,Slice as t}from"@tiptap/pm/model";const n=new Set(["P","LI","H1","H2","H3","H4","H5","H6","TD","TH","PRE"]),r=new Set(["bold","italic","strike","underline","code"]);function o(e){const t=Array.from(e.querySelectorAll("br"));for(const r of t){let t=!1,o=r.parentElement;for(;o&&o!==e;){if(n.has(o.tagName)){t=!0;break}o=o.parentElement}t||r.parentElement.replaceChild(document.createElement("p"),r)}}function a(e){for(const t of["p","div"])for(const n of Array.from(e.querySelectorAll(t))){const e=n.lastElementChild;"BR"===(null==e?void 0:e.tagName)&&n.childNodes.length>1&&e.remove()}}function l(t){const n=[];return t.forEach((t=>{var r,o;if("paragraph"===t.type.name){if(1===t.childCount&&"hardBreak"===(null===(r=t.firstChild)||void 0===r?void 0:r.type.name))return void n.push(t.type.create(t.attrs));if(t.childCount>1&&"hardBreak"===(null===(o=t.lastChild)||void 0===o?void 0:o.type.name)){const r=[];return t.content.forEach(((e,n,o)=>{o<t.childCount-1&&r.push(e)})),void n.push(t.copy(e.from(r)))}}t.isBlock&&!t.isLeaf?n.push(t.copy(l(t.content))):n.push(t)})),e.from(n)}function c(t){const n=[];return t.forEach((e=>{if(e.isText){const t=e.marks.filter((e=>r.has(e.type.name)));n.push(t.length===e.marks.length?e:e.mark(t))}else n.push(e.copy(c(e.content)))})),e.from(n)}function i(n,r){const o=n.split("\n").map((e=>r.node("paragraph",null,e?[r.text(e)]:[])));return new t(e.fromArray(o),1,1)}function p(n,r){const o=n.replace(/\n+$/,"");if(!o)return new t(e.from(r.node("paragraph",null,[])),1,1);const a=r.nodes.hardBreak,l=o.split("\n"),c=[];return l.forEach(((e,t)=>{e&&c.push(r.text(e)),t<l.length-1&&a&&c.push(a.create())})),new t(e.from(r.node("paragraph",null,c.length?c:[])),1,1)}export{n as TEXTBLOCK_TAGS,o as cleanBlockBrs,l as normalizeHardBreaks,i as parseClipboardText,p as parseClipboardTextAsBreaks,c as stripRichMarks,a as stripTrailingBrs};
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 * 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 * 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 using double-newline as paragraph separator\n * and single newline as a line break (<br>) within a paragraph. Produces\n * the same structure as Gmail's Cmd+V output.\n *\n * Best for channels with CSS-based paragraph gaps (e.g. email) where the\n * margin between `<p>` tags already provides visual separation.\n */\nexport function parseClipboardTextAsParagraphs(\n\ttext: string,\n\tschema: Schema,\n): Slice {\n\tconst trimmed = text.replace(/\\n+$/, '');\n\tconst blocks = trimmed.split(/\\n\\n+/);\n\tconst hardBreak = schema.nodes['hardBreak'];\n\tconst paragraphs = blocks.map((block) => {\n\t\tconst lines = block.split('\\n');\n\t\tconst children: PMNode[] = [];\n\t\tlines.forEach((line, i) => {\n\t\t\tif (line) children.push(schema.text(line));\n\t\t\tif (i < lines.length - 1 && hardBreak) {\n\t\t\t\tchildren.push(hardBreak.create());\n\t\t\t}\n\t\t});\n\t\treturn schema.node('paragraph', null, children.length ? children : []);\n\t});\n\treturn new Slice(Fragment.fromArray(paragraphs), 1, 1);\n}\n"],"names":["TEXTBLOCK_TAGS","Set","BASIC_MARKS","cleanBlockBrs","container","brs","Array","from","querySelectorAll","br","insideTextblock","el","parentElement","has","tagName","replaceChild","document","createElement","stripTrailingBrs","tag","last","lastElementChild","childNodes","length","remove","normalizeHardBreaks","fragment","nodes","forEach","node","type","name","childCount","_a","firstChild","push","create","attrs","_b","lastChild","children","content","child","_offset","index","copy","Fragment","isBlock","isLeaf","stripRichMarks","isText","kept","marks","filter","m","mark","parseClipboardText","text","schema","paragraphs","split","map","line","Slice","fromArray","parseClipboardTextAsParagraphs","blocks","replace","hardBreak","block","lines","i"],"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/**\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 * 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","BASIC_MARKS","cleanBlockBrs","container","brs","Array","from","querySelectorAll","br","insideTextblock","el","parentElement","has","tagName","replaceChild","document","createElement","stripTrailingBrs","tag","last","lastElementChild","childNodes","length","remove","normalizeHardBreaks","fragment","nodes","forEach","node","type","name","childCount","_a","firstChild","push","create","attrs","_b","lastChild","children","content","child","_offset","index","copy","Fragment","isBlock","isLeaf","stripRichMarks","isText","kept","marks","filter","m","mark","parseClipboardText","text","schema","paragraphs","split","map","line","Slice","fromArray","parseClipboardTextAsBreaks","trimmed","replace","hardBreak","lines","i"],"mappings":"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"}
@@ -39,11 +39,9 @@ export declare function stripRichMarks(fragment: Fragment): Fragment;
39
39
  */
40
40
  export declare function parseClipboardText(text: string, schema: Schema): Slice;
41
41
  /**
42
- * Parse plain clipboard text using double-newline as paragraph separator
43
- * and single newline as a line break (<br>) within a paragraph. Produces
44
- * the same structure as Gmail's Cmd+V output.
45
- *
46
- * Best for channels with CSS-based paragraph gaps (e.g. email) where the
47
- * margin between `<p>` tags already provides visual separation.
42
+ * Parse plain clipboard text into a single paragraph with hardBreaks for
43
+ * every `\n`. Produces the same structure as Gmail's Cmd+Shift+V output:
44
+ * one block element with `<br>` for line breaks and `<br><br>` for blank
45
+ * lines. This gives identical html and text output to Gmail.
48
46
  */
49
- export declare function parseClipboardTextAsParagraphs(text: string, schema: Schema): Slice;
47
+ export declare function parseClipboardTextAsBreaks(text: string, schema: Schema): Slice;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bikdotai/bik-component-library",
3
- "version": "0.0.809-beta.12",
3
+ "version": "0.0.809-beta.13",
4
4
  "description": "Bik Component Library",
5
5
  "repository": {
6
6
  "type": "git",