@blocknote/core 0.9.3 → 0.9.4

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 (52) hide show
  1. package/dist/blocknote.js +1623 -1318
  2. package/dist/blocknote.js.map +1 -1
  3. package/dist/blocknote.umd.cjs +5 -5
  4. package/dist/blocknote.umd.cjs.map +1 -1
  5. package/dist/style.css +1 -1
  6. package/package.json +2 -2
  7. package/src/BlockNoteEditor.ts +44 -12
  8. package/src/api/blockManipulation/__snapshots__/blockManipulation.test.ts.snap +21 -21
  9. package/src/api/blockManipulation/blockManipulation.test.ts +8 -11
  10. package/src/api/formatConversions/__snapshots__/formatConversions.test.ts.snap +3 -3
  11. package/src/api/formatConversions/formatConversions.test.ts +5 -5
  12. package/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap +3 -3
  13. package/src/api/nodeConversions/nodeConversions.test.ts +10 -4
  14. package/src/api/nodeConversions/nodeConversions.ts +9 -7
  15. package/src/api/nodeConversions/testUtil.ts +3 -3
  16. package/src/editor.module.css +1 -1
  17. package/src/extensions/BackgroundColor/BackgroundColorExtension.ts +5 -3
  18. package/src/extensions/BackgroundColor/BackgroundColorMark.ts +2 -1
  19. package/src/extensions/Blocks/NonEditableBlockPlugin.ts +17 -0
  20. package/src/extensions/Blocks/api/block.ts +29 -16
  21. package/src/extensions/Blocks/api/blockTypes.ts +79 -27
  22. package/src/extensions/Blocks/api/defaultBlocks.ts +13 -41
  23. package/src/extensions/Blocks/api/defaultProps.ts +16 -0
  24. package/src/extensions/Blocks/nodes/Block.module.css +78 -24
  25. package/src/extensions/Blocks/nodes/BlockContainer.ts +17 -41
  26. package/src/extensions/Blocks/nodes/BlockContent/HeadingBlockContent/HeadingBlockContent.ts +59 -13
  27. package/src/extensions/Blocks/nodes/BlockContent/ImageBlockContent/ImageBlockContent.ts +305 -0
  28. package/src/extensions/Blocks/nodes/BlockContent/ImageBlockContent/uploadToTmpFilesDotOrg_DEV_ONLY.ts +13 -0
  29. package/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.ts +24 -2
  30. package/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.ts +146 -120
  31. package/src/extensions/Blocks/nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent.ts +12 -2
  32. package/src/extensions/ImageToolbar/ImageToolbarPlugin.ts +239 -0
  33. package/src/extensions/SlashMenu/defaultSlashMenuItems.ts +47 -6
  34. package/src/extensions/TextColor/TextColorExtension.ts +4 -3
  35. package/src/extensions/TextColor/TextColorMark.ts +2 -1
  36. package/src/index.ts +4 -0
  37. package/types/src/BlockNoteEditor.d.ts +9 -0
  38. package/types/src/BlockNoteExtensions.d.ts +1 -1
  39. package/types/src/extensions/Blocks/api/block.d.ts +7 -8
  40. package/types/src/extensions/Blocks/api/blockTypes.d.ts +29 -20
  41. package/types/src/extensions/Blocks/api/defaultBlocks.d.ts +55 -51
  42. package/types/src/extensions/Blocks/api/defaultProps.d.ts +2 -2
  43. package/types/src/extensions/Blocks/nodes/BlockContent/HeadingBlockContent/HeadingBlockContent.d.ts +43 -9
  44. package/types/src/extensions/Blocks/nodes/BlockContent/ImageBlockContent/ImageBlockContent.d.ts +2 -2
  45. package/types/src/extensions/Blocks/nodes/BlockContent/ImageBlockContent/uploadToTmpFilesDotOrg_DEV_ONLY.d.ts +1 -0
  46. package/types/src/extensions/Blocks/nodes/BlockContent/ImageBlockContent/uploadToTmpFilesOrg_DEV_ONLY.d.ts +1 -0
  47. package/types/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent.d.ts +35 -9
  48. package/types/src/extensions/Blocks/nodes/BlockContent/ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent.d.ts +35 -9
  49. package/types/src/extensions/Blocks/nodes/BlockContent/ParagraphBlockContent/ParagraphBlockContent.d.ts +36 -1
  50. package/types/src/extensions/SlashMenu/defaultSlashMenuItems.d.ts +1 -1
  51. package/types/src/index.d.ts +4 -0
  52. package/types/src/shared/plugins/suggestion/SuggestionPlugin.d.ts +1 -1
@@ -1,21 +1,28 @@
1
1
  import { InputRule, mergeAttributes } from "@tiptap/core";
2
+ import { defaultProps } from "../../../api/defaultProps";
2
3
  import { createTipTapBlock } from "../../../api/block";
3
- import styles from "../../Block.module.css";
4
+ import { BlockSpec, PropSchema } from "../../../api/blockTypes";
4
5
  import { mergeCSSClasses } from "../../../../../shared/utils";
6
+ import styles from "../../Block.module.css";
7
+
8
+ export const headingPropSchema = {
9
+ ...defaultProps,
10
+ level: { default: 1, values: [1, 2, 3] as const },
11
+ } satisfies PropSchema;
5
12
 
6
- export const HeadingBlockContent = createTipTapBlock<"heading">({
13
+ const HeadingBlockContent = createTipTapBlock<"heading", true>({
7
14
  name: "heading",
8
15
  content: "inline*",
9
16
 
10
17
  addAttributes() {
11
18
  return {
12
19
  level: {
13
- default: "1",
20
+ default: 1,
14
21
  // instead of "level" attributes, use "data-level"
15
- parseHTML: (element) => element.getAttribute("data-level"),
22
+ parseHTML: (element) => element.getAttribute("data-level")!,
16
23
  renderHTML: (attributes) => {
17
24
  return {
18
- "data-level": attributes.level,
25
+ "data-level": (attributes.level as number).toString(),
19
26
  };
20
27
  },
21
28
  },
@@ -24,16 +31,18 @@ export const HeadingBlockContent = createTipTapBlock<"heading">({
24
31
 
25
32
  addInputRules() {
26
33
  return [
27
- ...["1", "2", "3"].map((level) => {
34
+ ...[1, 2, 3].map((level) => {
28
35
  // Creates a heading of appropriate level when starting with "#", "##", or "###".
29
36
  return new InputRule({
30
- find: new RegExp(`^(#{${parseInt(level)}})\\s$`),
37
+ find: new RegExp(`^(#{${level}})\\s$`),
31
38
  handler: ({ state, chain, range }) => {
32
39
  chain()
33
- .BNUpdateBlock(state.selection.from, {
40
+ .BNUpdateBlock<{
41
+ heading: BlockSpec<"heading", typeof headingPropSchema, true>;
42
+ }>(state.selection.from, {
34
43
  type: "heading",
35
44
  props: {
36
- level: level as "1" | "2" | "3",
45
+ level: level as 1 | 2 | 3,
37
46
  },
38
47
  })
39
48
  // Removes the "#" character(s) used to set the heading.
@@ -44,21 +53,53 @@ export const HeadingBlockContent = createTipTapBlock<"heading">({
44
53
  ];
45
54
  },
46
55
 
56
+ addKeyboardShortcuts() {
57
+ return {
58
+ "Mod-Alt-1": () =>
59
+ this.editor.commands.BNUpdateBlock<{
60
+ heading: BlockSpec<"heading", typeof headingPropSchema, true>;
61
+ }>(this.editor.state.selection.anchor, {
62
+ type: "heading",
63
+ props: {
64
+ level: 1,
65
+ },
66
+ }),
67
+ "Mod-Alt-2": () =>
68
+ this.editor.commands.BNUpdateBlock<{
69
+ heading: BlockSpec<"heading", typeof headingPropSchema, true>;
70
+ }>(this.editor.state.selection.anchor, {
71
+ type: "heading",
72
+ props: {
73
+ level: 2,
74
+ },
75
+ }),
76
+ "Mod-Alt-3": () =>
77
+ this.editor.commands.BNUpdateBlock<{
78
+ heading: BlockSpec<"heading", typeof headingPropSchema, true>;
79
+ }>(this.editor.state.selection.anchor, {
80
+ type: "heading",
81
+ props: {
82
+ level: 3,
83
+ },
84
+ }),
85
+ };
86
+ },
87
+
47
88
  parseHTML() {
48
89
  return [
49
90
  {
50
91
  tag: "h1",
51
- attrs: { level: "1" },
92
+ attrs: { level: 1 },
52
93
  node: "heading",
53
94
  },
54
95
  {
55
96
  tag: "h2",
56
- attrs: { level: "2" },
97
+ attrs: { level: 2 },
57
98
  node: "heading",
58
99
  },
59
100
  {
60
101
  tag: "h3",
61
- attrs: { level: "3" },
102
+ attrs: { level: 3 },
62
103
  node: "heading",
63
104
  },
64
105
  ];
@@ -81,7 +122,7 @@ export const HeadingBlockContent = createTipTapBlock<"heading">({
81
122
  "data-content-type": this.name,
82
123
  }),
83
124
  [
84
- "h" + node.attrs.level,
125
+ `h${node.attrs.level}`,
85
126
  {
86
127
  ...inlineContentDOMAttributes,
87
128
  class: mergeCSSClasses(
@@ -94,3 +135,8 @@ export const HeadingBlockContent = createTipTapBlock<"heading">({
94
135
  ];
95
136
  },
96
137
  });
138
+
139
+ export const Heading = {
140
+ node: HeadingBlockContent,
141
+ propSchema: headingPropSchema,
142
+ } satisfies BlockSpec<"heading", typeof headingPropSchema, true>;
@@ -0,0 +1,305 @@
1
+ import { createBlockSpec } from "../../../api/block";
2
+ import { defaultProps } from "../../../api/defaultProps";
3
+ import { BlockSpec, PropSchema, SpecificBlock } from "../../../api/blockTypes";
4
+ import { BlockNoteEditor } from "../../../../../BlockNoteEditor";
5
+ import { imageToolbarPluginKey } from "../../../../ImageToolbar/ImageToolbarPlugin";
6
+ import styles from "../../Block.module.css";
7
+
8
+ export const imagePropSchema = {
9
+ textAlignment: defaultProps.textAlignment,
10
+ backgroundColor: defaultProps.backgroundColor,
11
+ // Image url.
12
+ url: {
13
+ default: "" as const,
14
+ },
15
+ // Image caption.
16
+ caption: {
17
+ default: "" as const,
18
+ },
19
+ // Image width in px.
20
+ width: {
21
+ default: 512 as const,
22
+ },
23
+ } satisfies PropSchema;
24
+
25
+ // Converts text alignment prop values to the flexbox `align-items` values.
26
+ const textAlignmentToAlignItems = (
27
+ textAlignment: "left" | "center" | "right" | "justify"
28
+ ): "flex-start" | "center" | "flex-end" => {
29
+ switch (textAlignment) {
30
+ case "left":
31
+ return "flex-start";
32
+ case "center":
33
+ return "center";
34
+ case "right":
35
+ return "flex-end";
36
+ default:
37
+ return "flex-start";
38
+ }
39
+ };
40
+
41
+ // Min image width in px.
42
+ const minWidth = 64;
43
+
44
+ const renderImage = (
45
+ block: SpecificBlock<
46
+ { image: BlockSpec<"image", typeof imagePropSchema, false> },
47
+ "image"
48
+ >,
49
+ editor: BlockNoteEditor<{
50
+ image: BlockSpec<"image", typeof imagePropSchema, false>;
51
+ }>
52
+ ) => {
53
+ // Wrapper element to set the image alignment, contains both image/image
54
+ // upload dashboard and caption.
55
+ const wrapper = document.createElement("div");
56
+ wrapper.className = styles.wrapper;
57
+ wrapper.style.alignItems = textAlignmentToAlignItems(
58
+ block.props.textAlignment
59
+ );
60
+
61
+ // Button element that acts as a placeholder for images with no src.
62
+ const addImageButton = document.createElement("div");
63
+ addImageButton.className = styles.addImageButton;
64
+ addImageButton.style.display = block.props.url === "" ? "" : "none";
65
+
66
+ // Icon for the add image button.
67
+ const addImageButtonIcon = document.createElement("div");
68
+ addImageButtonIcon.className = styles.addImageButtonIcon;
69
+
70
+ // Text for the add image button.
71
+ const addImageButtonText = document.createElement("p");
72
+ addImageButtonText.className = styles.addImageButtonText;
73
+ addImageButtonText.innerText = "Add Image";
74
+
75
+ // Wrapper element for the image, resize handles and caption.
76
+ const imageAndCaptionWrapper = document.createElement("div");
77
+ imageAndCaptionWrapper.className = styles.imageAndCaptionWrapper;
78
+ imageAndCaptionWrapper.style.display = block.props.url !== "" ? "" : "none";
79
+
80
+ // Wrapper element for the image and resize handles.
81
+ const imageWrapper = document.createElement("div");
82
+ imageWrapper.className = styles.imageWrapper;
83
+ imageWrapper.style.display = block.props.url !== "" ? "" : "none";
84
+
85
+ // Image element.
86
+ const image = document.createElement("img");
87
+ image.className = styles.image;
88
+ image.src = block.props.url;
89
+ image.alt = "placeholder";
90
+ image.contentEditable = "false";
91
+ image.draggable = false;
92
+ image.style.width = `${Math.min(
93
+ block.props.width,
94
+ editor.domElement.firstElementChild!.clientWidth
95
+ )}px`;
96
+
97
+ // Resize handle elements.
98
+ const leftResizeHandle = document.createElement("div");
99
+ leftResizeHandle.className = styles.resizeHandle;
100
+ leftResizeHandle.style.left = "4px";
101
+ const rightResizeHandle = document.createElement("div");
102
+ rightResizeHandle.className = styles.resizeHandle;
103
+ rightResizeHandle.style.right = "4px";
104
+
105
+ // Caption element.
106
+ const caption = document.createElement("p");
107
+ caption.className = styles.caption;
108
+ caption.innerText = block.props.caption;
109
+ caption.style.padding = block.props.caption ? "4px" : "";
110
+
111
+ // Adds a light blue outline to selected image blocks.
112
+ const handleEditorUpdate = () => {
113
+ const selection = editor.getSelection()?.blocks || [];
114
+ const currentBlock = editor.getTextCursorPosition().block;
115
+
116
+ const isSelected =
117
+ [currentBlock, ...selection].find(
118
+ (selectedBlock) => selectedBlock.id === block.id
119
+ ) !== undefined;
120
+
121
+ if (isSelected) {
122
+ addImageButton.style.outline = "4px solid rgb(100, 160, 255)";
123
+ imageAndCaptionWrapper.style.outline = "4px solid rgb(100, 160, 255)";
124
+ } else {
125
+ addImageButton.style.outline = "";
126
+ imageAndCaptionWrapper.style.outline = "";
127
+ }
128
+ };
129
+ editor.onEditorContentChange(handleEditorUpdate);
130
+ editor.onEditorSelectionChange(handleEditorUpdate);
131
+
132
+ // Temporary parameters set when the user begins resizing the image, used to
133
+ // calculate the new width of the image.
134
+ let resizeParams:
135
+ | {
136
+ handleUsed: "left" | "right";
137
+ initialWidth: number;
138
+ initialClientX: number;
139
+ }
140
+ | undefined;
141
+
142
+ // Updates the image width with an updated width depending on the cursor X
143
+ // offset from when the resize began, and which resize handle is being used.
144
+ const windowMouseMoveHandler = (event: MouseEvent) => {
145
+ if (!resizeParams) {
146
+ return;
147
+ }
148
+
149
+ let newWidth: number;
150
+
151
+ if (textAlignmentToAlignItems(block.props.textAlignment) === "center") {
152
+ if (resizeParams.handleUsed === "left") {
153
+ newWidth =
154
+ resizeParams.initialWidth +
155
+ (resizeParams.initialClientX - event.clientX) * 2;
156
+ } else {
157
+ newWidth =
158
+ resizeParams.initialWidth +
159
+ (event.clientX - resizeParams.initialClientX) * 2;
160
+ }
161
+ } else {
162
+ if (resizeParams.handleUsed === "left") {
163
+ newWidth =
164
+ resizeParams.initialWidth +
165
+ resizeParams.initialClientX -
166
+ event.clientX;
167
+ } else {
168
+ newWidth =
169
+ resizeParams.initialWidth +
170
+ event.clientX -
171
+ resizeParams.initialClientX;
172
+ }
173
+ }
174
+
175
+ // Ensures the image is not wider than the editor and not smaller than a
176
+ // predetermined minimum width.
177
+ if (newWidth < minWidth) {
178
+ image.style.width = `${minWidth}px`;
179
+ } else if (newWidth > editor.domElement.firstElementChild!.clientWidth) {
180
+ image.style.width = `${
181
+ editor.domElement.firstElementChild!.clientWidth
182
+ }px`;
183
+ } else {
184
+ image.style.width = `${newWidth}px`;
185
+ }
186
+ };
187
+ // Stops mouse movements from resizing the image and updates the block's
188
+ // `width` prop to the new value.
189
+ const windowMouseUpHandler = (event: MouseEvent) => {
190
+ if (!resizeParams) {
191
+ return;
192
+ }
193
+
194
+ // Hides the drag handles if the cursor is no longer over the image.
195
+ if (
196
+ (!event.target || !imageWrapper.contains(event.target as Node)) &&
197
+ imageWrapper.contains(leftResizeHandle) &&
198
+ imageWrapper.contains(rightResizeHandle)
199
+ ) {
200
+ leftResizeHandle.style.display = "none";
201
+ rightResizeHandle.style.display = "none";
202
+ }
203
+
204
+ resizeParams = undefined;
205
+
206
+ editor.updateBlock(block, {
207
+ type: "image",
208
+ props: {
209
+ // Removes "px" from the end of the width string and converts to float.
210
+ width: parseFloat(image.style.width.slice(0, -2)),
211
+ },
212
+ });
213
+ };
214
+
215
+ // Prevents focus from moving to the button.
216
+ const addImageButtonMouseDownHandler = (event: MouseEvent) => {
217
+ event.preventDefault();
218
+ };
219
+ // Opens the image toolbar.
220
+ const addImageButtonClickHandler = () => {
221
+ editor._tiptapEditor.view.dispatch(
222
+ editor._tiptapEditor.state.tr.setMeta(imageToolbarPluginKey, {
223
+ block: block,
224
+ })
225
+ );
226
+ };
227
+
228
+ // Sets the resize params, allowing the user to begin resizing the image by
229
+ // moving the cursor left or right.
230
+ const leftResizeHandleMouseDownHandler = (event: MouseEvent) => {
231
+ event.preventDefault();
232
+
233
+ leftResizeHandle.style.display = "block";
234
+ rightResizeHandle.style.display = "block";
235
+
236
+ resizeParams = {
237
+ handleUsed: "left",
238
+ initialWidth: block.props.width,
239
+ initialClientX: event.clientX,
240
+ };
241
+ };
242
+ const rightResizeHandleMouseDownHandler = (event: MouseEvent) => {
243
+ event.preventDefault();
244
+
245
+ leftResizeHandle.style.display = "block";
246
+ rightResizeHandle.style.display = "block";
247
+
248
+ resizeParams = {
249
+ handleUsed: "right",
250
+ initialWidth: block.props.width,
251
+ initialClientX: event.clientX,
252
+ };
253
+ };
254
+
255
+ wrapper.appendChild(addImageButton);
256
+ addImageButton.appendChild(addImageButtonIcon);
257
+ addImageButton.appendChild(addImageButtonText);
258
+ wrapper.appendChild(imageAndCaptionWrapper);
259
+ imageAndCaptionWrapper.appendChild(imageWrapper);
260
+ imageWrapper.appendChild(image);
261
+ imageWrapper.appendChild(leftResizeHandle);
262
+ imageWrapper.appendChild(rightResizeHandle);
263
+ imageAndCaptionWrapper.appendChild(caption);
264
+
265
+ window.addEventListener("mousemove", windowMouseMoveHandler);
266
+ window.addEventListener("mouseup", windowMouseUpHandler);
267
+ addImageButton.addEventListener("mousedown", addImageButtonMouseDownHandler);
268
+ addImageButton.addEventListener("click", addImageButtonClickHandler);
269
+ leftResizeHandle.addEventListener(
270
+ "mousedown",
271
+ leftResizeHandleMouseDownHandler
272
+ );
273
+ rightResizeHandle.addEventListener(
274
+ "mousedown",
275
+ rightResizeHandleMouseDownHandler
276
+ );
277
+
278
+ return {
279
+ dom: wrapper,
280
+ destroy: () => {
281
+ window.removeEventListener("mousemove", windowMouseMoveHandler);
282
+ window.removeEventListener("mouseup", windowMouseUpHandler);
283
+ addImageButton.removeEventListener(
284
+ "mousedown",
285
+ addImageButtonMouseDownHandler
286
+ );
287
+ addImageButton.removeEventListener("click", addImageButtonClickHandler);
288
+ leftResizeHandle.removeEventListener(
289
+ "mousedown",
290
+ leftResizeHandleMouseDownHandler
291
+ );
292
+ rightResizeHandle.removeEventListener(
293
+ "mousedown",
294
+ rightResizeHandleMouseDownHandler
295
+ );
296
+ },
297
+ };
298
+ };
299
+
300
+ export const Image = createBlockSpec({
301
+ type: "image",
302
+ propSchema: imagePropSchema,
303
+ containsInlineContent: false,
304
+ render: renderImage,
305
+ });
@@ -0,0 +1,13 @@
1
+ export const uploadToTmpFilesDotOrg_DEV_ONLY = async (file: File) => {
2
+ const body = new FormData();
3
+ body.append("file", file);
4
+
5
+ const ret = await fetch("https://tmpfiles.org/api/v1/upload", {
6
+ method: "POST",
7
+ body: body,
8
+ });
9
+ return (await ret.json()).data.url.replace(
10
+ "tmpfiles.org/",
11
+ "tmpfiles.org/dl/"
12
+ );
13
+ };
@@ -1,10 +1,16 @@
1
1
  import { InputRule, mergeAttributes } from "@tiptap/core";
2
+ import { defaultProps } from "../../../../api/defaultProps";
2
3
  import { createTipTapBlock } from "../../../../api/block";
4
+ import { BlockSpec, PropSchema } from "../../../../api/blockTypes";
5
+ import { mergeCSSClasses } from "../../../../../../shared/utils";
3
6
  import { handleEnter } from "../ListItemKeyboardShortcuts";
4
7
  import styles from "../../../Block.module.css";
5
- import { mergeCSSClasses } from "../../../../../../shared/utils";
6
8
 
7
- export const BulletListItemBlockContent = createTipTapBlock<"bulletListItem">({
9
+ export const bulletListItemPropSchema = {
10
+ ...defaultProps,
11
+ } satisfies PropSchema;
12
+
13
+ const BulletListItemBlockContent = createTipTapBlock<"bulletListItem", true>({
8
14
  name: "bulletListItem",
9
15
  content: "inline*",
10
16
 
@@ -29,6 +35,17 @@ export const BulletListItemBlockContent = createTipTapBlock<"bulletListItem">({
29
35
  addKeyboardShortcuts() {
30
36
  return {
31
37
  Enter: () => handleEnter(this.editor),
38
+ "Mod-Shift-7": () =>
39
+ this.editor.commands.BNUpdateBlock<{
40
+ bulletListItem: BlockSpec<
41
+ "bulletListItem",
42
+ typeof bulletListItemPropSchema,
43
+ true
44
+ >;
45
+ }>(this.editor.state.selection.anchor, {
46
+ type: "bulletListItem",
47
+ props: {},
48
+ }),
32
49
  };
33
50
  },
34
51
 
@@ -112,3 +129,8 @@ export const BulletListItemBlockContent = createTipTapBlock<"bulletListItem">({
112
129
  ];
113
130
  },
114
131
  });
132
+
133
+ export const BulletListItem = {
134
+ node: BulletListItemBlockContent,
135
+ propSchema: bulletListItemPropSchema,
136
+ } satisfies BlockSpec<"bulletListItem", typeof bulletListItemPropSchema, true>;