Skip to content

Commit

Permalink
feat(extension-count): add ability to count characters/words and set …
Browse files Browse the repository at this point in the history
…a soft max
  • Loading branch information
whawker committed Mar 18, 2022
1 parent 927f5c8 commit 2873f02
Show file tree
Hide file tree
Showing 30 changed files with 856 additions and 4 deletions.
23 changes: 23 additions & 0 deletions packages/remirror__extension-count/CHANGELOG.md
@@ -0,0 +1,23 @@
# @remirror/extension-count

## 1.0.1

> 2021-07-17
### Patch Changes

- [#1002](https://github.com/remirror/remirror/pull/1002) [`b3ea6f10d`](https://github.com/remirror/remirror/commit/b3ea6f10d4917f933971236be936731f75a69a70) Thanks [@ifiokjr](https://github.com/ifiokjr)! - Use carets `^` for versioning of `remirror` packages.

- Updated dependencies [[`b3ea6f10d`](https://github.com/remirror/remirror/commit/b3ea6f10d4917f933971236be936731f75a69a70)]:
- @remirror/core@1.0.1
- @remirror/pm@1.0.1

## 1.0.0

> 2021-07-17
### Patch Changes

- Updated dependencies [[`8202b65ef`](https://github.com/remirror/remirror/commit/8202b65efbce5a8338c45fd34b3efb676b7e54e7), [`adfb12a4c`](https://github.com/remirror/remirror/commit/adfb12a4cee7031eec4baa10830b0fc0134ebdc8), [`7f3569729`](https://github.com/remirror/remirror/commit/7f3569729c0d843b7745a490feda383b31aa2b7e), [`270edd91b`](https://github.com/remirror/remirror/commit/270edd91ba6badf9468721e35fa0ddc6a21c6dd2), [`b4dfcad36`](https://github.com/remirror/remirror/commit/b4dfcad364a0b41d321fbd26a97377f2b6d4047c), [`e9b10fa5a`](https://github.com/remirror/remirror/commit/e9b10fa5a50dd3e342b75b0a852627db99f22dc2), [`6ab7d2224`](https://github.com/remirror/remirror/commit/6ab7d2224d16ba821d8510e0498aaa9c420922c4), [`270edd91b`](https://github.com/remirror/remirror/commit/270edd91ba6badf9468721e35fa0ddc6a21c6dd2), [`270edd91b`](https://github.com/remirror/remirror/commit/270edd91ba6badf9468721e35fa0ddc6a21c6dd2), [`7024de573`](https://github.com/remirror/remirror/commit/7024de5738a968f2914a999e570d723899815611), [`03d0ae485`](https://github.com/remirror/remirror/commit/03d0ae485079a166a223b902ea72cbe62504b0f0)]:
- @remirror/core@1.0.0
- @remirror/pm@1.0.0
@@ -0,0 +1,5 @@
import { extensionValidityTest } from 'jest-remirror';

import { CountExtension } from '../';

extensionValidityTest(CountExtension);
38 changes: 38 additions & 0 deletions packages/remirror__extension-count/__tests__/tsconfig.json
@@ -0,0 +1,38 @@
{
"__AUTO_GENERATED__": [
"To update the configuration edit the following field.",
"`package.json > @remirror > tsconfigs > '__tests__'`",
"",
"Then run: `pnpm -w generate:ts`"
],
"extends": "../../../support/tsconfig.base.json",
"compilerOptions": {
"types": [
"jest",
"jest-extended",
"jest-axe",
"@testing-library/jest-dom",
"snapshot-diff",
"node"
],
"declaration": false,
"noEmit": true,
"skipLibCheck": true,
"importsNotUsedAsValues": "remove"
},
"include": ["./"],
"references": [
{
"path": "../src"
},
{
"path": "../../testing/src"
},
{
"path": "../../remirror/src"
},
{
"path": "../../remirror__core/src"
}
]
}
57 changes: 57 additions & 0 deletions packages/remirror__extension-count/package.json
@@ -0,0 +1,57 @@
{
"name": "@remirror/extension-count",
"version": "1.0.1",
"description": "Count characters or words in your editor, and set a soft max length",
"keywords": [
"remirror",
"extension"
],
"homepage": "https://github.com/remirror/remirror/tree/HEAD/packages/remirror__extension-count",
"repository": {
"type": "git",
"url": "https://github.com/remirror/remirror.git",
"directory": "packages/remirror__extension-count"
},
"license": "MIT",
"contributors": [
"Will Hawker <w.hawker@hotmail.co.uk>"
],
"sideEffects": false,
"exports": {
".": {
"import": "./dist/remirror-extension-count.esm.js",
"require": "./dist/remirror-extension-count.cjs.js",
"browser": "./dist/remirror-extension-count.browser.esm.js",
"types": "./dist/remirror-extension-count.cjs.d.ts",
"default": "./dist/remirror-extension-count.esm.js"
},
"./package.json": "./package.json",
"./types/*": "./dist/declarations/src/*.d.ts"
},
"main": "./dist/remirror-extension-count.cjs.js",
"module": "./dist/remirror-extension-count.esm.js",
"browser": {
"./dist/remirror-extension-count.cjs.js": "./dist/remirror-extension-count.browser.cjs.js",
"./dist/remirror-extension-count.esm.js": "./dist/remirror-extension-count.browser.esm.js"
},
"types": "./dist/remirror-extension-count.cjs.d.ts",
"files": [
"dist"
],
"dependencies": {
"@babel/runtime": "^7.13.10",
"@remirror/core": "^1.3.5"
},
"devDependencies": {
"@remirror/pm": "^1.0.11"
},
"peerDependencies": {
"@remirror/pm": "^1.0.1"
},
"publishConfig": {
"access": "public"
},
"@remirror": {
"sizeLimit": "5 KB"
}
}
36 changes: 36 additions & 0 deletions packages/remirror__extension-count/readme.md
@@ -0,0 +1,36 @@
# @remirror/extension-count

> **Count words or characters in your editor, and set a soft max length**
[![Version][version]][npm] [![Weekly Downloads][downloads-badge]][npm] [![Bundled size][size-badge]][size] [![Typed Codebase][typescript]](#) [![MIT License][license]](#)

[version]: https://flat.badgen.net/npm/v/@remirror/extension-count
[npm]: https://npmjs.com/package/@remirror/extension-count
[license]: https://flat.badgen.net/badge/license/MIT/purple
[size]: https://bundlephobia.com/result?p=@remirror/extension-count
[size-badge]: https://flat.badgen.net/bundlephobia/minzip/@remirror/extension-count
[typescript]: https://flat.badgen.net/badge/icon/TypeScript?icon=typescript&label
[downloads-badge]: https://badgen.net/npm/dw/@remirror/extension-count/red?icon=npm

## Installation

```bash
# yarn
yarn add @remirror/extension-count

# pnpm
pnpm add @remirror/extension-count

# npm
npm install @remirror/extension-count
```

## Usage

The following code creates an instance of this extension.

```ts
import { CountExtension } from '@remirror/extension-count';

const extension = new CountExtension();
```
193 changes: 193 additions & 0 deletions packages/remirror__extension-count/src/count-extension.ts
@@ -0,0 +1,193 @@
import type {
CreateExtensionPlugin,
EditorState,
Helper,
Static,
Transaction,
} from '@remirror/core';
import { extension, findMatches, helper, PlainExtension } from '@remirror/core';
import { Decoration, DecorationSet } from '@remirror/pm/view';

import {
getCharacterExceededPosition,
getTextLength,
getWordExceededPosition,
WORDS_REGEX,
} from './count-utils';

export enum CountStrategy {
CHARACTERS = 'CHARACTERS',
WORDS = 'WORDS',
}

export interface CountOptions {
/**
* An optional soft limit. Text that exceeds this limit will be highlighted.
*
* @default -1
*/
maximum?: Static<number>;

/**
* The classname to use when highlighting text that exceed the given maximum.
*
* @default 'remirror-max-count-exceeded'
*/
maximumExceededClassName?: Static<string>;

/**
* The counting strategy to use. Either CountStrategy.CHARACTERS or CountStrategy.WORDS
*
* @default CountStrategy.CHARACTERS
*/
maximumStrategy?: Static<CountStrategy>;
}

interface CountPluginState {
decorationSet: DecorationSet;
}

/**
* Count words or characters in your editor, and set a soft max length
*/
@extension<CountOptions>({
defaultOptions: {
maximum: -1,
maximumExceededClassName: 'remirror-max-count-exceeded',
maximumStrategy: CountStrategy.CHARACTERS,
},
staticKeys: ['maximum', 'maximumStrategy', 'maximumExceededClassName'],
})
export class CountExtension extends PlainExtension<CountOptions> {
get name() {
return 'count' as const;
}

/**
* Get the configured maximum characters/words.
*/
@helper()
getCountMaximum(): Helper<number> {
return this.options.maximum;
}

/**
* Get the count of characters in the document.
*
* @param state
*/
@helper()
getCharacterCount(state: EditorState = this.store.getState()): Helper<number> {
let count = 0;

state.doc.nodesBetween(0, state.doc.nodeSize - 2, (node) => {
count += getTextLength(node);
return true;
});

// Remove the last line break character
return Math.max(count - 1, 0);
}

/**
* Get the count of words in the document.
*
* @param state
*/
@helper()
getWordCount(state: EditorState = this.store.getState()): Helper<number> {
const text = this.store.helpers.getText({ lineBreakDivider: ' ', state });
return findMatches(text, WORDS_REGEX).length;
}

/**
* Is the current number of characters/words valid in the current strategy.
*
* @param state
*/
@helper()
isCountValid(state: EditorState = this.store.getState()): Helper<boolean> {
const { maximumStrategy, maximum } = this.options;

if (maximum < 1) {
return true;
}

if (maximumStrategy === CountStrategy.CHARACTERS) {
const count = this.store.helpers.getCharacterCount(state);
return count <= maximum;
}

return this.store.helpers.getWordCount(state) <= maximum;
}

protected createDecorationSet(state: EditorState): DecorationSet {
const { maximum = -1, maximumStrategy, maximumExceededClassName } = this.options;

const isCharacterCountStrategy = maximumStrategy === CountStrategy.CHARACTERS;
const posStrategy = isCharacterCountStrategy
? getCharacterExceededPosition
: getWordExceededPosition;

const pos = posStrategy(state, maximum);

return DecorationSet.create(state.doc, [
Decoration.inline(pos, state.doc.nodeSize - 2, {
class: maximumExceededClassName,
}),
]);
}

createPlugin(): CreateExtensionPlugin<CountPluginState> {
const { maximum } = this.options;

return {
state: {
init: (_, state: EditorState) => {
if (this.isCountValid(state)) {
return {
decorationSet: DecorationSet.empty,
};
}

return {
decorationSet: this.createDecorationSet(state),
};
},
apply: (
tr: Transaction,
pluginState: CountPluginState,
_: EditorState,
state: EditorState,
) => {
if (!tr.docChanged || maximum < 1) {
return pluginState;
}

if (this.isCountValid(state)) {
return {
decorationSet: DecorationSet.empty,
};
}

return {
decorationSet: this.createDecorationSet(state),
};
},
},
props: {
decorations(state: EditorState) {
return this.getState(state).decorationSet;
},
},
};
}
}

declare global {
namespace Remirror {
interface AllExtensions {
count: CountExtension;
}
}
}

0 comments on commit 2873f02

Please sign in to comment.