@portabletext/plugin-input-rule 0.4.0 → 0.4.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portabletext/plugin-input-rule",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Easily configure input rules in the Portable Text Editor",
5
5
  "keywords": [
6
6
  "portabletext",
@@ -45,20 +45,22 @@
45
45
  "@sanity/pkg-utils": "^8.1.4",
46
46
  "@types/react": "^19.2.0",
47
47
  "@vitejs/plugin-react": "^5.0.4",
48
+ "@vitest/browser": "^4.0.4",
49
+ "@vitest/browser-playwright": "^4.0.4",
48
50
  "babel-plugin-react-compiler": "1.0.0",
49
51
  "eslint": "^9.38.0",
50
52
  "eslint-formatter-gha": "^1.6.0",
51
- "eslint-plugin-react-hooks": "7.0.0",
53
+ "eslint-plugin-react-hooks": "7.0.1",
52
54
  "react": "^19.2.0",
53
55
  "typescript": "5.9.3",
54
56
  "typescript-eslint": "^8.46.1",
55
- "vitest": "^3.2.4",
56
- "@portabletext/editor": "2.15.5",
57
- "racejar": "1.3.2",
58
- "@portabletext/schema": "1.2.0"
57
+ "vitest": "^4.0.4",
58
+ "@portabletext/editor": "2.17.0",
59
+ "@portabletext/schema": "1.2.0",
60
+ "racejar": "1.3.2"
59
61
  },
60
62
  "peerDependencies": {
61
- "@portabletext/editor": "^2.15.5",
63
+ "@portabletext/editor": "^2.17.0",
62
64
  "react": "^19.1.1"
63
65
  },
64
66
  "publishConfig": {
@@ -201,3 +201,8 @@ Feature: Edge Cases
201
201
  And "{ArrowLeft}" is pressed
202
202
  And undo is performed
203
203
  Then the text is "-"
204
+
205
+ Scenario Outline: Multiple overlapping matches in one insertion
206
+ Given the text ""
207
+ When "1*2*3" is inserted
208
+ Then the text is "1×2×3"
@@ -57,6 +57,11 @@ const unmatchedGroupsRule = defineTextTransformRule({
57
57
  transform: () => '<hr />',
58
58
  })
59
59
 
60
+ const multiplicationRule = defineTextTransformRule({
61
+ on: /\d+\s?([*x])\s?\d+/,
62
+ transform: () => '×',
63
+ })
64
+
60
65
  Feature({
61
66
  hooks: [
62
67
  Before(async (context: Context) => {
@@ -71,6 +76,7 @@ Feature({
71
76
  <InputRulePlugin rules={[betterH2Rule]} />
72
77
  <InputRulePlugin rules={[replaceAandCRule]} />
73
78
  <InputRulePlugin rules={[unmatchedGroupsRule]} />
79
+ <InputRulePlugin rules={[multiplicationRule]} />
74
80
  </>
75
81
  ),
76
82
  schemaDefinition: defineSchema({
@@ -0,0 +1,30 @@
1
+ Feature: Emoji Picker Rules
2
+
3
+ Background:
4
+ Given the editor is focused
5
+
6
+ Scenario: Trigger Rule
7
+ Given the text ""
8
+ When ":" is typed
9
+ Then the keyword is ""
10
+
11
+ Scenario: Partial Keyword Rule
12
+ Given the text ""
13
+ When ":jo" is typed
14
+ Then the keyword is "jo"
15
+
16
+ Scenario: Keyword Rule
17
+ Given the text ""
18
+ When ":joy:" is typed
19
+ Then the keyword is "joy"
20
+
21
+ Scenario Outline: Consecutive keywords
22
+ Given the text ":joy:"
23
+ When <text> is typed
24
+ Then the keyword is <keyword>
25
+
26
+ Examples:
27
+ | text | keyword |
28
+ | ":" | "" |
29
+ | ":cat" | "cat" |
30
+ | ":cat:" | "cat" |
@@ -0,0 +1,124 @@
1
+ import {useEditor} from '@portabletext/editor'
2
+ import {defineBehavior, effect, raise} from '@portabletext/editor/behaviors'
3
+ import {parameterTypes} from '@portabletext/editor/test'
4
+ import {
5
+ createTestEditor,
6
+ stepDefinitions,
7
+ type Context,
8
+ } from '@portabletext/editor/test/vitest'
9
+ import {defineSchema} from '@portabletext/schema'
10
+ import {Before, Then} from 'racejar'
11
+ import {Feature} from 'racejar/vitest'
12
+ import {useEffect, useState} from 'react'
13
+ import {expect, vi} from 'vitest'
14
+ import {page, type Locator} from 'vitest/browser'
15
+ import emojiPickerRulesFeature from './emoji-picker-rules.feature?raw'
16
+ import {defineInputRule} from './input-rule'
17
+ import {defineInputRuleBehavior} from './plugin.input-rule'
18
+
19
+ const triggerRule = defineInputRule({
20
+ on: /:/,
21
+ actions: [
22
+ () => [
23
+ raise({
24
+ type: 'custom.keyword found',
25
+ keyword: '',
26
+ }),
27
+ ],
28
+ ],
29
+ })
30
+ const partialKeywordRule = defineInputRule({
31
+ on: /:[a-zA-Z-_0-9]+/,
32
+ guard: ({event}) => {
33
+ const lastMatch = event.matches.at(-1)
34
+
35
+ if (!lastMatch) {
36
+ return false
37
+ }
38
+
39
+ return {keyword: lastMatch.text.slice(1)}
40
+ },
41
+ actions: [(_, {keyword}) => [raise({type: 'custom.keyword found', keyword})]],
42
+ })
43
+ const keywordRule = defineInputRule({
44
+ on: /:[a-zA-Z-_0-9]+:/,
45
+ guard: ({event}) => {
46
+ const lastMatch = event.matches.at(-1)
47
+
48
+ if (!lastMatch) {
49
+ return false
50
+ }
51
+
52
+ return {keyword: lastMatch.text.slice(1, -1)}
53
+ },
54
+ actions: [(_, {keyword}) => [raise({type: 'custom.keyword found', keyword})]],
55
+ })
56
+
57
+ function EmojiPickerRulesPlugin() {
58
+ const editor = useEditor()
59
+ const [keyword, setKeyword] = useState('')
60
+
61
+ useEffect(() => {
62
+ const unregisterBehaviors = [
63
+ editor.registerBehavior({
64
+ behavior: defineInputRuleBehavior({
65
+ rules: [keywordRule, partialKeywordRule, triggerRule],
66
+ }),
67
+ }),
68
+ editor.registerBehavior({
69
+ behavior: defineBehavior<{keyword: string}>({
70
+ on: 'custom.keyword found',
71
+ actions: [
72
+ ({event}) => [
73
+ effect(() => {
74
+ setKeyword(event.keyword)
75
+ }),
76
+ ],
77
+ ],
78
+ }),
79
+ }),
80
+ ]
81
+
82
+ return () => {
83
+ for (const unregister of unregisterBehaviors) {
84
+ unregister()
85
+ }
86
+ }
87
+ }, [])
88
+
89
+ return <div data-testid="keyword">{keyword}</div>
90
+ }
91
+
92
+ Feature({
93
+ hooks: [
94
+ Before(async (context: Context & {keywordLocator: Locator}) => {
95
+ const {editor, locator} = await createTestEditor({
96
+ children: <EmojiPickerRulesPlugin />,
97
+ schemaDefinition: defineSchema({
98
+ decorators: [{name: 'strong'}],
99
+ annotations: [{name: 'link'}],
100
+ inlineObjects: [{name: 'stock-ticker'}],
101
+ }),
102
+ })
103
+
104
+ context.locator = locator
105
+ context.editor = editor
106
+ context.keywordLocator = page.getByTestId('keyword')
107
+
108
+ await vi.waitFor(() =>
109
+ expect.element(context.keywordLocator).toBeInTheDocument(),
110
+ )
111
+ }),
112
+ ],
113
+ featureText: emojiPickerRulesFeature,
114
+ stepDefinitions: [
115
+ ...stepDefinitions,
116
+ Then(
117
+ 'the keyword is {string}',
118
+ (context: Context & {keywordLocator: Locator}, keyword: string) => {
119
+ expect(context.keywordLocator.element().textContent).toEqual(keyword)
120
+ },
121
+ ),
122
+ ],
123
+ parameterTypes,
124
+ })