All files / roosterjs-content-model-api/lib/publicApi/segment changeCapitalization.ts

97.14% Statements 34/35
90.91% Branches 20/22
100% Functions 4/4
96.97% Lines 32/33

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 1001x                             1x         15x   15x 17x 14x   3x 3x     4x 4x     6x           6x 6x   6x 9x         6x 6x     1x           1x   1x   6x 1x                             6x       6x   6x 3x   3x 1x 2x 1x   1x       5x    
import { formatSegmentWithContentModel } from '../utils/formatSegmentWithContentModel';
import type {
    IEditor,
    ShallowMutableContentModelParagraph,
    ShallowMutableContentModelSegment,
} from 'roosterjs-content-model-types';
 
/**
 * Change the capitalization of text in the selection
 * @param editor The editor instance
 * @param capitalization The case option
 * @param language Optional parameter for language string that should comply to "IETF BCP 47 Tags for
 * Identifying Languages". For example: 'en' or 'en-US' for English, 'tr' for Turkish.
 * Default is the host environment’s current locale.
 */
export function changeCapitalization(
    editor: IEditor,
    capitalization: 'sentence' | 'lowerCase' | 'upperCase' | 'capitalize',
    language?: string
) {
    editor.focus();
 
    formatSegmentWithContentModel(editor, 'changeCapitalization', (_, __, segment, paragraph) => {
        if (segment?.segmentType == 'Text') {
            switch (capitalization) {
                case 'lowerCase':
                    segment.text = segment.text.toLocaleLowerCase(language);
                    break;
 
                case 'upperCase':
                    segment.text = segment.text.toLocaleUpperCase(language);
                    break;
 
                case 'capitalize':
                    const wordArray = segment.text.toLocaleLowerCase(language).split(' ');
 
                    // When a collapsed selection is in the middle of a word, the word is split
                    // into multiple text segments around the selection marker. In that case the
                    // first segment of the word continues from a previous segment, so its first
                    // word must not be capitalized to avoid results like "HeLlo" for "he|llo".
                    const precedingChar = getPrecedingCharacter(paragraph, segment);
                    const startIndex = precedingChar && precedingChar != ' ' ? 1 : 0;
 
                    for (let i = startIndex; i < wordArray.length; i++) {
                        wordArray[i] =
                            wordArray[i].charAt(0).toLocaleUpperCase(language) +
                            wordArray[i].slice(1);
                    }
 
                    segment.text = wordArray.join(' ');
                    break;
 
                case 'sentence':
                    const punctuationMarks = '[\\.\\!\\?]';
                    // Find a match of a word character either:
                    // - At the beginning of a string with or without preceding whitespace, for
                    // example: '  hello world' and 'hello world' strings would both match 'h'.
                    // - Or preceded by a punctuation mark and at least one whitespace, for
                    // example 'yes. hello world' would match 'y' and 'h'.
                    const regex = new RegExp('^\\s*\\w|' + punctuationMarks + '\\s+\\w', 'g');
 
                    segment.text = segment.text
                        .toLocaleLowerCase(language)
                        .replace(regex, match => match.toLocaleUpperCase(language));
                    break;
            }
        }
    });
}
 
/**
 * Get the character immediately preceding the given segment within its paragraph, skipping
 * selection markers (which carry no text). Returns an empty string if there is no preceding
 * text character (e.g. the segment starts the paragraph or follows a non-text segment).
 */
function getPrecedingCharacter(
    paragraph: ShallowMutableContentModelParagraph | null,
    segment: ShallowMutableContentModelSegment
): string {
    Iif (!paragraph) {
        return '';
    }
 
    const index = paragraph.segments.indexOf(segment);
 
    for (let i = index - 1; i >= 0; i--) {
        const previous = paragraph.segments[i];
 
        if (previous.segmentType == 'SelectionMarker') {
            continue;
        } else if (previous.segmentType == 'Text' && previous.text.length > 0) {
            return previous.text[previous.text.length - 1];
        } else {
            break;
        }
    }
 
    return '';
}