1- import { getMarksFromSelection } from '../helpers/getMarksFromSelection.js' ;
1+ import { getSelectionFormattingState } from '../helpers/getMarksFromSelection.js' ;
22
33/**
4- * Cascade-aware toggle for marks that may be provided by styles (e.g., rStyle in runProperties) .
4+ * Cascade-aware toggle for marks that may be provided by styles.
55 *
66 * Behavior:
77 * - If a negation mark is active → remove it (turn ON again)
8- * - Else if an inline mark is active → remove it (turn OFF)
9- * - Else if style provides the effect → add a negation mark (turn OFF style)
8+ * - Else if direct inline formatting is active and style is also ON → remove inline and add negation
9+ * - Else if only direct inline formatting is active → remove it (turn OFF)
10+ * - Else if only style provides the effect → add a negation mark (turn OFF style)
1011 * - Else → add regular inline mark (turn ON)
1112 *
1213 * @param {string } markName
1314 * @param {{
1415 * negationAttrs?: Object,
1516 * isNegation?: (attrs:Object)=>boolean,
16- * styleDetector?: ({state: any, selectionMarks: any[], markName: string})=>boolean,
1717 * extendEmptyMarkRange?: boolean,
1818 * }} [options]
1919 */
@@ -22,143 +22,44 @@ export const toggleMarkCascade =
2222 ( { state, chain, editor } ) => {
2323 const {
2424 negationAttrs = { value : '0' } ,
25- isNegation = ( attrs ) => attrs ?. value === '0' ,
26- styleDetector = defaultStyleDetector ,
25+ isNegation = ( attrs ) => attrs ?. value === '0' || attrs ?. value === false ,
2726 extendEmptyMarkRange = false ,
2827 } = options ;
2928
30- const selectionMarks = getMarksFromSelection ( state ) || [ ] ;
31- const inlineMarks = selectionMarks . filter ( ( m ) => m . type ?. name === markName ) ;
32- const hasNegation = inlineMarks . some ( ( m ) => isNegation ( m . attrs || { } ) ) ;
33- const hasInline = inlineMarks . some ( ( m ) => ! isNegation ( m . attrs || { } ) ) ;
34- const styleOn = styleDetector ( { state, selectionMarks, markName, editor } ) ;
29+ const formattingState = getSelectionFormattingState ( state , editor ) ;
30+ const directMarksForType = ( formattingState ?. inlineMarks || [ ] ) . filter ( ( m ) => m . type ?. name === markName ) ;
31+ const hasNegation = directMarksForType . some ( ( m ) => isNegation ( m . attrs || { } ) ) ;
32+ const hasInline = directMarksForType . some ( ( m ) => ! isNegation ( m . attrs || { } ) ) ;
33+ const styleValue = formattingState ?. styleRunProperties ?. [ markName ] ;
34+ const styleOn = isRunPropertyEnabled ( styleValue ) ;
3535
3636 const cmdChain = chain ( ) ;
37- // 1) If negation already present, remove it (turn back ON)
3837 if ( hasNegation ) return cmdChain . unsetMark ( markName , { extendEmptyMarkRange } ) . run ( ) ;
3938
40- // 2) If inline is present and style is also ON, we must both remove inline AND add negation
4139 if ( hasInline && styleOn ) {
4240 return cmdChain
4341 . unsetMark ( markName , { extendEmptyMarkRange } )
4442 . setMark ( markName , negationAttrs , { extendEmptyMarkRange } )
4543 . run ( ) ;
4644 }
4745
48- // 3) If only inline is present, remove it (turn OFF)
4946 if ( hasInline ) return cmdChain . unsetMark ( markName , { extendEmptyMarkRange } ) . run ( ) ;
50-
51- // 4) If only style is present, add negation (turn OFF)
5247 if ( styleOn ) return cmdChain . setMark ( markName , negationAttrs , { extendEmptyMarkRange } ) . run ( ) ;
5348
54- // 5) Neither inline nor style is present; turn ON inline
5549 return cmdChain . setMark ( markName , { } , { extendEmptyMarkRange } ) . run ( ) ;
5650 } ;
5751
58- /**
59- * Default style detector that checks run-level or paragraph-level styleId
60- * @param {Object } params
61- * @returns {boolean }
62- */
63- export function defaultStyleDetector ( { state, selectionMarks, markName, editor } ) {
64- try {
65- const styleId = getEffectiveStyleId ( state , selectionMarks ) ;
66- if ( ! styleId || ! editor ?. converter ?. linkedStyles ) return false ;
67- // Resolve styles with basedOn chain
68- const styles = editor . converter . linkedStyles ;
69- const seen = new Set ( ) ;
70- let current = styleId ;
71- const key = mapMarkToStyleKey ( markName ) ;
72- while ( current && ! seen . has ( current ) ) {
73- seen . add ( current ) ;
74- const style = styles . find ( ( s ) => s . id === current ) ;
75- const def = style ?. definition ?. styles || { } ;
76- if ( key in def ) {
77- const raw = def [ key ] ;
78- // Some style parsers set the key with undefined value to indicate presence (ON)
79- if ( raw === undefined ) return true ;
80- const val = raw ?. value ?? raw ;
81- return isStyleTokenEnabled ( val ) ;
82- }
83- current = style ?. definition ?. attrs ?. basedOn || null ;
52+ function isRunPropertyEnabled ( value ) {
53+ if ( value == null ) return false ;
54+ if ( typeof value === 'object' ) {
55+ if ( 'w:val' in value ) {
56+ return isStyleTokenEnabled ( value [ 'w:val' ] ) ;
57+ }
58+ if ( 'val' in value ) {
59+ return isStyleTokenEnabled ( value . val ) ;
8460 }
85- return false ;
86- } catch {
87- return false ;
88- }
89- }
90-
91- /**
92- * Determines the effective style ID for the current selection/cursor position
93- * by checking multiple sources in priority order.
94- *
95- * Priority hierarchy:
96- * 1. Run-level rStyle from selection marks (highest priority)
97- * 2. Cursor-adjacent node marks (handles boundaries where selection marks omit run mark)
98- * 3. TextStyle styleId mark from selection marks
99- * 4. Paragraph ancestor styleId (lowest priority)
100- *
101- * @param {Object } state - The ProseMirror editor state
102- * @param {Array } selectionMarks - Array of marks from the current selection
103- * @returns {string|null } The effective style ID, or null if none found
104- */
105- export function getEffectiveStyleId ( state , selectionMarks ) {
106- // 1) Run-level style resolved from the current mark set
107- const sidFromMarks = getStyleIdFromMarks ( selectionMarks ) ;
108- if ( sidFromMarks ) return sidFromMarks ;
109-
110- // 2) Cursor-adjacent marks (handles cursor at text boundaries where selection marks omit run mark)
111- const $from = state . selection . $from ;
112- const before = $from . nodeBefore ;
113- const after = $from . nodeAfter ;
114- if ( before && before . marks ) {
115- const sid = getStyleIdFromMarks ( before . marks ) ;
116- if ( sid ) return sid ;
117- }
118- if ( after && after . marks ) {
119- const sid = getStyleIdFromMarks ( after . marks ) ;
120- if ( sid ) return sid ;
121- }
122-
123- // 3) TextStyle styleId mark
124- const ts = selectionMarks . find ( ( m ) => m . type ?. name === 'textStyle' && m . attrs ?. styleId ) ;
125- if ( ts ) return ts . attrs . styleId ;
126-
127- // 4) Paragraph ancestor styleId
128- const pos = state . selection . $from . pos ;
129- const $pos = state . doc . resolve ( pos ) ;
130- for ( let d = $pos . depth ; d >= 0 ; d -- ) {
131- const n = $pos . node ( d ) ;
132- if ( n ?. type ?. name === 'paragraph' ) return n . attrs ?. styleId || null ;
13361 }
134- return null ;
135- }
136-
137- /**
138- * Get the style ID from an array of marks.
139- * @param {import('prosemirror-model').Mark[] } marks
140- * @returns {string|null }
141- */
142- export function getStyleIdFromMarks ( marks ) {
143- if ( ! Array . isArray ( marks ) ) return null ;
144-
145- const textStyleMark = marks . find ( ( m ) => m . type ?. name === 'textStyle' && m . attrs ?. styleId ) ;
146- if ( textStyleMark ) return textStyleMark . attrs . styleId ;
147-
148- return null ;
149- }
150-
151- /**
152- * Maps a mark name to its corresponding style key.
153- * Special case: both 'textStyle' and 'color' marks map to the 'color' style key.
154- * All other mark names map directly to themselves.
155- *
156- * @param {string } markName - The name of the mark to map
157- * @returns {string } The corresponding style key
158- */
159- export function mapMarkToStyleKey ( markName ) {
160- if ( markName === 'textStyle' || markName === 'color' ) return 'color' ;
161- return markName ;
62+ return isStyleTokenEnabled ( value ) ;
16263}
16364
16465export function isStyleTokenEnabled ( val ) {
0 commit comments