@portabletext/editor 2.13.2 → 2.13.3

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.
@@ -1,5 +1,5 @@
1
1
  import { Behavior, Editor, EditorEmittedEvent, EditorSchema } from "../_chunks-dts/behavior.types.action.js";
2
- import * as react21 from "react";
2
+ import * as react12 from "react";
3
3
  import React from "react";
4
4
  /**
5
5
  * @beta
@@ -181,7 +181,7 @@ type MarkdownPluginConfig = MarkdownBehaviorsConfig & {
181
181
  */
182
182
  declare function MarkdownPlugin(props: {
183
183
  config: MarkdownPluginConfig;
184
- }): react21.JSX.Element;
184
+ }): react12.JSX.Element;
185
185
  /**
186
186
  * @beta
187
187
  * Restrict the editor to one line. The plugin takes care of blocking
@@ -192,5 +192,5 @@ declare function MarkdownPlugin(props: {
192
192
  *
193
193
  * @deprecated Install the plugin from `@portabletext/plugin-one-line`
194
194
  */
195
- declare function OneLinePlugin(): react21.JSX.Element;
195
+ declare function OneLinePlugin(): react12.JSX.Element;
196
196
  export { BehaviorPlugin, DecoratorShortcutPlugin, EditorRefPlugin, EventListenerPlugin, MarkdownPlugin, type MarkdownPluginConfig, OneLinePlugin };
@@ -127,6 +127,9 @@ declare function getListIndex({
127
127
  }: {
128
128
  path: BlockPath;
129
129
  }): EditorSelector<number | undefined>;
130
+ /**
131
+ * @beta
132
+ */
130
133
  type MarkState = {
131
134
  state: 'unchanged';
132
135
  marks: Array<string>;
@@ -319,4 +322,4 @@ declare const isSelectionCollapsed: EditorSelector<boolean>;
319
322
  * @public
320
323
  */
321
324
  declare const isSelectionExpanded: EditorSelector<boolean>;
322
- export { getActiveAnnotations, getActiveListItem, getActiveStyle, getAnchorBlock, getAnchorChild, getAnchorSpan, getAnchorTextBlock, getBlockOffsets, getBlockTextBefore, getCaretWordSelection, getFirstBlock, getFocusBlock, getFocusBlockObject, getFocusChild, getFocusInlineObject, getFocusListBlock, getFocusSpan, getFocusTextBlock, getLastBlock, getListIndex, getMarkState, getNextBlock, getNextInlineObject, getPreviousBlock, getPreviousInlineObject, getSelectedBlocks, getSelectedSlice, getSelectedSpans, getSelectedTextBlocks, getSelectedValue, getSelection, getSelectionEndBlock, getSelectionEndChild, getSelectionEndPoint, getSelectionStartBlock, getSelectionStartChild, getSelectionStartPoint, getSelectionText, getTrimmedSelection, getValue, isActiveAnnotation, isActiveDecorator, isActiveListItem, isActiveStyle, isAtTheEndOfBlock, isAtTheStartOfBlock, isOverlappingSelection, isPointAfterSelection, isPointBeforeSelection, isSelectingEntireBlocks, isSelectionCollapsed, isSelectionExpanded };
325
+ export { MarkState, getActiveAnnotations, getActiveListItem, getActiveStyle, getAnchorBlock, getAnchorChild, getAnchorSpan, getAnchorTextBlock, getBlockOffsets, getBlockTextBefore, getCaretWordSelection, getFirstBlock, getFocusBlock, getFocusBlockObject, getFocusChild, getFocusInlineObject, getFocusListBlock, getFocusSpan, getFocusTextBlock, getLastBlock, getListIndex, getMarkState, getNextBlock, getNextInlineObject, getPreviousBlock, getPreviousInlineObject, getSelectedBlocks, getSelectedSlice, getSelectedSpans, getSelectedTextBlocks, getSelectedValue, getSelection, getSelectionEndBlock, getSelectionEndChild, getSelectionEndPoint, getSelectionStartBlock, getSelectionStartChild, getSelectionStartPoint, getSelectionText, getTrimmedSelection, getValue, isActiveAnnotation, isActiveDecorator, isActiveListItem, isActiveStyle, isAtTheEndOfBlock, isAtTheStartOfBlock, isOverlappingSelection, isPointAfterSelection, isPointBeforeSelection, isSelectingEntireBlocks, isSelectionCollapsed, isSelectionExpanded };
@@ -127,6 +127,9 @@ declare function getListIndex({
127
127
  }: {
128
128
  path: BlockPath;
129
129
  }): EditorSelector<number | undefined>;
130
+ /**
131
+ * @beta
132
+ */
130
133
  type MarkState = {
131
134
  state: 'unchanged';
132
135
  marks: Array<string>;
@@ -319,4 +322,4 @@ declare const isSelectionCollapsed: EditorSelector<boolean>;
319
322
  * @public
320
323
  */
321
324
  declare const isSelectionExpanded: EditorSelector<boolean>;
322
- export { getActiveAnnotations, getActiveListItem, getActiveStyle, getAnchorBlock, getAnchorChild, getAnchorSpan, getAnchorTextBlock, getBlockOffsets, getBlockTextBefore, getCaretWordSelection, getFirstBlock, getFocusBlock, getFocusBlockObject, getFocusChild, getFocusInlineObject, getFocusListBlock, getFocusSpan, getFocusTextBlock, getLastBlock, getListIndex, getMarkState, getNextBlock, getNextInlineObject, getPreviousBlock, getPreviousInlineObject, getSelectedBlocks, getSelectedSlice, getSelectedSpans, getSelectedTextBlocks, getSelectedValue, getSelection, getSelectionEndBlock, getSelectionEndChild, getSelectionEndPoint, getSelectionStartBlock, getSelectionStartChild, getSelectionStartPoint, getSelectionText, getTrimmedSelection, getValue, isActiveAnnotation, isActiveDecorator, isActiveListItem, isActiveStyle, isAtTheEndOfBlock, isAtTheStartOfBlock, isOverlappingSelection, isPointAfterSelection, isPointBeforeSelection, isSelectingEntireBlocks, isSelectionCollapsed, isSelectionExpanded };
325
+ export { MarkState, getActiveAnnotations, getActiveListItem, getActiveStyle, getAnchorBlock, getAnchorChild, getAnchorSpan, getAnchorTextBlock, getBlockOffsets, getBlockTextBefore, getCaretWordSelection, getFirstBlock, getFocusBlock, getFocusBlockObject, getFocusChild, getFocusInlineObject, getFocusListBlock, getFocusSpan, getFocusTextBlock, getLastBlock, getListIndex, getMarkState, getNextBlock, getNextInlineObject, getPreviousBlock, getPreviousInlineObject, getSelectedBlocks, getSelectedSlice, getSelectedSpans, getSelectedTextBlocks, getSelectedValue, getSelection, getSelectionEndBlock, getSelectionEndChild, getSelectionEndPoint, getSelectionStartBlock, getSelectionStartChild, getSelectionStartPoint, getSelectionText, getTrimmedSelection, getValue, isActiveAnnotation, isActiveDecorator, isActiveListItem, isActiveStyle, isAtTheEndOfBlock, isAtTheStartOfBlock, isOverlappingSelection, isPointAfterSelection, isPointBeforeSelection, isSelectingEntireBlocks, isSelectionCollapsed, isSelectionExpanded };
@@ -1,5 +1,5 @@
1
1
  import { BlockOffset, BlockPath, ChildPath, EditorContext, EditorSelection, EditorSelectionPoint } from "../_chunks-dts/behavior.types.action.cjs";
2
- import * as _sanity_types8 from "@sanity/types";
2
+ import * as _sanity_types9 from "@sanity/types";
3
3
  import { KeyedSegment, PortableTextBlock, PortableTextTextBlock } from "@sanity/types";
4
4
  import { isSpan, isTextBlock } from "@portabletext/schema";
5
5
  /**
@@ -143,7 +143,7 @@ declare function mergeTextBlocks({
143
143
  context: Pick<EditorContext, 'keyGenerator' | 'schema'>;
144
144
  targetBlock: PortableTextTextBlock;
145
145
  incomingBlock: PortableTextTextBlock;
146
- }): PortableTextTextBlock<_sanity_types8.PortableTextObject | _sanity_types8.PortableTextSpan>;
146
+ }): PortableTextTextBlock<_sanity_types9.PortableTextObject | _sanity_types9.PortableTextSpan>;
147
147
  /**
148
148
  * @public
149
149
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portabletext/editor",
3
- "version": "2.13.2",
3
+ "version": "2.13.3",
4
4
  "description": "Portable Text Editor made in React",
5
5
  "keywords": [
6
6
  "sanity",
@@ -85,16 +85,16 @@
85
85
  "slate-dom": "^0.118.1",
86
86
  "slate-react": "0.117.4",
87
87
  "xstate": "^5.22.0",
88
- "@portabletext/block-tools": "^3.5.7",
89
- "@portabletext/keyboard-shortcuts": "^1.1.1",
88
+ "@portabletext/block-tools": "^3.5.8",
89
+ "@portabletext/patches": "^1.1.8",
90
90
  "@portabletext/schema": "^1.2.0",
91
- "@portabletext/patches": "^1.1.8"
91
+ "@portabletext/keyboard-shortcuts": "^1.1.1"
92
92
  },
93
93
  "devDependencies": {
94
94
  "@sanity/diff-match-patch": "^3.2.0",
95
95
  "@sanity/pkg-utils": "^8.1.14",
96
- "@sanity/schema": "^4.9.0",
97
- "@sanity/types": "^4.9.0",
96
+ "@sanity/schema": "^4.10.1",
97
+ "@sanity/types": "^4.10.1",
98
98
  "@types/debug": "^4.1.12",
99
99
  "@types/lodash": "^4.17.20",
100
100
  "@types/lodash.startcase": "^4.4.9",
@@ -116,14 +116,14 @@
116
116
  "vite": "^7.1.7",
117
117
  "vitest": "^3.2.4",
118
118
  "vitest-browser-react": "^1.0.1",
119
- "@portabletext/sanity-bridge": "1.1.11",
119
+ "@portabletext/sanity-bridge": "1.1.12",
120
120
  "@portabletext/test": "^0.0.0",
121
121
  "racejar": "1.3.0"
122
122
  },
123
123
  "peerDependencies": {
124
- "@portabletext/sanity-bridge": "^1.1.11",
125
- "@sanity/schema": "^4.9.0",
126
- "@sanity/types": "^4.9.0",
124
+ "@portabletext/sanity-bridge": "^1.1.12",
125
+ "@sanity/schema": "^4.10.1",
126
+ "@sanity/types": "^4.10.1",
127
127
  "react": "^18.3 || ^19",
128
128
  "rxjs": "^7.8.2"
129
129
  },
@@ -51,6 +51,7 @@ import {usePortableTextEditor} from './hooks/usePortableTextEditor'
51
51
  import {createWithHotkeys} from './plugins/createWithHotKeys'
52
52
  import {rangeDecorationsMachine} from './range-decorations-machine'
53
53
  import {RelayActorContext} from './relay-actor-context'
54
+ import {validateSelectionMachine} from './validate-selection-machine'
54
55
 
55
56
  const debug = debugWithName('component:Editable')
56
57
 
@@ -142,6 +143,11 @@ export const PortableTextEditable = forwardRef<
142
143
  s.matches({'edit mode': 'read only'}),
143
144
  )
144
145
  const slateEditor = useSlate()
146
+ const validateSelectionActor = useActorRef(validateSelectionMachine, {
147
+ input: {
148
+ slateEditor,
149
+ },
150
+ })
145
151
 
146
152
  const rangeDecorationsActor = useActorRef(rangeDecorationsMachine, {
147
153
  input: {
@@ -936,21 +942,24 @@ export const PortableTextEditable = forwardRef<
936
942
  )
937
943
 
938
944
  const callbackRef = useCallback(
939
- (node: HTMLDivElement | null) => {
945
+ (editorElement: HTMLDivElement | null) => {
940
946
  if (typeof forwardedRef === 'function') {
941
- forwardedRef(node)
947
+ forwardedRef(editorElement)
942
948
  } else if (forwardedRef) {
943
- forwardedRef.current = node
949
+ forwardedRef.current = editorElement
944
950
  }
945
951
 
946
- if (node) {
952
+ if (editorElement) {
947
953
  // Observe mutations (child list and subtree) to this component's DOM,
948
954
  // and make sure the editor selection is valid when that happens.
949
955
  const mutationObserver = new MutationObserver(() => {
950
- validateSelection(slateEditor, node)
956
+ validateSelectionActor.send({
957
+ type: 'validate selection',
958
+ editorElement,
959
+ })
951
960
  })
952
961
 
953
- mutationObserver.observe(node, {
962
+ mutationObserver.observe(editorElement, {
954
963
  attributeOldValue: false,
955
964
  attributes: false,
956
965
  characterData: false,
@@ -963,7 +972,7 @@ export const PortableTextEditable = forwardRef<
963
972
  }
964
973
  }
965
974
  },
966
- [forwardedRef, slateEditor],
975
+ [forwardedRef, validateSelectionActor],
967
976
  )
968
977
 
969
978
  if (!portableTextEditor) {
@@ -1007,72 +1016,3 @@ export const PortableTextEditable = forwardRef<
1007
1016
  })
1008
1017
 
1009
1018
  PortableTextEditable.displayName = 'ForwardRef(PortableTextEditable)'
1010
-
1011
- // This function will handle unexpected DOM changes inside the Editable rendering,
1012
- // and make sure that we can maintain a stable slateEditor.selection when that happens.
1013
- //
1014
- // For example, if this Editable is rendered inside something that might re-render
1015
- // this component (hidden contexts) while the user is still actively changing the
1016
- // contentEditable, this could interfere with the intermediate DOM selection,
1017
- // which again could be picked up by ReactEditor's event listeners.
1018
- // If that range is invalid at that point, the slate.editorSelection could be
1019
- // set either wrong, or invalid, to which slateEditor will throw exceptions
1020
- // that are impossible to recover properly from or result in a wrong selection.
1021
- //
1022
- // Also the other way around, when the ReactEditor will try to create a DOM Range
1023
- // from the current slateEditor.selection, it may throw unrecoverable errors
1024
- // if the current editor.selection is invalid according to the DOM.
1025
- // If this is the case, default to selecting the top of the document, if the
1026
- // user already had a selection.
1027
- function validateSelection(slateEditor: Editor, activeElement: HTMLDivElement) {
1028
- if (!slateEditor.selection) {
1029
- return
1030
- }
1031
-
1032
- let root: Document | ShadowRoot | undefined
1033
-
1034
- try {
1035
- root = ReactEditor.findDocumentOrShadowRoot(slateEditor)
1036
- } catch {}
1037
-
1038
- if (!root) {
1039
- // The editor has most likely been unmounted
1040
- return
1041
- }
1042
-
1043
- // Return if the editor isn't the active element
1044
- if (activeElement !== root.activeElement) {
1045
- return
1046
- }
1047
- const window = ReactEditor.getWindow(slateEditor)
1048
- const domSelection = window.getSelection()
1049
- if (!domSelection || domSelection.rangeCount === 0) {
1050
- return
1051
- }
1052
- const existingDOMRange = domSelection.getRangeAt(0)
1053
- try {
1054
- const newDOMRange = ReactEditor.toDOMRange(
1055
- slateEditor,
1056
- slateEditor.selection,
1057
- )
1058
- if (
1059
- newDOMRange.startOffset !== existingDOMRange.startOffset ||
1060
- newDOMRange.endOffset !== existingDOMRange.endOffset
1061
- ) {
1062
- debug('DOM range out of sync, validating selection')
1063
- // Remove all ranges temporary
1064
- domSelection?.removeAllRanges()
1065
- // Set the correct range
1066
- domSelection.addRange(newDOMRange)
1067
- }
1068
- } catch {
1069
- debug(`Could not resolve selection, selecting top document`)
1070
- // Deselect the editor
1071
- Transforms.deselect(slateEditor)
1072
- // Select top document if there is a top block to select
1073
- if (slateEditor.children.length > 0) {
1074
- Transforms.select(slateEditor, [0, 0])
1075
- }
1076
- slateEditor.onChange()
1077
- }
1078
- }
@@ -0,0 +1,47 @@
1
+ import {getTersePt} from '@portabletext/test'
2
+ import {userEvent} from '@vitest/browser/context'
3
+ import {describe, expect, test, vi} from 'vitest'
4
+ import {getSelectionAfterText} from '../internal-utils/text-selection'
5
+ import {createTestEditor} from '../test/vitest'
6
+ import {validateSelectionMachine} from './validate-selection-machine'
7
+
8
+ describe(validateSelectionMachine.id, () => {
9
+ test('Scenario: Does not validate selection while Slate has pending operations', async () => {
10
+ const {editor, locator} = await createTestEditor()
11
+
12
+ await userEvent.click(locator)
13
+
14
+ editor.send({type: 'insert.text', text: 'foo'})
15
+
16
+ await vi.waitFor(() => {
17
+ expect(getTersePt(editor.getSnapshot().context)).toEqual(['foo'])
18
+ expect(editor.getSnapshot().context.selection).toEqual(
19
+ getSelectionAfterText(editor.getSnapshot().context, 'foo'),
20
+ )
21
+ })
22
+
23
+ // This event is being sent in before "foo" has been inserted in the DOM
24
+ // This means that when the MutationObserver is finally triggered for the
25
+ // "foo" insertion, "bar" will be in the Slate state but not in the DOM.
26
+ // This causes the selection to be out of sync and this is why we need to
27
+ // make sure the selection is not validated before Slate has committed all
28
+ // pending operations.
29
+ editor.send({type: 'insert.text', text: 'bar'})
30
+
31
+ await vi.waitFor(() => {
32
+ expect(getTersePt(editor.getSnapshot().context)).toEqual(['foobar'])
33
+ expect(editor.getSnapshot().context.selection).toEqual(
34
+ getSelectionAfterText(editor.getSnapshot().context, 'foobar'),
35
+ )
36
+ })
37
+
38
+ editor.send({type: 'delete.backward', unit: 'character'})
39
+
40
+ await vi.waitFor(() => {
41
+ expect(getTersePt(editor.getSnapshot().context)).toEqual(['fooba'])
42
+ expect(editor.getSnapshot().context.selection).toEqual(
43
+ getSelectionAfterText(editor.getSnapshot().context, 'fooba'),
44
+ )
45
+ })
46
+ })
47
+ })
@@ -0,0 +1,149 @@
1
+ import {Editor, Transforms} from 'slate'
2
+ import {ReactEditor} from 'slate-react'
3
+ import {setup} from 'xstate'
4
+ import {debugWithName} from '../internal-utils/debug'
5
+ import type {PortableTextSlateEditor} from '../types/editor'
6
+
7
+ const debug = debugWithName('validate selection machine')
8
+
9
+ const validateSelectionSetup = setup({
10
+ types: {
11
+ context: {} as {
12
+ slateEditor: PortableTextSlateEditor
13
+ },
14
+ input: {} as {
15
+ slateEditor: PortableTextSlateEditor
16
+ },
17
+ events: {} as {
18
+ type: 'validate selection'
19
+ editorElement: HTMLDivElement
20
+ },
21
+ },
22
+ guards: {
23
+ 'pending operations': ({context}) =>
24
+ context.slateEditor.operations.length > 0,
25
+ },
26
+ })
27
+
28
+ const validateSelectionAction = validateSelectionSetup.createAction(
29
+ ({context, event}) => {
30
+ validateSelection(context.slateEditor, event.editorElement)
31
+ },
32
+ )
33
+
34
+ export const validateSelectionMachine = validateSelectionSetup.createMachine({
35
+ id: 'validate selection',
36
+ context: ({input}) => ({
37
+ slateEditor: input.slateEditor,
38
+ }),
39
+ initial: 'idle',
40
+ states: {
41
+ idle: {
42
+ on: {
43
+ 'validate selection': [
44
+ {
45
+ guard: 'pending operations',
46
+ target: 'waiting',
47
+ },
48
+ {
49
+ actions: [validateSelectionAction],
50
+ target: 'idle',
51
+ },
52
+ ],
53
+ },
54
+ },
55
+ waiting: {
56
+ after: {
57
+ 0: [
58
+ {
59
+ guard: 'pending operations',
60
+ target: '.',
61
+ reenter: true,
62
+ },
63
+ {
64
+ target: 'idle',
65
+ actions: [validateSelectionAction],
66
+ },
67
+ ],
68
+ },
69
+ on: {
70
+ 'validate selection': {
71
+ target: '.',
72
+ reenter: true,
73
+ },
74
+ },
75
+ },
76
+ },
77
+ })
78
+
79
+ // This function will handle unexpected DOM changes inside the Editable rendering,
80
+ // and make sure that we can maintain a stable slateEditor.selection when that happens.
81
+ //
82
+ // For example, if this Editable is rendered inside something that might re-render
83
+ // this component (hidden contexts) while the user is still actively changing the
84
+ // contentEditable, this could interfere with the intermediate DOM selection,
85
+ // which again could be picked up by ReactEditor's event listeners.
86
+ // If that range is invalid at that point, the slate.editorSelection could be
87
+ // set either wrong, or invalid, to which slateEditor will throw exceptions
88
+ // that are impossible to recover properly from or result in a wrong selection.
89
+ //
90
+ // Also the other way around, when the ReactEditor will try to create a DOM Range
91
+ // from the current slateEditor.selection, it may throw unrecoverable errors
92
+ // if the current editor.selection is invalid according to the DOM.
93
+ // If this is the case, default to selecting the top of the document, if the
94
+ // user already had a selection.
95
+ function validateSelection(
96
+ slateEditor: PortableTextSlateEditor,
97
+ editorElement: HTMLDivElement,
98
+ ) {
99
+ if (!slateEditor.selection) {
100
+ return
101
+ }
102
+
103
+ let root: Document | ShadowRoot | undefined
104
+
105
+ try {
106
+ root = ReactEditor.findDocumentOrShadowRoot(slateEditor)
107
+ } catch {}
108
+
109
+ if (!root) {
110
+ // The editor has most likely been unmounted
111
+ return
112
+ }
113
+
114
+ // Return if the editor isn't the active element
115
+ if (editorElement !== root.activeElement) {
116
+ return
117
+ }
118
+ const window = ReactEditor.getWindow(slateEditor)
119
+ const domSelection = window.getSelection()
120
+ if (!domSelection || domSelection.rangeCount === 0) {
121
+ return
122
+ }
123
+ const existingDOMRange = domSelection.getRangeAt(0)
124
+ try {
125
+ const newDOMRange = ReactEditor.toDOMRange(
126
+ slateEditor,
127
+ slateEditor.selection,
128
+ )
129
+ if (
130
+ newDOMRange.startOffset !== existingDOMRange.startOffset ||
131
+ newDOMRange.endOffset !== existingDOMRange.endOffset
132
+ ) {
133
+ debug('DOM range out of sync, validating selection')
134
+ // Remove all ranges temporary
135
+ domSelection?.removeAllRanges()
136
+ // Set the correct range
137
+ domSelection.addRange(newDOMRange)
138
+ }
139
+ } catch {
140
+ debug(`Could not resolve selection, selecting top document`)
141
+ // Deselect the editor
142
+ Transforms.deselect(slateEditor)
143
+ // Select top document if there is a top block to select
144
+ if (slateEditor.children.length > 0) {
145
+ Transforms.select(slateEditor, Editor.start(slateEditor, []))
146
+ }
147
+ slateEditor.onChange()
148
+ }
149
+ }
@@ -17,7 +17,7 @@ export {getFocusSpan} from './selector.get-focus-span'
17
17
  export {getFocusTextBlock} from './selector.get-focus-text-block'
18
18
  export {getLastBlock} from './selector.get-last-block'
19
19
  export {getListIndex} from './selector.get-list-state'
20
- export {getMarkState} from './selector.get-mark-state'
20
+ export {getMarkState, type MarkState} from './selector.get-mark-state'
21
21
  export {getNextBlock} from './selector.get-next-block'
22
22
  export {getNextInlineObject} from './selector.get-next-inline-object'
23
23
  export {getPreviousBlock} from './selector.get-previous-block'
@@ -8,6 +8,9 @@ import {getNextSpan} from './selector.get-next-span'
8
8
  import {getPreviousSpan} from './selector.get-previous-span'
9
9
  import {getSelectedSpans} from './selector.get-selected-spans'
10
10
 
11
+ /**
12
+ * @beta
13
+ */
11
14
  export type MarkState =
12
15
  | {
13
16
  state: 'unchanged'
@@ -70,7 +70,7 @@ const parameterType = {
70
70
  }),
71
71
  tersePt: createParameterType<Array<string>>({
72
72
  name: 'terse-pt',
73
- matcher: /"([a-z-,#>:\\n \d|{}'"‘’“”?—]*)"/u,
73
+ matcher: /"([A-Za-z-,#>:\\n \d|{}()'"‘’“”?—.…→©]*)"/u,
74
74
  type: Array,
75
75
  transform: parseTersePtString,
76
76
  }),