@@ -13,6 +13,7 @@ import {
1313 ContentModelSegment ,
1414 ContentModelSegmentHandler ,
1515 ModelToDomContext ,
16+ ModelToDomSegmentContext ,
1617} from 'roosterjs-content-model-types' ;
1718
1819describe ( 'handleParagraph' , ( ) => {
@@ -1170,4 +1171,231 @@ describe('Handle paragraph and adjust selections', () => {
11701171 context . rewriteFromModel
11711172 ) ;
11721173 } ) ;
1174+
1175+ describe ( 'noFollowingTextSegmentOrLast' , ( ) => {
1176+ let parent : HTMLElement ;
1177+ let context : ModelToDomContext ;
1178+ let handleSegment : jasmine . Spy < ContentModelSegmentHandler < ContentModelSegment > > ;
1179+
1180+ beforeEach ( ( ) => {
1181+ parent = document . createElement ( 'div' ) ;
1182+ handleSegment = jasmine . createSpy ( 'handleSegment' ) ;
1183+ context = createModelToDomContext (
1184+ {
1185+ allowCacheElement : true ,
1186+ } ,
1187+ {
1188+ modelHandlerOverride : {
1189+ segment : handleSegment ,
1190+ } ,
1191+ }
1192+ ) ;
1193+ } ) ;
1194+
1195+ it ( 'should be true for the only text segment' , ( ) => {
1196+ const captured : ( boolean | undefined ) [ ] = [ ] ;
1197+ handleSegment . and . callFake ( ( _doc , _parent , _seg , ctx ) => {
1198+ captured . push ( ( ctx as ModelToDomSegmentContext ) . noFollowingTextSegmentOrLast ) ;
1199+ } ) ;
1200+
1201+ const paragraph : ContentModelParagraph = {
1202+ blockType : 'Paragraph' ,
1203+ segments : [ { segmentType : 'Text' , text : 'hello' , format : { } } ] ,
1204+ format : { } ,
1205+ } ;
1206+ handleParagraph ( document , parent , paragraph , context , null ) ;
1207+
1208+ expect ( captured ) . toEqual ( [ true ] ) ;
1209+ } ) ;
1210+
1211+ it ( 'should be false for text before another text segment' , ( ) => {
1212+ const captured : ( boolean | undefined ) [ ] = [ ] ;
1213+ handleSegment . and . callFake ( ( _doc , _parent , _seg , ctx ) => {
1214+ captured . push ( ( ctx as ModelToDomSegmentContext ) . noFollowingTextSegmentOrLast ) ;
1215+ } ) ;
1216+
1217+ const paragraph : ContentModelParagraph = {
1218+ blockType : 'Paragraph' ,
1219+ segments : [
1220+ { segmentType : 'Text' , text : 'first' , format : { } } ,
1221+ { segmentType : 'Text' , text : 'second' , format : { } } ,
1222+ ] ,
1223+ format : { } ,
1224+ } ;
1225+ handleParagraph ( document , parent , paragraph , context , null ) ;
1226+
1227+ expect ( captured ) . toEqual ( [ false , true ] ) ;
1228+ } ) ;
1229+
1230+ it ( 'should be true for text followed by a Br segment' , ( ) => {
1231+ const captured : ( boolean | undefined ) [ ] = [ ] ;
1232+ handleSegment . and . callFake ( ( _doc , _parent , _seg , ctx ) => {
1233+ captured . push ( ( ctx as ModelToDomSegmentContext ) . noFollowingTextSegmentOrLast ) ;
1234+ } ) ;
1235+
1236+ const paragraph : ContentModelParagraph = {
1237+ blockType : 'Paragraph' ,
1238+ segments : [
1239+ { segmentType : 'Text' , text : 'hello' , format : { } } ,
1240+ { segmentType : 'Br' , format : { } } ,
1241+ ] ,
1242+ format : { } ,
1243+ } ;
1244+ handleParagraph ( document , parent , paragraph , context , null ) ;
1245+
1246+ // Text is followed by Br (non-text), so noFollowingTextSegmentOrLast is true for text; also true for Br
1247+ expect ( captured ) . toEqual ( [ true , true ] ) ;
1248+ } ) ;
1249+
1250+ it ( 'should be true for text followed by an Image segment' , ( ) => {
1251+ const captured : ( boolean | undefined ) [ ] = [ ] ;
1252+ handleSegment . and . callFake ( ( _doc , _parent , _seg , ctx ) => {
1253+ captured . push ( ( ctx as ModelToDomSegmentContext ) . noFollowingTextSegmentOrLast ) ;
1254+ } ) ;
1255+
1256+ const paragraph : ContentModelParagraph = {
1257+ blockType : 'Paragraph' ,
1258+ segments : [
1259+ { segmentType : 'Text' , text : 'hello' , format : { } } ,
1260+ { segmentType : 'Image' , src : 'test.png' , format : { } , dataset : { } } ,
1261+ ] ,
1262+ format : { } ,
1263+ } ;
1264+ handleParagraph ( document , parent , paragraph , context , null ) ;
1265+
1266+ expect ( captured ) . toEqual ( [ true , true ] ) ;
1267+ } ) ;
1268+
1269+ it ( 'should skip SelectionMarker when determining next text segment' , ( ) => {
1270+ const captured : ( boolean | undefined ) [ ] = [ ] ;
1271+ handleSegment . and . callFake ( ( _doc , _parent , _seg , ctx ) => {
1272+ captured . push ( ( ctx as ModelToDomSegmentContext ) . noFollowingTextSegmentOrLast ) ;
1273+ } ) ;
1274+
1275+ const paragraph : ContentModelParagraph = {
1276+ blockType : 'Paragraph' ,
1277+ segments : [
1278+ { segmentType : 'Text' , text : 'first' , format : { } } ,
1279+ { segmentType : 'SelectionMarker' , isSelected : true , format : { } } ,
1280+ { segmentType : 'Text' , text : 'second' , format : { } } ,
1281+ ] ,
1282+ format : { } ,
1283+ } ;
1284+ handleParagraph ( document , parent , paragraph , context , null ) ;
1285+
1286+ // first text -> marker -> second text: first=false, marker=false, second=true
1287+ expect ( captured ) . toEqual ( [ false , false , true ] ) ;
1288+ } ) ;
1289+
1290+ it ( 'should be true when text is followed by SelectionMarker only' , ( ) => {
1291+ const captured : ( boolean | undefined ) [ ] = [ ] ;
1292+ handleSegment . and . callFake ( ( _doc , _parent , _seg , ctx ) => {
1293+ captured . push ( ( ctx as ModelToDomSegmentContext ) . noFollowingTextSegmentOrLast ) ;
1294+ } ) ;
1295+
1296+ const paragraph : ContentModelParagraph = {
1297+ blockType : 'Paragraph' ,
1298+ segments : [
1299+ { segmentType : 'Text' , text : 'hello' , format : { } } ,
1300+ { segmentType : 'SelectionMarker' , isSelected : true , format : { } } ,
1301+ ] ,
1302+ format : { } ,
1303+ } ;
1304+ handleParagraph ( document , parent , paragraph , context , null ) ;
1305+
1306+ // text -> marker (no text after): both should be true
1307+ expect ( captured ) . toEqual ( [ true , true ] ) ;
1308+ } ) ;
1309+
1310+ it ( 'should be true for text followed by Entity segment' , ( ) => {
1311+ const captured : ( boolean | undefined ) [ ] = [ ] ;
1312+ handleSegment . and . callFake ( ( _doc , _parent , _seg , ctx ) => {
1313+ captured . push ( ( ctx as ModelToDomSegmentContext ) . noFollowingTextSegmentOrLast ) ;
1314+ } ) ;
1315+
1316+ const wrapper = document . createElement ( 'span' ) ;
1317+ const paragraph : ContentModelParagraph = {
1318+ blockType : 'Paragraph' ,
1319+ segments : [
1320+ { segmentType : 'Text' , text : 'hello' , format : { } } ,
1321+ {
1322+ segmentType : 'Entity' ,
1323+ blockType : 'Entity' ,
1324+ entityFormat : { entityType : 'test' , id : 'e1' , isReadonly : true } ,
1325+ format : { } ,
1326+ wrapper,
1327+ } ,
1328+ ] ,
1329+ format : { } ,
1330+ } ;
1331+ handleParagraph ( document , parent , paragraph , context , null ) ;
1332+
1333+ expect ( captured ) . toEqual ( [ true , true ] ) ;
1334+ } ) ;
1335+
1336+ it ( 'should handle text followed by General segment' , ( ) => {
1337+ const captured : ( boolean | undefined ) [ ] = [ ] ;
1338+ handleSegment . and . callFake ( ( _doc , _parent , _seg , ctx ) => {
1339+ captured . push ( ( ctx as ModelToDomSegmentContext ) . noFollowingTextSegmentOrLast ) ;
1340+ } ) ;
1341+
1342+ const paragraph : ContentModelParagraph = {
1343+ blockType : 'Paragraph' ,
1344+ segments : [
1345+ { segmentType : 'Text' , text : 'hello' , format : { } } ,
1346+ {
1347+ segmentType : 'General' ,
1348+ blockType : 'BlockGroup' ,
1349+ blockGroupType : 'General' ,
1350+ blocks : [ ] ,
1351+ element : document . createElement ( 'span' ) ,
1352+ format : { } ,
1353+ } ,
1354+ ] ,
1355+ format : { } ,
1356+ } ;
1357+ handleParagraph ( document , parent , paragraph , context , null ) ;
1358+
1359+ expect ( captured ) . toEqual ( [ true , true ] ) ;
1360+ } ) ;
1361+
1362+ it ( 'should be false for text when text comes after non-text segment' , ( ) => {
1363+ const captured : ( boolean | undefined ) [ ] = [ ] ;
1364+ handleSegment . and . callFake ( ( _doc , _parent , _seg , ctx ) => {
1365+ captured . push ( ( ctx as ModelToDomSegmentContext ) . noFollowingTextSegmentOrLast ) ;
1366+ } ) ;
1367+
1368+ const paragraph : ContentModelParagraph = {
1369+ blockType : 'Paragraph' ,
1370+ segments : [
1371+ { segmentType : 'Text' , text : 'first' , format : { } } ,
1372+ { segmentType : 'Br' , format : { } } ,
1373+ { segmentType : 'Text' , text : 'second' , format : { } } ,
1374+ ] ,
1375+ format : { } ,
1376+ } ;
1377+ handleParagraph ( document , parent , paragraph , context , null ) ;
1378+
1379+ // first text: Br is next non-marker -> false (Br breaks, no text immediately), but actually
1380+ // hasTextSegmentAfter looks past Br to find Text, Br is not Text so returns false at index 0
1381+ // Br at index 1: next is Text -> false? No, hasTextSegmentAfter checks if type === 'Text'
1382+ // Actually: index 0 -> next is Br (not SelectionMarker, not Text) -> false -> noFollowingTextSegmentOrLast=true
1383+ // index 1 (Br) -> next is Text -> true -> noFollowingTextSegmentOrLast=false
1384+ // index 2 (Text) -> no more -> noFollowingTextSegmentOrLast=true
1385+ expect ( captured ) . toEqual ( [ true , false , true ] ) ;
1386+ } ) ;
1387+
1388+ it ( 'should be cleaned up after processing paragraph' , ( ) => {
1389+ handleSegment . and . callFake ( ( ) => { } ) ;
1390+
1391+ const paragraph : ContentModelParagraph = {
1392+ blockType : 'Paragraph' ,
1393+ segments : [ { segmentType : 'Text' , text : 'hello' , format : { } } ] ,
1394+ format : { } ,
1395+ } ;
1396+ handleParagraph ( document , parent , paragraph , context , null ) ;
1397+
1398+ expect ( ( context as ModelToDomSegmentContext ) . noFollowingTextSegmentOrLast ) . toBeUndefined ( ) ;
1399+ } ) ;
1400+ } ) ;
11731401} ) ;
0 commit comments