File: //home/arjun/projects/buyercall/node_modules/@ckeditor/ckeditor5-find-and-replace/src/utils.js
/**
* @license Copyright (c) 2003-2022, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
/**
* @module find-and-replace/utils
*/
import { uid, Collection } from 'ckeditor5/src/utils';
import { escapeRegExp } from 'lodash-es';
/**
* Executes findCallback and updates search results list.
*
* @param {module:engine/model/range~Range} range The model range to scan for matches.
* @param {module:engine/model/model~Model} model The model.
* @param {Function} findCallback The callback that should return `true` if provided text matches the search term.
* @param {module:utils/collection~Collection} [startResults] An optional collection of find matches that the function should
* start with. This would be a collection returned by a previous `updateFindResultFromRange()` call.
* @returns {module:utils/collection~Collection} A collection of objects describing find match.
*
* An example structure:
*
* ```js
* {
* id: resultId,
* label: foundItem.label,
* marker
* }
* ```
*/
export function updateFindResultFromRange( range, model, findCallback, startResults ) {
const results = startResults || new Collection();
model.change( writer => {
[ ...range ].forEach( ( { type, item } ) => {
if ( type === 'elementStart' ) {
if ( model.schema.checkChild( item, '$text' ) ) {
const foundItems = findCallback( {
item,
text: rangeToText( model.createRangeIn( item ) )
} );
if ( !foundItems ) {
return;
}
foundItems.forEach( foundItem => {
const resultId = `findResult:${ uid() }`;
const marker = writer.addMarker( resultId, {
usingOperation: false,
affectsData: false,
range: writer.createRange(
writer.createPositionAt( item, foundItem.start ),
writer.createPositionAt( item, foundItem.end )
)
} );
const index = findInsertIndex( results, marker );
results.add(
{
id: resultId,
label: foundItem.label,
marker
},
index
);
} );
}
}
} );
} );
return results;
}
/**
* Returns text representation of a range. The returned text length should be the same as range length.
* In order to achieve this this function will replace inline elements (text-line) as new line character ("\n").
*
* @param {module:engine/model/range~Range} range The model range.
* @returns {String} The text content of the provided range.
*/
export function rangeToText( range ) {
return Array.from( range.getItems() ).reduce( ( rangeText, node ) => {
// Trim text to a last occurrence of an inline element and update range start.
if ( !( node.is( 'text' ) || node.is( 'textProxy' ) ) ) {
// Editor has only one inline element defined in schema: `<softBreak>` which is treated as new line character in blocks.
// Special handling might be needed for other inline elements (inline widgets).
return `${ rangeText }\n`;
}
return rangeText + node.data;
}, '' );
}
// Finds the appropriate index in the resultsList Collection.
function findInsertIndex( resultsList, markerToInsert ) {
const result = resultsList.find( ( { marker } ) => {
return markerToInsert.getStart().isBefore( marker.getStart() );
} );
return result ? resultsList.getIndex( result ) : resultsList.length;
}
// Maps RegExp match result to find result.
function regexpMatchToFindResult( matchResult ) {
const lastGroupIndex = matchResult.length - 1;
let startOffset = matchResult.index;
// Searches with match all flag have an extra matching group with empty string or white space matched before the word.
// If the search term starts with the space already, there is no extra group even with match all flag on.
if ( matchResult.length === 3 ) {
startOffset += matchResult[ 1 ].length;
}
return {
label: matchResult[ lastGroupIndex ],
start: startOffset,
end: startOffset + matchResult[ lastGroupIndex ].length
};
}
/**
* Creates a text matching callback for a specified search term and matching options.
*
* @param {String} searchTerm The search term.
* @param {Object} [options] Matching options.
* @param {Boolean} [options.matchCase=false] If set to `true` letter casing will be ignored.
* @param {Boolean} [options.wholeWords=false] If set to `true` only whole words that match `callbackOrText` will be matched.
* @returns {Function}
*/
export function findByTextCallback( searchTerm, options ) {
let flags = 'gu';
if ( !options.matchCase ) {
flags += 'i';
}
let regExpQuery = `(${ escapeRegExp( searchTerm ) })`;
if ( options.wholeWords ) {
const nonLetterGroup = '[^a-zA-Z\u00C0-\u024F\u1E00-\u1EFF]';
if ( !new RegExp( '^' + nonLetterGroup ).test( searchTerm ) ) {
regExpQuery = `(^|${ nonLetterGroup }|_)${ regExpQuery }`;
}
if ( !new RegExp( nonLetterGroup + '$' ).test( searchTerm ) ) {
regExpQuery = `${ regExpQuery }(?=_|${ nonLetterGroup }|$)`;
}
}
const regExp = new RegExp( regExpQuery, flags );
function findCallback( { text } ) {
const matches = [ ...text.matchAll( regExp ) ];
return matches.map( regexpMatchToFindResult );
}
return findCallback;
}