Skip to content

Commit 7a5d3ec

Browse files
BryanValverdeUJiuqingSongwisaulniwisaulni
authored
Bump to 9.32.0 (#3086)
* Fix #3080 (#3084) * Add auto direction to setDirection (#3082) * Initial commit * Pending changes exported from your codespace * fix code and tests * Address comments --------- Co-authored-by: wisaulni <[email protected]> Co-authored-by: Bryan Valverde U <[email protected]> * update versions --------- Co-authored-by: Jiuqing Song <[email protected]> Co-authored-by: wisaulni <[email protected]> Co-authored-by: wisaulni <[email protected]>
1 parent b415cf1 commit 7a5d3ec

File tree

6 files changed

+306
-14
lines changed

6 files changed

+306
-14
lines changed

packages/roosterjs-content-model-api/lib/modelApi/block/setModelDirection.ts

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,22 @@ import type {
1414
PaddingFormat,
1515
ReadonlyContentModelBlock,
1616
ReadonlyContentModelDocument,
17+
ReadonlyContentModelText,
1718
} from 'roosterjs-content-model-types';
1819

20+
// Regexes for character direction detection
21+
// Strongly typed RTL character ranges. Referenced unicode's DerivedBidiClass.txt, excluding things in the 2 bit range.
22+
const RTL_CHAR_REGEX = /[\u0590-\u05FF\u0600-\u08FF\uFB1D-\uFDFF\uFE70-\uFEFF]/g;
23+
const URL_CHAR_REGEX = /http\S+|www\S+|https\S+|<a\s+(?:[^>]*?\s+)?href=(["']).*?\1.*?>.*?<\/a>/g;
24+
const WHITESPACE_REGEX = /\s/g;
25+
1926
/**
2027
* @internal
2128
*/
22-
export function setModelDirection(model: ReadonlyContentModelDocument, direction: 'ltr' | 'rtl') {
29+
export function setModelDirection(
30+
model: ReadonlyContentModelDocument,
31+
direction: 'ltr' | 'rtl' | 'auto'
32+
) {
2333
splitSelectedParagraphByBr(model);
2434

2535
const paragraphOrListItemOrTable = getOperationalBlocks<ContentModelListItem>(
@@ -29,20 +39,26 @@ export function setModelDirection(model: ReadonlyContentModelDocument, direction
2939
);
3040

3141
paragraphOrListItemOrTable.forEach(({ block }) => {
42+
let calcDirection: 'ltr' | 'rtl';
43+
if (direction === 'auto') {
44+
calcDirection = determineTextDirection(block);
45+
} else {
46+
calcDirection = direction;
47+
}
3248
if (isBlockGroupOfType<ContentModelListItem>(block, 'ListItem')) {
3349
const items = findListItemsInSameThread(model, block);
3450

3551
items.forEach(readonlyItem => {
3652
const item = mutateBlock(readonlyItem);
3753

3854
item.levels.forEach(level => {
39-
level.format.direction = direction;
55+
level.format.direction = calcDirection;
4056
});
4157

42-
item.blocks.forEach(block => internalSetDirection(block, direction));
58+
item.blocks.forEach(block => internalSetDirection(block, calcDirection));
4359
});
4460
} else if (block) {
45-
internalSetDirection(block, direction);
61+
internalSetDirection(block, calcDirection);
4662
}
4763
});
4864

@@ -99,3 +115,34 @@ function setProperty(
99115
delete format[key];
100116
}
101117
}
118+
119+
// Designed to match browser's 'auto' detection, by scanning over the inner text until it hits a strong LTR/RTL character
120+
function determineTextDirection(block: ReadonlyContentModelBlock): 'ltr' | 'rtl' {
121+
if (block.blockType === 'Paragraph') {
122+
const findTextSegements: ReadonlyContentModelText[] = block.segments.filter(
123+
(seg): seg is ReadonlyContentModelText => seg.segmentType === 'Text'
124+
);
125+
let innerText =
126+
findTextSegements.length > 0
127+
? findTextSegements.reduce((prev, seg) => prev + seg.text, '')
128+
: undefined;
129+
if (!!innerText) {
130+
// Remove links
131+
innerText = innerText.replace(URL_CHAR_REGEX, '');
132+
133+
// Remove whitespace
134+
innerText = innerText.replace(WHITESPACE_REGEX, '');
135+
136+
const rtlMatches = innerText.match(RTL_CHAR_REGEX);
137+
const rtlCount = rtlMatches ? rtlMatches.length : 0;
138+
139+
const ltrCount = innerText.length - rtlCount;
140+
141+
return rtlCount > ltrCount ? 'rtl' : 'ltr';
142+
} else {
143+
return 'ltr'; // Default to LTR if no text is found
144+
}
145+
} else {
146+
return 'ltr';
147+
}
148+
}

packages/roosterjs-content-model-api/lib/publicApi/block/setDirection.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import type { IEditor } from 'roosterjs-content-model-types';
44
/**
55
* Set text direction of selected paragraphs (Left to right or Right to left)
66
* @param editor The editor to set alignment
7-
* @param direction Direction value: ltr (Left to right) or rtl (Right to left)
7+
* @param direction Direction value: ltr (Left to right) or rtl (Right to left), or 'auto' (Based on the first characters of the document, set the direction automatically)
88
*/
9-
export function setDirection(editor: IEditor, direction: 'ltr' | 'rtl') {
9+
export function setDirection(editor: IEditor, direction: 'ltr' | 'rtl' | 'auto') {
1010
editor.focus();
1111

1212
editor.formatContentModel(model => setModelDirection(model, direction), {

packages/roosterjs-content-model-api/test/publicApi/block/setDirectionTest.ts

Lines changed: 214 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ describe('setDirection', () => {
66
function runTest(
77
model: ContentModelDocument,
88
result: ContentModelDocument,
9-
calledTimes: number
9+
calledTimes: number,
10+
overrideDirection?: 'ltr' | 'rtl' | 'auto'
1011
) {
1112
paragraphTestCommon(
1213
'setDirection',
13-
editor => setDirection(editor, 'rtl'),
14+
editor => setDirection(editor, overrideDirection ?? 'rtl'),
1415
model,
1516
result,
1617
calledTimes
@@ -519,4 +520,215 @@ describe('setDirection', () => {
519520
1
520521
);
521522
});
523+
524+
it('Properly detects ltr', () => {
525+
runTest(
526+
{
527+
blockGroupType: 'Document',
528+
blocks: [
529+
{
530+
blockType: 'Paragraph',
531+
format: {},
532+
segments: [
533+
{
534+
segmentType: 'Text',
535+
text: 'test',
536+
format: {},
537+
isSelected: true,
538+
},
539+
],
540+
},
541+
],
542+
},
543+
{
544+
blockGroupType: 'Document',
545+
blocks: [
546+
{
547+
blockType: 'Paragraph',
548+
format: {},
549+
segments: [
550+
{
551+
segmentType: 'Text',
552+
text: 'test',
553+
format: {},
554+
isSelected: true,
555+
},
556+
],
557+
},
558+
],
559+
},
560+
1,
561+
'auto'
562+
);
563+
});
564+
565+
it('Properly detects rtl', () => {
566+
runTest(
567+
{
568+
blockGroupType: 'Document',
569+
blocks: [
570+
{
571+
blockType: 'Paragraph',
572+
format: {},
573+
segments: [
574+
{
575+
segmentType: 'Text',
576+
text: 'هذه جملة مثال',
577+
format: {},
578+
isSelected: true,
579+
},
580+
],
581+
},
582+
],
583+
},
584+
{
585+
blockGroupType: 'Document',
586+
blocks: [
587+
{
588+
blockType: 'Paragraph',
589+
format: {
590+
direction: 'rtl',
591+
},
592+
segments: [
593+
{
594+
segmentType: 'Text',
595+
text: 'هذه جملة مثال',
596+
format: {},
597+
isSelected: true,
598+
},
599+
],
600+
},
601+
],
602+
},
603+
1,
604+
'auto'
605+
);
606+
});
607+
608+
it('Properly detects rtl, even with some ltr characters', () => {
609+
runTest(
610+
{
611+
blockGroupType: 'Document',
612+
blocks: [
613+
{
614+
blockType: 'Paragraph',
615+
format: {},
616+
segments: [
617+
{
618+
segmentType: 'Text',
619+
text: 'هذه جملة مثال hello',
620+
format: {},
621+
isSelected: true,
622+
},
623+
],
624+
},
625+
],
626+
},
627+
{
628+
blockGroupType: 'Document',
629+
blocks: [
630+
{
631+
blockType: 'Paragraph',
632+
format: {
633+
direction: 'rtl',
634+
},
635+
segments: [
636+
{
637+
segmentType: 'Text',
638+
text: 'هذه جملة مثال hello',
639+
format: {},
640+
isSelected: true,
641+
},
642+
],
643+
},
644+
],
645+
},
646+
1,
647+
'auto'
648+
);
649+
});
650+
651+
it('Properly detects ltr, even with some rtl characters', () => {
652+
runTest(
653+
{
654+
blockGroupType: 'Document',
655+
blocks: [
656+
{
657+
blockType: 'Paragraph',
658+
format: {},
659+
segments: [
660+
{
661+
segmentType: 'Text',
662+
text: 'hello world كلمة',
663+
format: {},
664+
isSelected: true,
665+
},
666+
],
667+
},
668+
],
669+
},
670+
{
671+
blockGroupType: 'Document',
672+
blocks: [
673+
{
674+
blockType: 'Paragraph',
675+
format: {},
676+
segments: [
677+
{
678+
segmentType: 'Text',
679+
text: 'hello world كلمة',
680+
format: {},
681+
isSelected: true,
682+
},
683+
],
684+
},
685+
],
686+
},
687+
1,
688+
'auto'
689+
);
690+
});
691+
692+
it('Properly detects rtl, even with a url', () => {
693+
runTest(
694+
{
695+
blockGroupType: 'Document',
696+
blocks: [
697+
{
698+
blockType: 'Paragraph',
699+
format: {},
700+
segments: [
701+
{
702+
segmentType: 'Text',
703+
text: 'هذه جملة مثال https://www.contoso.com',
704+
format: {},
705+
isSelected: true,
706+
},
707+
],
708+
},
709+
],
710+
},
711+
{
712+
blockGroupType: 'Document',
713+
blocks: [
714+
{
715+
blockType: 'Paragraph',
716+
format: {
717+
direction: 'rtl',
718+
},
719+
segments: [
720+
{
721+
segmentType: 'Text',
722+
text: 'هذه جملة مثال https://www.contoso.com',
723+
format: {},
724+
isSelected: true,
725+
},
726+
],
727+
},
728+
],
729+
},
730+
1,
731+
'auto'
732+
);
733+
});
522734
});

packages/roosterjs-content-model-plugins/lib/edit/keyboardEnter.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export function keyboardEnter(
3535
? []
3636
: [handleAutoLink, handleEnterOnList, deleteEmptyQuote];
3737

38-
if (handleNormalEnter || hasEnterForEntity(result.insertPoint?.paragraph)) {
38+
if (handleNormalEnter || handleEnterForEntity(result.insertPoint?.paragraph)) {
3939
steps.push(handleEnterOnParagraph);
4040
}
4141

@@ -64,9 +64,9 @@ export function keyboardEnter(
6464
);
6565
}
6666

67-
function hasEnterForEntity(paragraph: ReadonlyContentModelParagraph | undefined) {
67+
function handleEnterForEntity(paragraph: ReadonlyContentModelParagraph | undefined) {
6868
return (
6969
paragraph &&
70-
(paragraph.isImplicit || paragraph.segments.some(x => x.segmentType == 'SelectionMarker'))
70+
(paragraph.isImplicit || paragraph.segments.some(x => x.segmentType == 'Entity'))
7171
);
7272
}

0 commit comments

Comments
 (0)