This repository has been archived by the owner on Jun 26, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #190 from ckeditor/t/ckeditor5/1490
Feature: Introduced the text transformation feature. Additionally, the `TextWatcher` util was moved to this package from `@ckeditor/ckeditor5-mention`. Closes ckeditor/ckeditor5#1490.
- Loading branch information
Showing
8 changed files
with
1,127 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,321 @@ | ||
/** | ||
* @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved. | ||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license | ||
*/ | ||
|
||
/** | ||
* @module typing/texttransformation | ||
*/ | ||
|
||
import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; | ||
import TextWatcher from './textwatcher'; | ||
|
||
// All named transformations. | ||
const TRANSFORMATIONS = { | ||
// Common symbols: | ||
copyright: { from: '(c)', to: '©' }, | ||
registeredTrademark: { from: '(r)', to: '®' }, | ||
trademark: { from: '(tm)', to: '™' }, | ||
|
||
// Mathematical: | ||
oneHalf: { from: '1/2', to: '½' }, | ||
oneThird: { from: '1/3', to: '⅓' }, | ||
twoThirds: { from: '2/3', to: '⅔' }, | ||
oneForth: { from: '1/4', to: '¼' }, | ||
threeQuarters: { from: '3/4', to: '¾' }, | ||
lessThenOrEqual: { from: '<=', to: '≤' }, | ||
greaterThenOrEqual: { from: '>=', to: '≥' }, | ||
notEqual: { from: '!=', to: '≠' }, | ||
arrowLeft: { from: '<-', to: '←' }, | ||
arrowRight: { from: '->', to: '→' }, | ||
|
||
// Typography: | ||
horizontalEllipsis: { from: '...', to: '…' }, | ||
enDash: { from: ' -- ', to: ' – ' }, | ||
emDash: { from: ' --- ', to: ' — ' }, | ||
|
||
// Quotations: | ||
// English, US | ||
quotesPrimary: { from: buildQuotesRegExp( '"' ), to: '$1“$2”' }, | ||
quotesSecondary: { from: buildQuotesRegExp( '\'' ), to: '$1‘$2’' }, | ||
|
||
// English, UK | ||
quotesPrimaryEnGb: { from: buildQuotesRegExp( '\'' ), to: '$1‘$2’' }, | ||
quotesSecondaryEnGb: { from: buildQuotesRegExp( '"' ), to: '$1“$2”' }, | ||
|
||
// Polish | ||
quotesPrimaryPl: { from: buildQuotesRegExp( '"' ), to: '$1„$2”' }, | ||
quotesSecondaryPl: { from: buildQuotesRegExp( '\'' ), to: '$1‚$2’' } | ||
}; | ||
|
||
// Transformation groups. | ||
const TRANSFORMATION_GROUPS = { | ||
symbols: [ 'copyright', 'registeredTrademark', 'trademark' ], | ||
mathematical: [ | ||
'oneHalf', 'oneThird', 'twoThirds', 'oneForth', 'threeQuarters', | ||
'lessThenOrEqual', 'greaterThenOrEqual', 'notEqual', | ||
'arrowLeft', 'arrowRight' | ||
], | ||
typography: [ 'horizontalEllipsis', 'enDash', 'emDash' ], | ||
quotes: [ 'quotesPrimary', 'quotesSecondary' ] | ||
}; | ||
|
||
// Set of default transformations provided by the feature. | ||
const DEFAULT_TRANSFORMATIONS = [ | ||
'symbols', | ||
'mathematical', | ||
'typography', | ||
'quotes' | ||
]; | ||
|
||
/** | ||
* The text transformation plugin. | ||
* | ||
* @extends module:core/plugin~Plugin | ||
*/ | ||
export default class TextTransformation extends Plugin { | ||
/** | ||
* @inheritDoc | ||
*/ | ||
static get pluginName() { | ||
return 'TextTransformation'; | ||
} | ||
|
||
/** | ||
* @inheritDoc | ||
*/ | ||
constructor( editor ) { | ||
super( editor ); | ||
|
||
editor.config.define( 'typing', { | ||
transformations: { | ||
include: DEFAULT_TRANSFORMATIONS | ||
} | ||
} ); | ||
} | ||
|
||
/** | ||
* @inheritDoc | ||
*/ | ||
init() { | ||
const editor = this.editor; | ||
const model = editor.model; | ||
|
||
const configuredTransformations = getConfiguredTransformations( editor.config.get( 'typing.transformations' ) ); | ||
|
||
for ( const transformation of configuredTransformations ) { | ||
const { from, to } = transformation; | ||
|
||
let testCallback; | ||
let textReplacer; | ||
|
||
if ( from instanceof RegExp ) { | ||
testCallback = text => from.test( text ); | ||
textReplacer = message => message.replace( from, to ); | ||
} else { | ||
testCallback = text => text.endsWith( from ); | ||
textReplacer = message => message.slice( 0, message.length - from.length ) + to; | ||
} | ||
|
||
const watcher = new TextWatcher( editor.model, testCallback ); | ||
|
||
watcher.on( 'matched:data', ( evt, data ) => { | ||
const selection = editor.model.document.selection; | ||
const focus = selection.focus; | ||
const textToReplaceLength = data.text.length; | ||
const textToInsert = textReplacer( data.text ); | ||
|
||
model.enqueueChange( model.createBatch(), writer => { | ||
const replaceRange = writer.createRange( focus.getShiftedBy( -textToReplaceLength ), focus ); | ||
|
||
model.insertContent( writer.createText( textToInsert, selection.getAttributes() ), replaceRange ); | ||
} ); | ||
} ); | ||
} | ||
} | ||
} | ||
|
||
// Returns a RegExp pattern string that detects a sentence inside a quote. | ||
// | ||
// @param {String} quoteCharacter a character to creat a pattern for. | ||
// @returns {String} | ||
function buildQuotesRegExp( quoteCharacter ) { | ||
return new RegExp( `(^|\\s)${ quoteCharacter }([^${ quoteCharacter }]+)${ quoteCharacter }$` ); | ||
} | ||
|
||
// Reads text transformation config and returns normalized array of transformations objects. | ||
// | ||
// @param {module:typing/texttransformation~TextTransformationDescription} config | ||
// @returns {Array.<module:typing/texttransformation~TextTransformationDescription>} | ||
function getConfiguredTransformations( config ) { | ||
const extra = config.extra || []; | ||
const remove = config.remove || []; | ||
const isNotRemoved = transformation => !remove.includes( transformation ); | ||
|
||
const configured = config.include.concat( extra ).filter( isNotRemoved ); | ||
|
||
return expandGroupsAndRemoveDuplicates( configured ) | ||
.filter( isNotRemoved ) // Filter out 'remove' transformations as they might be set in group | ||
.map( transformation => TRANSFORMATIONS[ transformation ] || transformation ); | ||
} | ||
|
||
// Reads definitions and expands named groups if needed to transformation names. | ||
// This method also removes duplicated named transformations if any. | ||
// | ||
// @param {Array.<String|Object>} definitions | ||
// @returns {Array.<String|Object>} | ||
function expandGroupsAndRemoveDuplicates( definitions ) { | ||
// Set is using to make sure that transformation names are not duplicated. | ||
const definedTransformations = new Set(); | ||
|
||
for ( const transformationOrGroup of definitions ) { | ||
if ( TRANSFORMATION_GROUPS[ transformationOrGroup ] ) { | ||
for ( const transformation of TRANSFORMATION_GROUPS[ transformationOrGroup ] ) { | ||
definedTransformations.add( transformation ); | ||
} | ||
} else { | ||
definedTransformations.add( transformationOrGroup ); | ||
} | ||
} | ||
|
||
return Array.from( definedTransformations ); | ||
} | ||
|
||
/** | ||
* Text transformation definition object. | ||
* | ||
* const transformations = [ | ||
* // Will replace foo with bar: | ||
* { from: 'foo', to: 'bar' }, | ||
* | ||
* // Will remove @ from emails on example.com domain, e.g. from user@example.com -> user.at.example.com: | ||
* { from: /([a-z-])@(example.com)$/i, to: '$1.at.$2' } | ||
* ] | ||
* | ||
* **Note:** The text watcher always evaluates the end of the input (the typed text). If you're passing a RegExp object you must | ||
* include `$` token to match the end of string. | ||
* | ||
* @typedef {Object} module:typing/texttransformation~TextTransformationDescription | ||
* @property {String|RegExp} from The string or RegExp to transform. | ||
* @property {String} to The text to transform compatible with `String.replace()` | ||
*/ | ||
|
||
/** | ||
* The configuration of the {@link module:typing/texttransformation~TextTransformation} feature. | ||
* | ||
* Read more in {@link module:typing/texttransformation~TextTransformationConfig}. | ||
* | ||
* @member {module:typing/texttransformation~TextTransformationConfig} module:typing/typing~TypingConfig#transformations | ||
*/ | ||
|
||
/** | ||
* The configuration of the text transformation feature. | ||
* | ||
* ClassicEditor | ||
* .create( editorElement, { | ||
* typing: { | ||
* transformations: ... // Text transformation feature options. | ||
* } | ||
* } ) | ||
* .then( ... ) | ||
* .catch( ... ); | ||
* | ||
* By default, the feature comes pre-configured | ||
* (via {@link module:typing/texttransformation~TextTransformationConfig#include `config.typing.transformations.include`}) with the | ||
* following groups of transformations: | ||
* | ||
* * Typography (group name: `typography`) | ||
* - `ellipsis`: transforms `...` to `…` | ||
* - `enDash`: transforms ` -- ` to ` – ` | ||
* - `emDash`: transforms ` --- ` to ` — ` | ||
* * Quotations (group name: `quotations`) | ||
* - `quotesPrimary`: transforms `"Foo bar"` to `“Foo bar”` | ||
* - `quotesSecondary`: transforms `'Foo bar'` to `‘Foo bar’` | ||
* * Symbols (group name: `symbols`) | ||
* - `trademark`: transforms `(tm)` to `™` | ||
* - `registeredTrademark`: transforms `(r)` to `®` | ||
* - `copyright`: transforms `(c)` to `©` | ||
* * Mathematical (group name: `mathematical`) | ||
* - `oneHalf`: transforms `1/2`, to: `½` | ||
* - `oneThird`: transforms `1/3`, to: `⅓` | ||
* - `twoThirds`: transforms `2/3`, to: `⅔` | ||
* - `oneForth`: transforms `1/4`, to: `¼` | ||
* - `threeQuarters`: transforms `3/4`, to: `¾` | ||
* - `lessThenOrEqual`: transforms `<=`, to: `≤` | ||
* - `greaterThenOrEqual`: transforms `>=`, to: `≥` | ||
* - `notEqual`: transforms `!=`, to: `≠` | ||
* - `arrowLeft`: transforms `<-`, to: `←` | ||
* - `arrowRight`: transforms `->`, to: `→` | ||
* * Misc: | ||
* - `quotesPrimaryEnGb`: transforms `'Foo bar'` to `‘Foo bar’` | ||
* - `quotesSecondaryEnGb`: transforms `"Foo bar"` to `“Foo bar”` | ||
* - `quotesPrimaryPl`: transforms `"Foo bar"` to `„Foo bar”` | ||
* - `quotesSecondaryPl`: transforms `'Foo bar'` to `‚Foo bar’` | ||
* | ||
* In order to load additional transformations, use the | ||
* {@link module:typing/texttransformation~TextTransformationConfig#extra `transformations.extra` option}. | ||
* | ||
* In order to narrow down the list of transformations, use the | ||
* {@link module:typing/texttransformation~TextTransformationConfig#remove `transformations.remove` option}. | ||
* | ||
* In order to completely override the supported transformations, use the | ||
* {@link module:typing/texttransformation~TextTransformationConfig#include `transformations.include` option}. | ||
* | ||
* Example: | ||
* | ||
* const transformationsConfig = { | ||
* include: [ | ||
* // Use only the 'quotes' and 'typography' groups. | ||
* 'quotes', | ||
* 'typography', | ||
* // Plus, some custom transformation. | ||
* { from: 'CKS', to: 'CKSource } | ||
* ], | ||
* | ||
* // Remove the 'ellipsis' transformation loaded by the 'typography' group. | ||
* remove: [ 'ellipsis' ] | ||
* }; | ||
* | ||
* @interface TextTransformationConfig | ||
*/ | ||
|
||
/* eslint-disable max-len */ | ||
/** | ||
* The standard list of text transformations supported by the editor. By default it comes pre-configured with a couple dozen of them | ||
* (see {@link module:typing/texttransformation~TextTransformationConfig} for the full list of them). You can override this list completely | ||
* by setting this option or use the other two options | ||
* ({@link module:typing/texttransformation~TextTransformationConfig#extra `transformations.extra`}, | ||
* {@link module:typing/texttransformation~TextTransformationConfig#remove `transformations.remove`}) to fine tune the default list. | ||
* | ||
* @member {Array.<module:typing/texttransformation~TextTransformationDescription>} module:typing/texttransformation~TextTransformationConfig#include | ||
*/ | ||
|
||
/** | ||
* The extra text transformations that are added to the transformations defined in | ||
* {@link module:typing/texttransformation~TextTransformationConfig#include `transformations.include`}. | ||
* | ||
* const transformationsConfig = { | ||
* extra: [ | ||
* { from: 'CKS', to: 'CKSource' } | ||
* ] | ||
* }; | ||
* | ||
* @member {Array.<module:typing/texttransformation~TextTransformationDescription>} module:typing/texttransformation~TextTransformationConfig#extra | ||
*/ | ||
|
||
/** | ||
* The text transformations names that are removed from transformations defined in | ||
* {@link module:typing/texttransformation~TextTransformationConfig#include `transformations.include`} or | ||
* {@link module:typing/texttransformation~TextTransformationConfig#extra `transformations.extra`}. | ||
* | ||
* const transformationsConfig = { | ||
* remove: [ | ||
* 'ellipsis', // Remove only 'ellipsis' from 'typography group. | ||
* 'mathematical' // Remove all transformations from 'mathematical' group. | ||
* ] | ||
* } | ||
* | ||
* @member {Array.<module:typing/texttransformation~TextTransformationDescription>} module:typing/texttransformation~TextTransformationConfig#remove | ||
*/ | ||
/* eslint-enable max-len */ |
Oops, something went wrong.