@@ -7,7 +7,7 @@ import { warnNoDOM } from './helpers/domWarnings.js';
77import { getTextContentFromNodes } from './helpers/getTextContentFromNodes.js' ;
88import { isRegExp } from './utilities/isRegExp.js' ;
99import { handleDocxPaste , wrapTextsInRuns } from './inputRules/docx-paste/docx-paste.js' ;
10- import { ListHelpers } from '@helpers/list-numbering-helpers.js' ;
10+ import { ListHelpers , createListIdAllocator } from '@helpers/list-numbering-helpers.js' ;
1111import { flattenListsInHtml , unflattenListsInHtml } from './inputRules/html/html-helpers.js' ;
1212import { handleGoogleDocsHtml } from './inputRules/google-docs-paste/google-docs-paste.js' ;
1313import {
@@ -53,43 +53,42 @@ export function isSuperdocOriginClipboardHtml(html) {
5353 return false ;
5454}
5555
56- /** Apply pasted body `sectPr` when target has single-column layout. */
57- function tryApplyEmbeddedBodySectPr ( editor , view , bodySectPr ) {
56+ /**
57+ * Apply pasted multi-column `bodySectPr` only when the document is still single-column.
58+ * Caller supplies how the clone is written (own `tr` vs dispatch).
59+ *
60+ * @param {object } editor
61+ * @param {object | null | undefined } bodySectPr
62+ * @param {import('prosemirror-model').Node } docForCurrentAttrs
63+ * @param {(clone: object) => void } applyClone
64+ */
65+ function applyEmbeddedBodySectPrWhenAllowed ( editor , bodySectPr , docForCurrentAttrs , applyClone ) {
5866 if ( ! bodySectPr || typeof bodySectPr !== 'object' ) return ;
5967
6068 const incomingCols = getSectPrColumns ( bodySectPr ) ;
6169 if ( ! incomingCols ?. count || incomingCols . count <= 1 ) return ;
6270
63- const current = view . state . doc . attrs ?. bodySectPr ;
71+ const current = docForCurrentAttrs . attrs ?. bodySectPr ;
6472 const currentCols = current && getSectPrColumns ( current ) ;
6573 if ( currentCols ?. count > 1 ) return ;
6674
6775 const clone = JSON . parse ( JSON . stringify ( bodySectPr ) ) ;
68- const tr = view . state . tr . setDocAttribute ( 'bodySectPr' , clone ) ;
69- const converter = editor ?. converter ;
70- if ( converter ) {
71- converter . bodySectPr = clone ;
76+ applyClone ( clone ) ;
77+ if ( editor ?. converter ) {
78+ editor . converter . bodySectPr = clone ;
7279 }
73- view . dispatch ( tr ) ;
7480}
7581
76- /** Like tryApplyEmbeddedBodySectPr but on `tr` (one dispatch with slice paste meta). */
77- function applyEmbeddedBodySectPrToTransaction ( editor , tr , bodySectPr , docBeforePaste ) {
78- if ( ! bodySectPr || typeof bodySectPr !== 'object' ) return ;
79-
80- const incomingCols = getSectPrColumns ( bodySectPr ) ;
81- if ( ! incomingCols ?. count || incomingCols . count <= 1 ) return ;
82-
83- const current = docBeforePaste . attrs ?. bodySectPr ;
84- const currentCols = current && getSectPrColumns ( current ) ;
85- if ( currentCols ?. count > 1 ) return ;
82+ function tryApplyEmbeddedBodySectPr ( editor , view , bodySectPr ) {
83+ applyEmbeddedBodySectPrWhenAllowed ( editor , bodySectPr , view . state . doc , ( clone ) => {
84+ view . dispatch ( view . state . tr . setDocAttribute ( 'bodySectPr' , clone ) ) ;
85+ } ) ;
86+ }
8687
87- const clone = JSON . parse ( JSON . stringify ( bodySectPr ) ) ;
88- tr . setDocAttribute ( 'bodySectPr' , clone ) ;
89- const converter = editor ?. converter ;
90- if ( converter ) {
91- converter . bodySectPr = clone ;
92- }
88+ function applyEmbeddedBodySectPrToTransaction ( editor , tr , bodySectPr , docBeforePaste ) {
89+ applyEmbeddedBodySectPrWhenAllowed ( editor , bodySectPr , docBeforePaste , ( clone ) => {
90+ tr . setDocAttribute ( 'bodySectPr' , clone ) ;
91+ } ) ;
9392}
9493
9594export class InputRule {
@@ -573,15 +572,25 @@ export function sanitizeHtml(html, forbiddenTags = ['meta', 'svg', 'script', 'st
573572 for ( let i = 0 ; i < node . childNodes . length ; i += 1 ) {
574573 const current = node . childNodes [ i ] ;
575574 if ( current ?. nodeType === Node . COMMENT_NODE && current . nodeValue ?. includes ( '[if !supportLists]' ) ) {
576- let j = i + 1 ;
577- while ( j < node . childNodes . length ) {
575+ const nodesToStrip = [ ] ;
576+ let endifComment = null ;
577+ for ( let j = i + 1 ; j < node . childNodes . length ; j += 1 ) {
578578 const next = node . childNodes [ j ] ;
579579 if ( next ?. nodeType === Node . COMMENT_NODE && next . nodeValue ?. includes ( '[endif]' ) ) {
580- node . removeChild ( next ) ;
580+ endifComment = next ;
581581 break ;
582582 }
583- node . removeChild ( next ) ;
583+ nodesToStrip . push ( next ) ;
584+ }
585+ if ( ! endifComment ) {
586+ node . removeChild ( current ) ;
587+ i -= 1 ;
588+ continue ;
584589 }
590+ for ( const n of nodesToStrip ) {
591+ node . removeChild ( n ) ;
592+ }
593+ node . removeChild ( endifComment ) ;
585594 node . removeChild ( current ) ;
586595 i -= 1 ;
587596 continue ;
@@ -677,8 +686,6 @@ function handleCutEvent(view, event, editor) {
677686 const { from, to } = view . state . selection ;
678687 if ( from === to ) return false ;
679688
680- event . preventDefault ( ) ;
681-
682689 try {
683690 const slice = view . state . doc . slice ( from , to ) ;
684691 const fragment = slice . content ;
@@ -702,12 +709,13 @@ function handleCutEvent(view, event, editor) {
702709 clipboardData . setData ( 'text/html' , embedSliceInHtml ( html , sliceJson , bodySectPrJson ) ) ;
703710 clipboardData . setData ( 'text/plain' , fragment . textBetween ( 0 , fragment . size , '\n\n' ) ) ;
704711
712+ event . preventDefault ( ) ;
705713 view . dispatch ( view . state . tr . deleteSelection ( ) . scrollIntoView ( ) ) ;
714+ return true ;
706715 } catch ( error ) {
707716 console . warn ( 'Failed to handle cut:' , error ) ;
717+ return false ;
708718 }
709-
710- return true ;
711719}
712720
713721const BULLET_MARKER_CHARS = new Set ( [ '•' , '◦' , '▪' , '\u2022' , '\u25E6' , '\u25AA' ] ) ;
@@ -735,25 +743,6 @@ function lvlTextForRemap(fmt, ilvl, lr) {
735743}
736744
737745/** Remap pasted list numIds and rebuild defs so target doc’s abstract ids don’t clash. */
738- function createListIdAllocator ( editor ) {
739- const existingIds = new Set (
740- Object . keys ( editor ?. converter ?. numbering ?. definitions || { } )
741- . map ( ( value ) => Number ( value ) )
742- . filter ( Number . isFinite ) ,
743- ) ;
744- let nextId = Number ( ListHelpers . getNewListId ( editor ) ) ;
745-
746- return ( ) => {
747- while ( ! Number . isFinite ( nextId ) || existingIds . has ( nextId ) ) {
748- nextId = Number . isFinite ( nextId ) ? nextId + 1 : Number ( ListHelpers . getNewListId ( editor ) ) ;
749- }
750- const allocatedId = nextId ;
751- existingIds . add ( allocatedId ) ;
752- nextId += 1 ;
753- return allocatedId ;
754- } ;
755- }
756-
757746function remapPastedListNumberingInFragment ( fragment , editor ) {
758747 if ( ! editor ?. converter || ! fragment . size ) {
759748 return fragment ;
0 commit comments