@portabletext/plugin-input-rule 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +5 -0
- package/dist/index.cjs +433 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +129 -0
- package/dist/index.d.ts +129 -0
- package/dist/index.js +439 -0
- package/dist/index.js.map +1 -0
- package/package.json +85 -0
- package/src/edge-cases.feature +81 -0
- package/src/edge-cases.test.tsx +59 -0
- package/src/global.d.ts +4 -0
- package/src/index.ts +3 -0
- package/src/input-rule.ts +82 -0
- package/src/plugin.input-rule.tsx +603 -0
- package/src/text-transform-rule.ts +94 -0
package/package.json
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@portabletext/plugin-input-rule",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Easily configure input rules in the Portable Text Editor",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"portabletext",
|
|
7
|
+
"plugin",
|
|
8
|
+
"input-rule"
|
|
9
|
+
],
|
|
10
|
+
"homepage": "https://www.portabletext.org/",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/portabletext/editor/issues"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/portabletext/editor.git",
|
|
17
|
+
"directory": "packages/plugin-input-rule"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"author": "Sanity.io <hello@sanity.io>",
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"type": "module",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"source": "./src/index.ts",
|
|
26
|
+
"import": "./dist/index.js",
|
|
27
|
+
"require": "./dist/index.cjs",
|
|
28
|
+
"default": "./dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./package.json": "./package.json"
|
|
31
|
+
},
|
|
32
|
+
"main": "./dist/index.cjs",
|
|
33
|
+
"module": "./dist/index.js",
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"src"
|
|
38
|
+
],
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@xstate/react": "^6.0.0",
|
|
41
|
+
"react-compiler-runtime": "19.1.0-rc.3",
|
|
42
|
+
"xstate": "^5.22.1"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@sanity/pkg-utils": "^8.1.4",
|
|
46
|
+
"@types/react": "^19.1.11",
|
|
47
|
+
"@vitejs/plugin-react": "^5.0.3",
|
|
48
|
+
"babel-plugin-react-compiler": "19.1.0-rc.3",
|
|
49
|
+
"eslint": "^9.34.0",
|
|
50
|
+
"eslint-formatter-gha": "^1.6.0",
|
|
51
|
+
"eslint-plugin-react-hooks": "6.0.0-rc.2",
|
|
52
|
+
"react": "^19.1.1",
|
|
53
|
+
"typescript": "5.9.3",
|
|
54
|
+
"typescript-eslint": "^8.41.0",
|
|
55
|
+
"vitest": "^3.2.4",
|
|
56
|
+
"@portabletext/editor": "2.13.5",
|
|
57
|
+
"racejar": "1.3.1",
|
|
58
|
+
"@portabletext/schema": "1.2.0"
|
|
59
|
+
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"@portabletext/editor": "^2.13.5",
|
|
62
|
+
"react": "^19.1.1"
|
|
63
|
+
},
|
|
64
|
+
"publishConfig": {
|
|
65
|
+
"access": "public"
|
|
66
|
+
},
|
|
67
|
+
"scripts": {
|
|
68
|
+
"build": "pkg-utils build --strict --check --clean",
|
|
69
|
+
"check:lint": "biome lint .",
|
|
70
|
+
"check:react-compiler": "eslint --cache .",
|
|
71
|
+
"check:types": "tsc",
|
|
72
|
+
"check:types:watch": "tsc --watch",
|
|
73
|
+
"clean": "del .turbo && del lib && del node_modules",
|
|
74
|
+
"dev": "pkg-utils watch",
|
|
75
|
+
"lint:fix": "biome lint --write .",
|
|
76
|
+
"test": "vitest --run",
|
|
77
|
+
"test:browser": "vitest --run --project browser",
|
|
78
|
+
"test:browser:chromium": "vitest run --project \"browser (chromium)\"",
|
|
79
|
+
"test:browser:chromium:watch": "vitest watch --project \"browser (chromium)\"",
|
|
80
|
+
"test:browser:firefox": "vitest run --project \"browser (firefox)\"",
|
|
81
|
+
"test:browser:firefox:watch": "vitest watch --project \"browser (firefox)\"",
|
|
82
|
+
"test:browser:webkit": "vitest run --project \"browser (webkit)\"",
|
|
83
|
+
"test:browser:webkit:watch": "vitest watch --project \"browser (webkit)\""
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Feature: Edge Cases
|
|
2
|
+
|
|
3
|
+
Background:
|
|
4
|
+
Given the editor is focused
|
|
5
|
+
And a global keymap
|
|
6
|
+
|
|
7
|
+
Scenario Outline: Longer Transform
|
|
8
|
+
Given the text <text>
|
|
9
|
+
When <inserted text> is inserted
|
|
10
|
+
And "new" is typed
|
|
11
|
+
Then the text is <new text>
|
|
12
|
+
|
|
13
|
+
Examples:
|
|
14
|
+
| text | inserted text | new text |
|
|
15
|
+
| "" | "." | "...new" |
|
|
16
|
+
| "foo" | "." | "foo...new" |
|
|
17
|
+
| "" | "foo." | "foo...new" |
|
|
18
|
+
| "foo." | "." | "foo....new" |
|
|
19
|
+
| "" | "foo.bar." | "foo...bar...new" |
|
|
20
|
+
| "foo.bar." | "baz." | "foo.bar.baz...new" |
|
|
21
|
+
|
|
22
|
+
Scenario Outline: End String Rule
|
|
23
|
+
Given the text <text>
|
|
24
|
+
When <inserted text> is inserted
|
|
25
|
+
And "new" is typed
|
|
26
|
+
Then the text is <new text>
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
| text | inserted text | new text |
|
|
30
|
+
| "-" | ">" | "→new" |
|
|
31
|
+
| "" | "->" | "→new" |
|
|
32
|
+
| "foo" | "->" | "foo→new" |
|
|
33
|
+
| "" | "foo->" | "foo→new" |
|
|
34
|
+
| "foo-" | ">bar" | "foo->barnew" |
|
|
35
|
+
| "" | "foo->bar->" | "foo->bar→new" |
|
|
36
|
+
| "foo->bar->" | "baz->" | "foo->bar->baz→new" |
|
|
37
|
+
|
|
38
|
+
Scenario Outline: Non-Global Rule
|
|
39
|
+
Given the text <text>
|
|
40
|
+
When <inserted text> is inserted
|
|
41
|
+
And "new" is typed
|
|
42
|
+
Then the text is <new text>
|
|
43
|
+
|
|
44
|
+
Examples:
|
|
45
|
+
| text | inserted text | new text |
|
|
46
|
+
| "(c" | ")" | "©new" |
|
|
47
|
+
| "" | "(c)" | "©new" |
|
|
48
|
+
| "foo" | "(c)" | "foo©new" |
|
|
49
|
+
| "" | "foo(c)" | "foo©new" |
|
|
50
|
+
| "foo(c" | ")bar" | "foo©barnew" |
|
|
51
|
+
| "" | "foo(c)bar(c)" | "foo©bar©new" |
|
|
52
|
+
| "foo(c)bar(c)" | "baz(c)" | "foo(c)bar(c)baz©new" |
|
|
53
|
+
|
|
54
|
+
Scenario Outline: Writing after Multiple Groups Rule
|
|
55
|
+
Given the text <text>
|
|
56
|
+
When <inserted text> is inserted
|
|
57
|
+
And "new" is typed
|
|
58
|
+
Then the text is <new text>
|
|
59
|
+
|
|
60
|
+
Examples:
|
|
61
|
+
| text | inserted text | new text |
|
|
62
|
+
| "" | "xfooy" | "zfooznew" |
|
|
63
|
+
| "xfoo" | "y" | "zfooznew" |
|
|
64
|
+
| "xfooy" | "z" | "xfooyznew" |
|
|
65
|
+
| "" | "xfyxoy" | "zfzzoznew" |
|
|
66
|
+
| "" | "xfyxoyxoy" | "zfzzozzoznew" |
|
|
67
|
+
|
|
68
|
+
Scenario Outline: Undoing Multiple Groups Rule
|
|
69
|
+
Given the text <text>
|
|
70
|
+
When <inserted text> is inserted
|
|
71
|
+
Then the text is <before undo>
|
|
72
|
+
When undo is performed
|
|
73
|
+
Then the text is <after undo>
|
|
74
|
+
|
|
75
|
+
Examples:
|
|
76
|
+
| text | inserted text | before undo | after undo |
|
|
77
|
+
| "" | "xfooy" | "zfooz" | "xfooy" |
|
|
78
|
+
| "xfoo" | "y" | "zfooz" | "xfooy" |
|
|
79
|
+
| "xfooy" | "z" | "xfooyz" | "xfooy" |
|
|
80
|
+
| "" | "xfyxoy" | "zfzzoz" | "xfyxoy" |
|
|
81
|
+
| "" | "xfyxoyxoy" | "zfzzozzoz" | "xfyxoyxoy" |
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {parameterTypes} from '@portabletext/editor/test'
|
|
2
|
+
import {
|
|
3
|
+
createTestEditor,
|
|
4
|
+
stepDefinitions,
|
|
5
|
+
type Context,
|
|
6
|
+
} from '@portabletext/editor/test/vitest'
|
|
7
|
+
import {defineSchema} from '@portabletext/schema'
|
|
8
|
+
import {Before} from 'racejar'
|
|
9
|
+
import {Feature} from 'racejar/vitest'
|
|
10
|
+
import edgeCasesFeature from './edge-cases.feature?raw'
|
|
11
|
+
import {InputRulePlugin} from './plugin.input-rule'
|
|
12
|
+
import {defineTextTransformRule} from './text-transform-rule'
|
|
13
|
+
|
|
14
|
+
const longerTransformRule = defineTextTransformRule({
|
|
15
|
+
on: /\./,
|
|
16
|
+
transform: () => '...',
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const endStringRule = defineTextTransformRule({
|
|
20
|
+
on: /->$/,
|
|
21
|
+
transform: () => '→',
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const nonGlobalRule = defineTextTransformRule({
|
|
25
|
+
on: /\(c\)/,
|
|
26
|
+
transform: () => '©',
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const multipleGroupsRule = defineTextTransformRule({
|
|
30
|
+
on: /(x)[fo]+(y)/,
|
|
31
|
+
transform: () => 'z',
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
Feature({
|
|
35
|
+
hooks: [
|
|
36
|
+
Before(async (context: Context) => {
|
|
37
|
+
const {editor, locator} = await createTestEditor({
|
|
38
|
+
children: (
|
|
39
|
+
<>
|
|
40
|
+
<InputRulePlugin rules={[longerTransformRule]} />
|
|
41
|
+
<InputRulePlugin rules={[endStringRule]} />
|
|
42
|
+
<InputRulePlugin rules={[nonGlobalRule]} />
|
|
43
|
+
<InputRulePlugin rules={[multipleGroupsRule]} />
|
|
44
|
+
</>
|
|
45
|
+
),
|
|
46
|
+
schemaDefinition: defineSchema({
|
|
47
|
+
decorators: [{name: 'strong'}],
|
|
48
|
+
annotations: [{name: 'link'}],
|
|
49
|
+
}),
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
context.locator = locator
|
|
53
|
+
context.editor = editor
|
|
54
|
+
}),
|
|
55
|
+
],
|
|
56
|
+
featureText: edgeCasesFeature,
|
|
57
|
+
stepDefinitions,
|
|
58
|
+
parameterTypes,
|
|
59
|
+
})
|
package/src/global.d.ts
ADDED
package/src/index.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BlockOffset,
|
|
3
|
+
BlockPath,
|
|
4
|
+
EditorSelection,
|
|
5
|
+
PortableTextTextBlock,
|
|
6
|
+
} from '@portabletext/editor'
|
|
7
|
+
import type {
|
|
8
|
+
BehaviorActionSet,
|
|
9
|
+
BehaviorGuard,
|
|
10
|
+
} from '@portabletext/editor/behaviors'
|
|
11
|
+
|
|
12
|
+
type InputRuleMatchLocation = {
|
|
13
|
+
/**
|
|
14
|
+
* Estimated selection of where in the original text the match is located.
|
|
15
|
+
* The selection is estimated since the match is found in the text after
|
|
16
|
+
* insertion.
|
|
17
|
+
*/
|
|
18
|
+
selection: NonNullable<EditorSelection>
|
|
19
|
+
/**
|
|
20
|
+
* Block offsets of the match in the text after the insertion
|
|
21
|
+
*/
|
|
22
|
+
targetOffsets: {
|
|
23
|
+
anchor: BlockOffset
|
|
24
|
+
focus: BlockOffset
|
|
25
|
+
backward: boolean
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Match found in the text after the insertion
|
|
31
|
+
* @alpha
|
|
32
|
+
*/
|
|
33
|
+
export type InputRuleMatch = InputRuleMatchLocation & {
|
|
34
|
+
groupMatches: Array<InputRuleMatchLocation>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @alpha
|
|
39
|
+
*/
|
|
40
|
+
export type InputRuleEvent = {
|
|
41
|
+
type: 'custom.input rule'
|
|
42
|
+
/**
|
|
43
|
+
* Matches found by the input rule
|
|
44
|
+
*/
|
|
45
|
+
matches: Array<InputRuleMatch>
|
|
46
|
+
/**
|
|
47
|
+
* The text before the insertion
|
|
48
|
+
*/
|
|
49
|
+
textBefore: string
|
|
50
|
+
/**
|
|
51
|
+
* The text is destined to be inserted
|
|
52
|
+
*/
|
|
53
|
+
textInserted: string
|
|
54
|
+
/**
|
|
55
|
+
* The text block where the insertion takes place
|
|
56
|
+
*/
|
|
57
|
+
focusTextBlock: {
|
|
58
|
+
path: BlockPath
|
|
59
|
+
node: PortableTextTextBlock
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @alpha
|
|
65
|
+
*/
|
|
66
|
+
export type InputRuleGuard = BehaviorGuard<InputRuleEvent, boolean>
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @alpha
|
|
70
|
+
*/
|
|
71
|
+
export type InputRule = {
|
|
72
|
+
on: RegExp
|
|
73
|
+
guard?: InputRuleGuard
|
|
74
|
+
actions: Array<BehaviorActionSet<InputRuleEvent, boolean>>
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @alpha
|
|
79
|
+
*/
|
|
80
|
+
export function defineInputRule(config: InputRule): InputRule {
|
|
81
|
+
return config
|
|
82
|
+
}
|