@@ -5,7 +5,7 @@ import {EditorState, TextSelection} from 'prosemirror-state';
55import type { Parser } from '../core/types/parser' ;
66import { ParserFacet } from '../core/utils/parser' ;
77
8- import { canApplyInlineMarkInMarkdown } from './marks' ;
8+ import { canApplyInlineMarkInMarkdown , selectionAllHasMarkWithAttr } from './marks' ;
99
1010const schema = new Schema ( {
1111 nodes : {
@@ -15,6 +15,21 @@ const schema = new Schema({
1515 } ,
1616} ) ;
1717
18+ // Schema with a color mark (parameterised, excludes itself)
19+ const colorSchema = new Schema ( {
20+ nodes : {
21+ doc : { content : 'block+' } ,
22+ paragraph : { content : 'inline*' , group : 'block' , marks : '_' } ,
23+ text : { group : 'inline' } ,
24+ } ,
25+ marks : {
26+ color : {
27+ attrs : { color : { } } ,
28+ excludes : '_' ,
29+ } ,
30+ } ,
31+ } ) ;
32+
1833const md = new MarkdownIt ( ) ;
1934const mockParser : Parser = {
2035 isPunctChar : ( ch : string ) => md . utils . isPunctChar ( ch ) ,
@@ -37,6 +52,87 @@ function canApply(text: string, from: number, to: number): boolean {
3752 return canApplyInlineMarkInMarkdown ( state ) ;
3853}
3954
55+ // ─── helpers for selectionAllHasMarkWithAttr tests ───────────────────────────
56+
57+ const colorMark = colorSchema . marks . color ;
58+
59+ /**
60+ * Build a state whose paragraph contains segments described by `parts`.
61+ * Each part is either a plain string, or {text, color} for a colored segment.
62+ * `from`/`to` are 0-based character indices inside the paragraph text.
63+ */
64+ function makeColorState (
65+ parts : Array < string | { text : string ; color : string } > ,
66+ from : number ,
67+ to : number ,
68+ ) : EditorState {
69+ const nodes = parts . map ( ( p ) => {
70+ if ( typeof p === 'string' ) return colorSchema . text ( p ) ;
71+ return colorSchema . text ( p . text , [ colorMark . create ( { color : p . color } ) ] ) ;
72+ } ) ;
73+ const doc = colorSchema . node ( 'doc' , null , [ colorSchema . node ( 'paragraph' , null , nodes ) ] ) ;
74+ // PM positions: 0=before doc, 1=start of paragraph content
75+ const sel = TextSelection . create ( doc , from + 1 , to + 1 ) ;
76+ return EditorState . create ( { doc, selection : sel } ) ;
77+ }
78+
79+ function allHasColor (
80+ parts : Array < string | { text : string ; color : string } > ,
81+ from : number ,
82+ to : number ,
83+ color : string ,
84+ ) : boolean {
85+ const state = makeColorState ( parts , from , to ) ;
86+ return selectionAllHasMarkWithAttr ( state , colorMark , 'color' , color ) ;
87+ }
88+
89+ describe ( 'selectionAllHasMarkWithAttr' , ( ) => {
90+ it ( 'returns true when entire selection has the exact color' , ( ) => {
91+ // "ABC" all red — select all 3 chars
92+ expect ( allHasColor ( [ { text : 'ABC' , color : 'red' } ] , 0 , 3 , 'red' ) ) . toBe ( true ) ;
93+ } ) ;
94+
95+ it ( 'returns false when part of the selection has no color' , ( ) => {
96+ // "AB" red, "C" plain — select all 3
97+ expect ( allHasColor ( [ { text : 'AB' , color : 'red' } , 'C' ] , 0 , 3 , 'red' ) ) . toBe ( false ) ;
98+ } ) ;
99+
100+ it ( 'returns false when part of the selection has a different color' , ( ) => {
101+ // "AB" red, "C" blue — select all 3, check for red
102+ expect (
103+ allHasColor (
104+ [
105+ { text : 'AB' , color : 'red' } ,
106+ { text : 'C' , color : 'blue' } ,
107+ ] ,
108+ 0 ,
109+ 3 ,
110+ 'red' ,
111+ ) ,
112+ ) . toBe ( false ) ;
113+ } ) ;
114+
115+ it ( 'returns false when checking a color that is not applied' , ( ) => {
116+ // "ABC" all red — check for blue
117+ expect ( allHasColor ( [ { text : 'ABC' , color : 'red' } ] , 0 , 3 , 'blue' ) ) . toBe ( false ) ;
118+ } ) ;
119+
120+ it ( 'returns true when selection covers only a whitespace-only node (skipped)' , ( ) => {
121+ // " " plain spaces — whitespace-only nodes are skipped, so result is vacuously true
122+ expect ( allHasColor ( [ ' ' ] , 0 , 3 , 'red' ) ) . toBe ( true ) ;
123+ } ) ;
124+
125+ it ( 'returns true for sub-selection that is entirely colored' , ( ) => {
126+ // "A" plain, "BCD" red, "E" plain — select chars 1–4 (BCD)
127+ expect ( allHasColor ( [ 'A' , { text : 'BCD' , color : 'red' } , 'E' ] , 1 , 4 , 'red' ) ) . toBe ( true ) ;
128+ } ) ;
129+
130+ it ( 'returns false for sub-selection that spans colored and plain' , ( ) => {
131+ // "AB" plain, "CD" red — select chars 1–4 (BCD)
132+ expect ( allHasColor ( [ 'AB' , { text : 'CD' , color : 'red' } ] , 1 , 4 , 'red' ) ) . toBe ( false ) ;
133+ } ) ;
134+ } ) ;
135+
40136describe ( 'canApplyInlineMarkInMarkdown' , ( ) => {
41137 it ( 'allows empty selection (cursor)' , ( ) => {
42138 expect ( canApply ( 'hello,' , 0 , 0 ) ) . toBe ( true ) ;
0 commit comments