@dxos/react-ui-editor 0.6.9 → 0.6.10-main.3cfcc89

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.
Files changed (75) hide show
  1. package/dist/lib/browser/index.mjs +759 -732
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/InputMode.stories.d.ts +2 -1
  5. package/dist/types/src/InputMode.stories.d.ts.map +1 -1
  6. package/dist/types/src/TextEditor.stories.d.ts +20 -13
  7. package/dist/types/src/TextEditor.stories.d.ts.map +1 -1
  8. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
  9. package/dist/types/src/defaults.d.ts +5 -1
  10. package/dist/types/src/defaults.d.ts.map +1 -1
  11. package/dist/types/src/extensions/autocomplete.d.ts +3 -2
  12. package/dist/types/src/extensions/autocomplete.d.ts.map +1 -1
  13. package/dist/types/src/extensions/automerge/automerge.stories.d.ts +2 -1
  14. package/dist/types/src/extensions/automerge/automerge.stories.d.ts.map +1 -1
  15. package/dist/types/src/extensions/comments.d.ts.map +1 -1
  16. package/dist/types/src/extensions/dnd.d.ts.map +1 -1
  17. package/dist/types/src/extensions/factories.d.ts +2 -2
  18. package/dist/types/src/extensions/factories.d.ts.map +1 -1
  19. package/dist/types/src/extensions/folding.d.ts.map +1 -1
  20. package/dist/types/src/extensions/markdown/action.d.ts +1 -1
  21. package/dist/types/src/extensions/markdown/action.d.ts.map +1 -1
  22. package/dist/types/src/extensions/markdown/bundle.d.ts.map +1 -1
  23. package/dist/types/src/extensions/markdown/changes.d.ts +10 -0
  24. package/dist/types/src/extensions/markdown/changes.d.ts.map +1 -0
  25. package/dist/types/src/extensions/markdown/changes.test.d.ts +2 -0
  26. package/dist/types/src/extensions/markdown/changes.test.d.ts.map +1 -0
  27. package/dist/types/src/extensions/markdown/debug.d.ts +11 -0
  28. package/dist/types/src/extensions/markdown/debug.d.ts.map +1 -0
  29. package/dist/types/src/extensions/markdown/decorate.d.ts.map +1 -1
  30. package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
  31. package/dist/types/src/extensions/markdown/index.d.ts +1 -0
  32. package/dist/types/src/extensions/markdown/index.d.ts.map +1 -1
  33. package/dist/types/src/extensions/markdown/styles.d.ts +4 -0
  34. package/dist/types/src/extensions/markdown/styles.d.ts.map +1 -0
  35. package/dist/types/src/index.d.ts +0 -1
  36. package/dist/types/src/index.d.ts.map +1 -1
  37. package/dist/types/src/styles/theme.d.ts +1 -1
  38. package/dist/types/src/styles/theme.d.ts.map +1 -1
  39. package/dist/types/src/styles/tokens.d.ts +2 -5
  40. package/dist/types/src/styles/tokens.d.ts.map +1 -1
  41. package/dist/types/src/translations.d.ts +1 -0
  42. package/dist/types/src/translations.d.ts.map +1 -1
  43. package/package.json +26 -27
  44. package/src/InputMode.stories.tsx +1 -1
  45. package/src/TextEditor.stories.tsx +125 -77
  46. package/src/components/Toolbar/Toolbar.tsx +91 -92
  47. package/src/defaults.ts +16 -11
  48. package/src/extensions/annotations.ts +2 -2
  49. package/src/extensions/autocomplete.ts +4 -1
  50. package/src/extensions/automerge/automerge.stories.tsx +1 -1
  51. package/src/extensions/awareness/awareness.ts +1 -1
  52. package/src/extensions/comments.ts +11 -45
  53. package/src/extensions/dnd.ts +3 -5
  54. package/src/extensions/factories.ts +8 -8
  55. package/src/extensions/folding.tsx +3 -4
  56. package/src/extensions/markdown/action.ts +1 -0
  57. package/src/extensions/markdown/bundle.ts +3 -1
  58. package/src/extensions/markdown/{link-paste.test.ts → changes.test.ts} +2 -2
  59. package/src/extensions/markdown/changes.ts +148 -0
  60. package/src/extensions/markdown/debug.ts +44 -0
  61. package/src/extensions/markdown/decorate.ts +35 -108
  62. package/src/extensions/markdown/formatting.ts +1 -2
  63. package/src/extensions/markdown/highlight.ts +2 -2
  64. package/src/extensions/markdown/index.ts +1 -0
  65. package/src/extensions/markdown/parser.test.ts +29 -0
  66. package/src/extensions/markdown/styles.ts +103 -0
  67. package/src/index.ts +0 -2
  68. package/src/styles/theme.ts +85 -147
  69. package/src/styles/tokens.ts +6 -6
  70. package/src/translations.ts +1 -0
  71. package/dist/types/src/extensions/markdown/link-paste.d.ts +0 -9
  72. package/dist/types/src/extensions/markdown/link-paste.d.ts.map +0 -1
  73. package/dist/types/src/extensions/markdown/link-paste.test.d.ts +0 -2
  74. package/dist/types/src/extensions/markdown/link-paste.test.d.ts.map +0 -1
  75. package/src/extensions/markdown/link-paste.ts +0 -107
@@ -9,6 +9,7 @@
9
9
  import {
10
10
  autocompletion,
11
11
  completionKeymap,
12
+ type CompletionSource,
12
13
  type Completion,
13
14
  type CompletionContext,
14
15
  type CompletionResult,
@@ -20,13 +21,14 @@ export type AutocompleteResult = Completion;
20
21
 
21
22
  export type AutocompleteOptions = {
22
23
  activateOnTyping?: boolean;
24
+ override?: CompletionSource[];
23
25
  onSearch?: (text: string) => Completion[];
24
26
  };
25
27
 
26
28
  /**
27
29
  * Autocomplete extension.
28
30
  */
29
- export const autocomplete = ({ activateOnTyping, onSearch }: AutocompleteOptions = {}) => {
31
+ export const autocomplete = ({ activateOnTyping, override, onSearch }: AutocompleteOptions = {}) => {
30
32
  const extentions = [
31
33
  // https://codemirror.net/docs/ref/#view.keymap
32
34
  // https://discuss.codemirror.net/t/how-can-i-replace-the-default-autocompletion-keymap-v6/3322
@@ -37,6 +39,7 @@ export const autocomplete = ({ activateOnTyping, onSearch }: AutocompleteOptions
37
39
  // https://codemirror.net/docs/ref/#autocomplete.autocompletion
38
40
  autocompletion({
39
41
  activateOnTyping,
42
+ override,
40
43
 
41
44
  // closeOnBlur: false,
42
45
  // defaultKeymap: false,
@@ -2,7 +2,7 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import '@dxosTheme';
5
+ import '@dxos-theme';
6
6
 
7
7
  import '@preact/signals-react';
8
8
  import React, { useEffect, useState } from 'react';
@@ -258,7 +258,7 @@ class RemoteCaretWidget extends WidgetType {
258
258
  }
259
259
  }
260
260
 
261
- const styles = EditorView.baseTheme({
261
+ const styles = EditorView.theme({
262
262
  '.cm-collab-selection': {},
263
263
  '.cm-collab-selectionLine': {
264
264
  padding: 0,
@@ -32,7 +32,6 @@ import { nonNullable } from '@dxos/util';
32
32
  import { Cursor } from './cursor';
33
33
  import { type Comment, type Range } from './types';
34
34
  import { overlap } from './util';
35
- import { getToken } from '../styles';
36
35
  import { callbackWrapper } from '../util';
37
36
 
38
37
  //
@@ -106,53 +105,20 @@ export const commentsState = StateField.define<CommentsState>({
106
105
  },
107
106
  });
108
107
 
109
- //
110
- // UX
111
- //
112
-
113
- const styles = EditorView.baseTheme({
108
+ /**
109
+ * NOTE: Matches search.
110
+ */
111
+ const styles = EditorView.theme({
114
112
  '.cm-comment, .cm-comment-current': {
113
+ margin: '0 -3px',
114
+ padding: '3px',
115
+ borderRadius: '3px',
116
+ backgroundColor: 'var(--dx-cmCommentSurface)',
117
+ color: 'var(--dx-cmComment)',
115
118
  cursor: 'pointer',
116
- borderWidth: '1px',
117
- borderStyle: 'solid',
118
- borderRadius: '2px',
119
- transition: 'background-color 0.1s ease',
120
- },
121
- // Light theme.
122
- '&light .cm-comment': {
123
- backgroundColor: getToken('extend.colors.yellow.50'),
124
- mixBlendMode: 'darken',
125
- borderColor: getToken('extend.colors.yellow.100'),
126
- },
127
- '&light .cm-comment:hover': { backgroundColor: getToken('extend.colors.yellow.100') },
128
- '&light .cm-comment-current': {
129
- backgroundColor: getToken('extend.colors.primary.100'),
130
- borderColor: getToken('extend.colors.primary.200'),
131
- },
132
- '&light .cm-comment-current:hover': {
133
- backgroundColor: getToken('extend.colors.primary.150'),
134
- borderColor: getToken('extend.colors.primary.250'),
135
- },
136
-
137
- // Dark theme.
138
- '&dark .cm-comment': {
139
- color: getToken('extend.colors.yellow.50'),
140
- backgroundColor: getToken('extend.colors.yellow.800'),
141
- borderColor: getToken('extend.colors.yellow.700'),
142
- mixBlendMode: 'plus-lighter',
143
- },
144
- '&dark .cm-comment:hover': {
145
- backgroundColor: getToken('extend.colors.yellow.700'),
146
- borderColor: getToken('extend.colors.yellow.650'),
147
- },
148
- '&dark .cm-comment-current': {
149
- color: getToken('extend.colors.primary.50'),
150
- backgroundColor: getToken('extend.colors.primary.800'),
151
- borderColor: getToken('extend.colors.primary.700'),
152
119
  },
153
- '&dark .cm-comment-current:hover': {
154
- backgroundColor: getToken('extend.colors.primary.700'),
155
- borderColor: getToken('extend.colors.primary.650'),
120
+ '.cm-comment:hover, .cm-comment-current': {
121
+ textDecoration: 'underline',
156
122
  },
157
123
  });
158
124
 
@@ -5,14 +5,12 @@
5
5
  import type { Extension } from '@codemirror/state';
6
6
  import { dropCursor, EditorView } from '@codemirror/view';
7
7
 
8
- import { getToken } from '../styles';
9
-
10
8
  export type DNDOptions = { onDrop?: (view: EditorView, event: { files: FileList }) => void };
11
9
 
12
- const styles = EditorView.baseTheme({
10
+ const styles = EditorView.theme({
13
11
  '.cm-dropCursor': {
14
- borderLeft: `2px solid ${getToken('extend.colors.primary.500')}`,
15
- color: getToken('extend.colors.primary.500'),
12
+ borderLeft: '2px solid var(--dx-accentText)',
13
+ color: 'var(--dx-accentText)',
16
14
  padding: '0 4px',
17
15
  },
18
16
  '.cm-dropCursor:after': {
@@ -3,7 +3,7 @@
3
3
  //
4
4
 
5
5
  import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
6
- import { defaultKeymap, history, historyKeymap, standardKeymap } from '@codemirror/commands';
6
+ import { defaultKeymap, history, historyKeymap, indentWithTab, standardKeymap } from '@codemirror/commands';
7
7
  import { bracketMatching } from '@codemirror/language';
8
8
  import { searchKeymap } from '@codemirror/search';
9
9
  import { EditorState, type Extension } from '@codemirror/state';
@@ -19,6 +19,7 @@ import {
19
19
  scrollPastEnd,
20
20
  } from '@codemirror/view';
21
21
  import defaultsDeep from 'lodash.defaultsdeep';
22
+ import merge from 'lodash.merge';
22
23
 
23
24
  import { generateName } from '@dxos/display-name';
24
25
  import { log } from '@dxos/log';
@@ -95,7 +96,7 @@ export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extensio
95
96
  props.bracketMatching && bracketMatching(),
96
97
  props.closeBrackets && closeBrackets(),
97
98
  props.dropCursor && dropCursor(),
98
- props.drawSelection && drawSelection(),
99
+ props.drawSelection && drawSelection({ cursorBlinkRate: 1_200 }),
99
100
  props.highlightActiveLine && highlightActiveLine(),
100
101
  props.history && history(),
101
102
  props.lineNumbers && lineNumbers(),
@@ -109,9 +110,9 @@ export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extensio
109
110
  keymap.of(
110
111
  [
111
112
  ...((props.keymap && keymaps[props.keymap]) ?? []),
112
- // NOTE: Tab configured by markdown extension.
113
+ // NOTE: Tabs are also configured by markdown extension.
113
114
  // https://codemirror.net/docs/ref/#commands.indentWithTab
114
- // ...(props.indentWithTab ? [indentWithTab] : []),
115
+ ...(props.indentWithTab ? [indentWithTab] : []),
115
116
  // https://codemirror.net/docs/ref/#autocomplete.closeBracketsKeymap
116
117
  ...(props.closeBrackets ? closeBracketsKeymap : []),
117
118
  // https://codemirror.net/docs/ref/#commands.historyKeymap
@@ -128,8 +129,8 @@ export const createBasicExtensions = (_props?: BasicExtensionsOptions): Extensio
128
129
  //
129
130
 
130
131
  export type ThemeExtensionsOptions = {
131
- theme?: ThemeStyles;
132
132
  themeMode?: ThemeMode;
133
+ styles?: ThemeStyles;
133
134
  slots?: {
134
135
  editor?: {
135
136
  className?: string;
@@ -148,12 +149,11 @@ const defaultThemeSlots = {
148
149
 
149
150
  // TODO(burdon): Should only have one baseTheme?
150
151
  // https://codemirror.net/examples/styling
151
- export const createThemeExtensions = ({ theme, themeMode, slots: _slots }: ThemeExtensionsOptions = {}): Extension => {
152
+ export const createThemeExtensions = ({ themeMode, styles, slots: _slots }: ThemeExtensionsOptions = {}): Extension => {
152
153
  const slots = defaultsDeep({}, _slots, defaultThemeSlots);
153
154
  return [
154
- EditorView.baseTheme(defaultTheme),
155
155
  EditorView.darkTheme.of(themeMode === 'dark'),
156
- theme && EditorView.theme(theme),
156
+ EditorView.baseTheme(styles ? merge({}, defaultTheme, styles) : defaultTheme),
157
157
  slots.editor?.className && EditorView.editorAttributes.of({ class: slots.editor.className }),
158
158
  slots.content?.className && EditorView.contentAttributes.of({ class: slots.content.className }),
159
159
  ].filter(isNotFalsy);
@@ -6,7 +6,8 @@ import { codeFolding, foldGutter } from '@codemirror/language';
6
6
  import { type Extension } from '@codemirror/state';
7
7
  import React from 'react';
8
8
 
9
- import { getSize, mx } from '@dxos/react-ui-theme';
9
+ import { Icon } from '@dxos/react-ui';
10
+ import { getSize } from '@dxos/react-ui-theme';
10
11
 
11
12
  import { renderRoot } from './util';
12
13
 
@@ -23,9 +24,7 @@ export const folding = (_props: FoldingOptions = {}): Extension => [
23
24
  markerDOM: (open) => {
24
25
  return renderRoot(
25
26
  document.createElement('div'),
26
- <svg className={mx(getSize(3), 'm-3 cursor-pointer', open && 'rotate-90')}>
27
- <use href={'/icons.svg#ph--caret-right--regular'} />
28
- </svg>,
27
+ <Icon icon='ph--caret-right--regular' classNames={[getSize(3), 'm-2 cursor-pointer', open && 'rotate-90']} />,
29
28
  );
30
29
  },
31
30
  }),
@@ -40,6 +40,7 @@ export type ActionType =
40
40
  | 'list-task'
41
41
  | 'mention'
42
42
  | 'prompt'
43
+ | 'search'
43
44
  | 'strikethrough'
44
45
  | 'table';
45
46
 
@@ -44,6 +44,9 @@ export const createMarkdownExtensions = ({ themeMode }: MarkdownBundleOptions =
44
44
  // Languages for syntax highlighting fenced code blocks.
45
45
  codeLanguages: languages,
46
46
 
47
+ // Don't complete HTML tags.
48
+ completeHTMLTags: false,
49
+
47
50
  // Parser extensions.
48
51
  extensions: [
49
52
  // GFM provided by default.
@@ -58,7 +61,6 @@ export const createMarkdownExtensions = ({ themeMode }: MarkdownBundleOptions =
58
61
  syntaxHighlighting(markdownHighlightStyle()),
59
62
 
60
63
  keymap.of([
61
- // TODO(burdon): Indent by 4 if in task list.
62
64
  // https://codemirror.net/docs/ref/#commands.indentWithTab
63
65
  indentWithTab,
64
66
 
@@ -6,7 +6,7 @@ import { expect } from 'chai';
6
6
 
7
7
  import { describe, test } from '@dxos/test';
8
8
 
9
- import { createLinkLabel } from './link-paste';
9
+ import { createLinkLabel } from './changes';
10
10
 
11
11
  const testCases = [
12
12
  { input: 'https://www.example.com', expected: 'example.com' },
@@ -19,7 +19,7 @@ const testCases = [
19
19
  { input: 'ftp://example.com', expected: 'ftp://example.com' },
20
20
  ];
21
21
 
22
- describe('links', () => {
22
+ describe('changes', () => {
23
23
  test('createLinkLabel', () => {
24
24
  testCases.forEach(({ input, expected }) => {
25
25
  expect(createLinkLabel(new URL(input))).to.eq(expected);
@@ -0,0 +1,148 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { syntaxTree } from '@codemirror/language';
6
+ import { type ChangeSpec, Transaction } from '@codemirror/state';
7
+ import { ViewPlugin, type ViewUpdate, type PluginValue } from '@codemirror/view';
8
+
9
+ /**
10
+ * Monitors and augments changes.
11
+ */
12
+ // TODO(burdon): Tests.
13
+ export const adjustChanges = () => {
14
+ return ViewPlugin.fromClass(
15
+ class implements PluginValue {
16
+ update(update: ViewUpdate) {
17
+ const tree = syntaxTree(update.state);
18
+ const adjustments: ChangeSpec[] = [];
19
+
20
+ for (const tr of update.transactions) {
21
+ const event = tr.annotation(Transaction.userEvent);
22
+ switch (event) {
23
+ //
24
+ // Enter
25
+ //
26
+ case 'input': {
27
+ const changes = tr.changes;
28
+ if (changes.empty) {
29
+ break;
30
+ }
31
+
32
+ changes.iterChanges((fromA) => {
33
+ const node = tree.resolveInner(fromA, 1);
34
+ if (node?.name === 'BulletList') {
35
+ // Add space to previous line if an empty list item (otherwise it is not interpreted as a Task).
36
+ const { text } = update.state.doc.lineAt(fromA);
37
+ if (text.endsWith(']')) {
38
+ adjustments.push({ from: fromA, to: fromA, insert: ' ' });
39
+ }
40
+ }
41
+ });
42
+
43
+ break;
44
+ }
45
+
46
+ //
47
+ // Paste
48
+ //
49
+ case 'input.paste': {
50
+ const changes = tr.changes;
51
+ if (changes.empty) {
52
+ break;
53
+ }
54
+
55
+ changes.iterChanges((fromA, toA, fromB, toB, text) => {
56
+ // Check for URL.
57
+ const url = getValidUrl(update.view.state.sliceDoc(fromB, toB));
58
+ if (url) {
59
+ const node = tree.resolveInner(fromA, -1);
60
+ const invalidPositions = new Set(['Link', 'LinkMark', 'Code', 'CodeText', 'FencedCode', 'URL']);
61
+ if (!invalidPositions.has(node?.name)) {
62
+ const replacedText = tr.startState.sliceDoc(fromA, toA);
63
+ adjustments.push({ from: fromA, to: toB, insert: createLink(url, replacedText) });
64
+ }
65
+ } else {
66
+ const node = tree.resolveInner(fromA, 1);
67
+ switch (node?.name) {
68
+ case 'Task': {
69
+ // Remove task marker if pasting into task list.
70
+ const str = text.toString();
71
+ const match = str.match(/\s*- \[[ xX]\]\s*(.+)/);
72
+ if (match) {
73
+ const [, replacement] = match;
74
+ adjustments.push({ from: fromA, to: toB, insert: replacement });
75
+ }
76
+ break;
77
+ }
78
+ }
79
+ }
80
+ });
81
+
82
+ break;
83
+ }
84
+ }
85
+ }
86
+
87
+ // TODO(burdon): Is this the right way to augment changes?
88
+ if (adjustments.length) {
89
+ setTimeout(() => {
90
+ update.view.dispatch(
91
+ update.view.state.update({
92
+ changes: adjustments,
93
+ }),
94
+ );
95
+ });
96
+ }
97
+ }
98
+ },
99
+ );
100
+ };
101
+
102
+ //
103
+ // Links
104
+ //
105
+
106
+ export const createLink = (url: URL, label: string): string => {
107
+ // Check if image.
108
+ // Example: https://dxos.network/dxos-logotype-blue.png
109
+ const { host, pathname } = url;
110
+ const [, extension] = pathname.split('.');
111
+ const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp'];
112
+ if (imageExtensions.includes(extension)) {
113
+ return `![${label || host}](${url})`;
114
+ }
115
+
116
+ if (!label) {
117
+ label = createLinkLabel(url);
118
+ }
119
+
120
+ return `[${label}](${url})`;
121
+ };
122
+
123
+ export const createLinkLabel = (url: URL): string => {
124
+ let { protocol, host, pathname } = url;
125
+ if (protocol === 'http:' || protocol === 'https:') {
126
+ protocol = '';
127
+ }
128
+
129
+ // NOTE(Zan): Consult: https://github.com/dxos/dxos/issues/7331 before changing this.
130
+ // Remove 'www.' if at the beginning of the URL
131
+ host = host.replace(/^www\./, '');
132
+
133
+ return [protocol, host].filter(Boolean).join('//') + (pathname !== '/' ? pathname : '');
134
+ };
135
+
136
+ const getValidUrl = (str: string): URL | undefined => {
137
+ const validProtocols = ['http:', 'https:', 'mailto:', 'tel:'];
138
+ try {
139
+ const url = new URL(str);
140
+ if (!validProtocols.includes(url.protocol)) {
141
+ return undefined;
142
+ }
143
+
144
+ return url;
145
+ } catch (_err) {
146
+ return undefined;
147
+ }
148
+ };
@@ -0,0 +1,44 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { syntaxTree } from '@codemirror/language';
6
+ import { type EditorState, StateField } from '@codemirror/state';
7
+ import { type TreeCursor } from '@lezer/common';
8
+
9
+ export const debugTree = (cb: (tree: DebugNode) => void) =>
10
+ StateField.define({
11
+ create: (state) => cb(convertTreeToJson(state)),
12
+ update: (value, tr) => cb(convertTreeToJson(tr.state)),
13
+ });
14
+
15
+ export type DebugNode = {
16
+ type: string;
17
+ from: number;
18
+ to: number;
19
+ text: string;
20
+ children: DebugNode[];
21
+ };
22
+
23
+ export const convertTreeToJson = (state: EditorState): DebugNode => {
24
+ const treeToJson = (cursor: TreeCursor): DebugNode => {
25
+ const node: DebugNode = {
26
+ type: cursor.type.name,
27
+ from: cursor.from,
28
+ to: cursor.to,
29
+ text: state.doc.slice(cursor.from, cursor.to).toString(),
30
+ children: [],
31
+ };
32
+
33
+ if (cursor.firstChild()) {
34
+ do {
35
+ node.children.push(treeToJson(cursor));
36
+ } while (cursor.nextSibling());
37
+ cursor.parent();
38
+ }
39
+
40
+ return node;
41
+ };
42
+
43
+ return treeToJson(syntaxTree(state).cursor());
44
+ };