@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/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
+ })
@@ -0,0 +1,4 @@
1
+ declare module '*.feature?raw' {
2
+ const content: string
3
+ export default content
4
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './input-rule'
2
+ export * from './plugin.input-rule'
3
+ export * from './text-transform-rule'
@@ -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
+ }