Skip to content

Commit

Permalink
Merge pull request #13226 from ckeditor/ck/13225-find-and-replace-utils
Browse files Browse the repository at this point in the history
Internal (find-and-replace): Moved find and replace utils into `FindAndReplaceUtils` plugin. Closes #13225.
  • Loading branch information
scofalik authored Jan 13, 2023
2 parents bf96e14 + 0c37667 commit e2cf011
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 176 deletions.
17 changes: 13 additions & 4 deletions packages/ckeditor5-find-and-replace/src/findandreplaceediting.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
*/

import { Plugin } from 'ckeditor5/src/core';
import { updateFindResultFromRange } from './utils';
import FindCommand from './findcommand';
import ReplaceCommand from './replacecommand';
import ReplaceAllCommand from './replaceallcommand';
import FindNextCommand from './findnextcommand';
import FindPreviousCommand from './findpreviouscommand';
import FindAndReplaceState from './findandreplacestate';
import FindAndReplaceUtils from './findandreplaceutils';

// eslint-disable-next-line ckeditor5-rules/ckeditor-imports
import { scrollViewportToShowTarget } from '@ckeditor/ckeditor5-utils/src/dom/scroll';
Expand All @@ -26,9 +26,10 @@ import '../theme/findandreplace.css';
const HIGHLIGHT_CLASS = 'ck-find-result_selected';

// Reacts to document changes in order to update search list.
function onDocumentChange( results, model, searchCallback ) {
function onDocumentChange( results, editor, searchCallback ) {
const changedNodes = new Set();
const removedMarkers = new Set();
const model = editor.model;

const changes = model.document.differ.getChanges();

Expand Down Expand Up @@ -73,7 +74,8 @@ function onDocumentChange( results, model, searchCallback ) {

// Run search callback again on updated nodes.
changedNodes.forEach( nodeToCheck => {
updateFindResultFromRange( model.createRangeOn( nodeToCheck ), model, searchCallback, results );
const findAndReplaceUtils = editor.plugins.get( 'FindAndReplaceUtils' );
findAndReplaceUtils.updateFindResultFromRange( model.createRangeOn( nodeToCheck ), model, searchCallback, results );
} );
}

Expand All @@ -83,6 +85,13 @@ function onDocumentChange( results, model, searchCallback ) {
* @extends module:core/plugin~Plugin
*/
export default class FindAndReplaceEditing extends Plugin {
/**
* @inheritDoc
*/
static get requires() {
return [ FindAndReplaceUtils ];
}

/**
* @inheritDoc
*/
Expand Down Expand Up @@ -173,7 +182,7 @@ export default class FindAndReplaceEditing extends Plugin {
this._activeResults = results;

// @todo: handle this listener, another copy is in findcommand.js file.
this.listenTo( model.document, 'change:data', () => onDocumentChange( this._activeResults, model, findCallback ) );
this.listenTo( model.document, 'change:data', () => onDocumentChange( this._activeResults, editor, findCallback ) );

return this._activeResults;
}
Expand Down
179 changes: 179 additions & 0 deletions packages/ckeditor5-find-and-replace/src/findandreplaceutils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/**
* @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/findandreplaceutils
*/

import { Plugin } from 'ckeditor5/src/core';
import { Collection, uid } from 'ckeditor5/src/utils';
import { escapeRegExp } from 'lodash-es';

/**
* A set of helpers related to find and replace.
*/
export default class FindAndReplaceUtils extends Plugin {
/**
* @inheritDoc
*/
static get pluginName() {
return 'FindAndReplaceUtils';
}

/**
* 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
* }
* ```
*/
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: this.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.
*/
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;
}, '' );
}

/**
* 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}
*/
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;
}
}

// 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
};
}
6 changes: 3 additions & 3 deletions packages/ckeditor5-find-and-replace/src/findcommand.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
*/

import { Command } from 'ckeditor5/src/core';
import { updateFindResultFromRange, findByTextCallback } from './utils';

/**
* The find command. It is used by the {@link module:find-and-replace/findandreplace~FindAndReplace find and replace feature}.
Expand Down Expand Up @@ -53,12 +52,13 @@ export default class FindCommand extends Command {
execute( callbackOrText, { matchCase, wholeWords } = {} ) {
const { editor } = this;
const { model } = editor;
const findAndReplaceUtils = editor.plugins.get( 'FindAndReplaceUtils' );

let findCallback;

// Allow to execute `find()` on a plugin with a keyword only.
if ( typeof callbackOrText === 'string' ) {
findCallback = findByTextCallback( callbackOrText, { matchCase, wholeWords } );
findCallback = findAndReplaceUtils.findByTextCallback( callbackOrText, { matchCase, wholeWords } );

this._state.searchText = callbackOrText;
} else {
Expand All @@ -67,7 +67,7 @@ export default class FindCommand extends Command {

// Initial search is done on all nodes in all roots inside the content.
const results = model.document.getRootNames()
.reduce( ( ( currentResults, rootName ) => updateFindResultFromRange(
.reduce( ( ( currentResults, rootName ) => findAndReplaceUtils.updateFindResultFromRange(
model.createRangeIn( model.document.getRoot( rootName ) ),
model,
findCallback,
Expand Down
1 change: 1 addition & 0 deletions packages/ckeditor5-find-and-replace/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
*/

export { default as FindAndReplace } from './findandreplace';
export { default as FindAndReplaceUtils } from './findandreplaceutils';
6 changes: 3 additions & 3 deletions packages/ckeditor5-find-and-replace/src/replaceallcommand.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
* @module find-and-replace/replaceallcommand
*/

import { updateFindResultFromRange, findByTextCallback } from './utils';
import { Collection } from 'ckeditor5/src/utils';
import ReplaceCommand from './replacecommand';

Expand Down Expand Up @@ -39,13 +38,14 @@ export default class ReplaceAllCommand extends ReplaceCommand {
execute( newText, textToReplace ) {
const { editor } = this;
const { model } = editor;
const findAndReplaceUtils = editor.plugins.get( 'FindAndReplaceUtils' );

const results = textToReplace instanceof Collection ?
textToReplace : model.document.getRootNames()
.reduce( ( ( currentResults, rootName ) => updateFindResultFromRange(
.reduce( ( ( currentResults, rootName ) => findAndReplaceUtils.updateFindResultFromRange(
model.createRangeIn( model.document.getRoot( rootName ) ),
model,
findByTextCallback( textToReplace, this._state ),
findAndReplaceUtils.findByTextCallback( textToReplace, this._state ),
currentResults
) ), null );

Expand Down
Loading

0 comments on commit e2cf011

Please sign in to comment.