@@ -380,16 +380,24 @@ export const applyLinkedStyleToTransaction = (tr, editor, style) => {
380380 } ;
381381 } ;
382382
383- // Function to clear formatting marks from text content
384- const clearFormattingMarks = ( startPos , endPos ) => {
385- tr . doc . nodesBetween ( startPos , endPos , ( node , pos ) => {
386- if ( node . isText && node . marks . length > 0 ) {
387- node . marks . forEach ( ( mark ) => {
388- if ( FORMATTING_MARK_NAMES . has ( mark . type . name ) ) {
389- tr . removeMark ( pos , pos + node . nodeSize , mark ) ;
390- }
391- } ) ;
383+ // Clear FORMATTING_MARK_NAMES only inside [rangeFrom, rangeTo), not across whole text nodes
384+ // (selection can split mid-node; removeMark must use the intersection with each text slice).
385+ const clearFormattingMarks = ( rangeFrom , rangeTo ) => {
386+ tr . doc . nodesBetween ( rangeFrom , rangeTo , ( node , pos ) => {
387+ if ( ! node . isText || node . marks . length === 0 ) {
388+ return true ;
392389 }
390+ const nodeEnd = pos + node . nodeSize ;
391+ const clearFrom = Math . max ( pos , rangeFrom ) ;
392+ const clearTo = Math . min ( nodeEnd , rangeTo ) ;
393+ if ( clearFrom >= clearTo ) {
394+ return true ;
395+ }
396+ node . marks . forEach ( ( mark ) => {
397+ if ( FORMATTING_MARK_NAMES . has ( mark . type . name ) ) {
398+ tr . removeMark ( clearFrom , clearTo , mark ) ;
399+ }
400+ } ) ;
393401 return true ;
394402 } ) ;
395403 } ;
@@ -447,7 +455,8 @@ export const applyLinkedStyleToTransaction = (tr, editor, style) => {
447455 if ( startPara && endPara && startPara . pos === endPara . pos ) {
448456 const bounds = getParagraphTextBounds ( tr . doc , startPara . pos , startPara . node ) ;
449457 const coversFullParagraphText = bounds && from <= bounds . from && to >= bounds . to ;
450- if ( ! coversFullParagraphText ) {
458+ // No text (empty / image-only): cannot do linked character range apply; use paragraph path below.
459+ if ( bounds && ! coversFullParagraphText ) {
451460 clearFormattingMarks ( from , to ) ;
452461 applyCharacterStyleMarkToRange ( tr , textStyleType , from , to , linkedCharStyleId ) ;
453462 clearStoredFormattingMarks ( ) ;
@@ -467,6 +476,18 @@ export const applyLinkedStyleToTransaction = (tr, editor, style) => {
467476 return true ;
468477 } ) ;
469478
479+ // nodesBetween often skips block parents when the range only covers inline content (e.g. image-only).
480+ if ( paragraphPositions . length === 0 && from !== to ) {
481+ const seen = new Set ( ) ;
482+ const pushParagraph = ( info ) => {
483+ if ( ! info || seen . has ( info . pos ) ) return ;
484+ seen . add ( info . pos ) ;
485+ paragraphPositions . push ( { node : info . node , pos : info . pos } ) ;
486+ } ;
487+ pushParagraph ( findParentNodeClosestToPos ( tr . doc . resolve ( from ) , ( n ) => n . type . name === 'paragraph' ) ) ;
488+ pushParagraph ( findParentNodeClosestToPos ( tr . doc . resolve ( to ) , ( n ) => n . type . name === 'paragraph' ) ) ;
489+ }
490+
470491 // Apply style to all paragraphs in selection (with clean attributes and cleared marks)
471492 paragraphPositions . forEach ( ( { node, pos } ) => {
472493 // Clear formatting marks within this paragraph
0 commit comments