testomatio-editor-blocks 0.4.26 → 0.4.28

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.
@@ -14,6 +14,16 @@ export declare const isEmptyParagraph: (b: any) => boolean;
14
14
  export declare function canInsertStepOrSnippet(editor: {
15
15
  document: any[];
16
16
  }, referenceBlockId: string): boolean;
17
+ /**
18
+ * Programmatically add an empty step block to the editor.
19
+ * - If a "Steps" heading exists, inserts after the last step/snippet under it.
20
+ * - Otherwise, appends a "Steps" heading + empty step at the end.
21
+ * Returns the inserted step's block ID (for focusing), or null.
22
+ */
23
+ export declare function addStepsBlock(editor: {
24
+ document: any[];
25
+ insertBlocks: (blocks: any[], referenceId: string, placement: string) => any[];
26
+ }): string | null;
17
27
  export declare const stepBlock: {
18
28
  config: {
19
29
  readonly type: "testStep";
@@ -85,6 +85,60 @@ export function canInsertStepOrSnippet(editor, referenceBlockId) {
85
85
  }
86
86
  return false;
87
87
  }
88
+ /**
89
+ * Programmatically add an empty step block to the editor.
90
+ * - If a "Steps" heading exists, inserts after the last step/snippet under it.
91
+ * - Otherwise, appends a "Steps" heading + empty step at the end.
92
+ * Returns the inserted step's block ID (for focusing), or null.
93
+ */
94
+ export function addStepsBlock(editor) {
95
+ var _a, _b, _c, _d;
96
+ const allBlocks = editor.document;
97
+ const emptyStep = {
98
+ type: "testStep",
99
+ props: { stepTitle: "", stepData: "", expectedResult: "" },
100
+ children: [],
101
+ };
102
+ let stepsHeadingIndex = -1;
103
+ for (let i = 0; i < allBlocks.length; i++) {
104
+ const b = allBlocks[i];
105
+ if (b.type !== "heading")
106
+ continue;
107
+ const text = (Array.isArray(b.content) ? b.content : [])
108
+ .filter((n) => n.type === "text")
109
+ .map((n) => { var _a; return (_a = n.text) !== null && _a !== void 0 ? _a : ""; })
110
+ .join("")
111
+ .trim()
112
+ .toLowerCase()
113
+ .replace(/[:\-–—]$/, "");
114
+ if (isStepsHeading(text)) {
115
+ stepsHeadingIndex = i;
116
+ break;
117
+ }
118
+ }
119
+ if (stepsHeadingIndex >= 0) {
120
+ let lastIndex = stepsHeadingIndex;
121
+ for (let i = stepsHeadingIndex + 1; i < allBlocks.length; i++) {
122
+ const b = allBlocks[i];
123
+ if (b.type === "testStep" || b.type === "snippet" || isEmptyParagraph(b)) {
124
+ lastIndex = i;
125
+ continue;
126
+ }
127
+ break;
128
+ }
129
+ const inserted = editor.insertBlocks([emptyStep], allBlocks[lastIndex].id, "after");
130
+ return (_b = (_a = inserted === null || inserted === void 0 ? void 0 : inserted[0]) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : null;
131
+ }
132
+ const lastBlock = allBlocks[allBlocks.length - 1];
133
+ const stepsHeading = {
134
+ type: "heading",
135
+ props: { level: 3 },
136
+ content: [{ type: "text", text: "Steps" }],
137
+ children: [],
138
+ };
139
+ const inserted = editor.insertBlocks([stepsHeading, emptyStep], lastBlock.id, "after");
140
+ return (_d = (_c = inserted === null || inserted === void 0 ? void 0 : inserted[1]) === null || _c === void 0 ? void 0 : _c.id) !== null && _d !== void 0 ? _d : null;
141
+ }
88
142
  export const stepBlock = createReactBlockSpec({
89
143
  type: "testStep",
90
144
  content: "none",
@@ -0,0 +1,12 @@
1
+ import type { BlockNoteEditor } from "@blocknote/core";
2
+ import type { CustomPartialBlock } from "./customMarkdownConverter";
3
+ type PasteHandlerContext = {
4
+ event: ClipboardEvent;
5
+ editor: BlockNoteEditor<any, any, any>;
6
+ defaultPasteHandler: (context?: {
7
+ prioritizeMarkdownOverHTML?: boolean;
8
+ plainTextAsMarkdown?: boolean;
9
+ }) => boolean | undefined;
10
+ };
11
+ export declare function createMarkdownPasteHandler(converter: (markdown: string) => CustomPartialBlock[]): ({ event, editor, defaultPasteHandler }: PasteHandlerContext) => boolean | undefined;
12
+ export {};
@@ -0,0 +1,49 @@
1
+ export function createMarkdownPasteHandler(converter) {
2
+ return ({ event, editor, defaultPasteHandler }) => {
3
+ var _a, _b, _c, _d, _e, _f, _g, _h;
4
+ const types = (_b = (_a = event.clipboardData) === null || _a === void 0 ? void 0 : _a.types) !== null && _b !== void 0 ? _b : [];
5
+ if (types.includes("blocknote/html"))
6
+ return defaultPasteHandler();
7
+ if (types.includes("vscode-editor-data"))
8
+ return defaultPasteHandler();
9
+ if (types.includes("text/html")) {
10
+ const html = (_d = (_c = event.clipboardData) === null || _c === void 0 ? void 0 : _c.getData("text/html")) !== null && _d !== void 0 ? _d : "";
11
+ if (/<(pre|code)[\s>]/i.test(html))
12
+ return defaultPasteHandler();
13
+ }
14
+ const cursorBlock = editor.getTextCursorPosition().block;
15
+ if ((cursorBlock === null || cursorBlock === void 0 ? void 0 : cursorBlock.type) === "codeBlock" || (cursorBlock === null || cursorBlock === void 0 ? void 0 : cursorBlock.type) === "quote" || (cursorBlock === null || cursorBlock === void 0 ? void 0 : cursorBlock.type) === "table")
16
+ return defaultPasteHandler();
17
+ const plainText = (_f = (_e = event.clipboardData) === null || _e === void 0 ? void 0 : _e.getData("text/plain")) !== null && _f !== void 0 ? _f : "";
18
+ if (!plainText.trim())
19
+ return defaultPasteHandler();
20
+ try {
21
+ const parsedBlocks = converter(plainText);
22
+ if (parsedBlocks.length === 0)
23
+ return defaultPasteHandler();
24
+ const selection = editor.getSelection();
25
+ const selectedIds = (_h = (_g = selection === null || selection === void 0 ? void 0 : selection.blocks) === null || _g === void 0 ? void 0 : _g.map((block) => block.id).filter((id) => Boolean(id))) !== null && _h !== void 0 ? _h : [];
26
+ if (selectedIds.length > 0) {
27
+ editor.replaceBlocks(selectedIds, parsedBlocks);
28
+ }
29
+ else {
30
+ const cursorBlock = editor.getTextCursorPosition().block;
31
+ if (cursorBlock) {
32
+ editor.replaceBlocks([cursorBlock.id], parsedBlocks);
33
+ }
34
+ else if (editor.document.length > 0) {
35
+ const reference = editor.document[editor.document.length - 1];
36
+ editor.insertBlocks(parsedBlocks, reference.id, "after");
37
+ }
38
+ else {
39
+ return defaultPasteHandler();
40
+ }
41
+ }
42
+ editor.focus();
43
+ return true;
44
+ }
45
+ catch {
46
+ return defaultPasteHandler();
47
+ }
48
+ };
49
+ }
@@ -464,9 +464,8 @@ function serializeBlocks(blocks, ctx) {
464
464
  export function blocksToMarkdown(blocks) {
465
465
  const lines = serializeBlocks(blocks, { listDepth: 0, insideQuote: false });
466
466
  const cleaned = lines
467
- // Collapse excessive blank lines but preserve one extra for empty paragraphs.
468
467
  .join("\n")
469
- .replace(/\n{4,}/g, "\n\n\n")
468
+ .replace(/\n{3,}/g, "\n\n")
470
469
  .trimEnd();
471
470
  return cleaned;
472
471
  }
@@ -1,8 +1,9 @@
1
1
  export { customSchema, type CustomSchema, type CustomBlock, type CustomEditor, } from "./editor/customSchema";
2
- export { stepBlock, canInsertStepOrSnippet, isStepsHeading } from "./editor/blocks/step";
2
+ export { stepBlock, canInsertStepOrSnippet, isStepsHeading, addStepsBlock } from "./editor/blocks/step";
3
3
  export { snippetBlock } from "./editor/blocks/snippet";
4
4
  export { markdownToHtml, htmlToMarkdown } from "./editor/blocks/markdown";
5
5
  export { blocksToMarkdown, markdownToBlocks, type CustomEditorBlock, type CustomPartialBlock, } from "./editor/customMarkdownConverter";
6
6
  export { useStepAutocomplete, parseStepsFromJsonApi, setStepsFetcher, type StepSuggestion, type StepJsonApiDocument, type StepJsonApiResource, } from "./editor/stepAutocomplete";
7
7
  export { useStepImageUpload, setImageUploadHandler, type StepImageUploadHandler, } from "./editor/stepImageUpload";
8
+ export { createMarkdownPasteHandler } from "./editor/createMarkdownPasteHandler";
8
9
  export declare const testomatioEditorClassName = "markdown testomatio-editor";
package/package/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  export { customSchema, } from "./editor/customSchema";
2
- export { stepBlock, canInsertStepOrSnippet, isStepsHeading } from "./editor/blocks/step";
2
+ export { stepBlock, canInsertStepOrSnippet, isStepsHeading, addStepsBlock } from "./editor/blocks/step";
3
3
  export { snippetBlock } from "./editor/blocks/snippet";
4
4
  export { markdownToHtml, htmlToMarkdown } from "./editor/blocks/markdown";
5
5
  export { blocksToMarkdown, markdownToBlocks, } from "./editor/customMarkdownConverter";
6
6
  export { useStepAutocomplete, parseStepsFromJsonApi, setStepsFetcher, } from "./editor/stepAutocomplete";
7
7
  export { useStepImageUpload, setImageUploadHandler, } from "./editor/stepImageUpload";
8
+ export { createMarkdownPasteHandler } from "./editor/createMarkdownPasteHandler";
8
9
  export const testomatioEditorClassName = "markdown testomatio-editor";
@@ -25,7 +25,7 @@
25
25
  --color-accent-500: #10b981;
26
26
 
27
27
  /* Selection */
28
- --color-selection: #f4d35e;
28
+ --color-selection: rgba(0, 120, 215, 0.3);
29
29
 
30
30
  /* Semantic tokens - these reference the palette above */
31
31
  --text-primary: #262626;
@@ -1162,7 +1162,7 @@ html.dark .bn-step-image-preview__content {
1162
1162
  }
1163
1163
 
1164
1164
  .bn-step-field:focus-within .overtype-wrapper .overtype-input::selection {
1165
- background-color: rgba(244, 211, 94, 0.4) !important;
1165
+ background-color: var(--color-selection) !important;
1166
1166
  }
1167
1167
 
1168
1168
  /* Hide OverType's built-in link tooltip — we use our own */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testomatio-editor-blocks",
3
- "version": "0.4.26",
3
+ "version": "0.4.28",
4
4
  "description": "Custom BlockNote schema, markdown conversion helpers, and UI for Testomatio-style test cases and steps.",
5
5
  "type": "module",
6
6
  "main": "./package/index.js",
package/src/App.tsx CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  type CustomEditorBlock,
18
18
  type CustomPartialBlock,
19
19
  } from "./editor/customMarkdownConverter";
20
+ import { createMarkdownPasteHandler } from "./editor/createMarkdownPasteHandler";
20
21
  import { customSchema, type CustomEditor } from "./editor/customSchema";
21
22
  import { setStepsFetcher, type StepJsonApiDocument } from "./editor/stepAutocomplete";
22
23
  import { setSnippetFetcher, type SnippetJsonApiDocument } from "./editor/snippetAutocomplete";
@@ -347,46 +348,7 @@ function CustomSlashMenu() {
347
348
  function App() {
348
349
  const editor = useCreateBlockNote({
349
350
  schema: customSchema,
350
- pasteHandler: ({ event, editor, defaultPasteHandler }) => {
351
- const plainText = event.clipboardData?.getData("text/plain") ?? "";
352
-
353
- if (!plainText.trim()) {
354
- return defaultPasteHandler();
355
- }
356
-
357
- try {
358
- const parsedBlocks = markdownToBlocks(plainText);
359
-
360
- if (parsedBlocks.length === 0) {
361
- return defaultPasteHandler();
362
- }
363
-
364
- const selection = editor.getSelection();
365
- const selectedIds = selection?.blocks
366
- ?.map((block) => block.id)
367
- .filter((id): id is string => Boolean(id)) ?? [];
368
-
369
- if (selectedIds.length > 0) {
370
- editor.replaceBlocks(selectedIds, parsedBlocks);
371
- } else {
372
- const cursorBlock = editor.getTextCursorPosition().block;
373
- if (cursorBlock) {
374
- editor.replaceBlocks([cursorBlock.id], parsedBlocks);
375
- } else if (editor.document.length > 0) {
376
- const reference = editor.document[editor.document.length - 1];
377
- editor.insertBlocks(parsedBlocks, reference.id, "after");
378
- } else {
379
- return defaultPasteHandler();
380
- }
381
- }
382
-
383
- editor.focus();
384
- return true;
385
- } catch (error) {
386
- console.error("Failed to paste custom markdown", error);
387
- return defaultPasteHandler();
388
- }
389
- },
351
+ pasteHandler: createMarkdownPasteHandler(markdownToBlocks),
390
352
  });
391
353
  const [markdown, setMarkdown] = useState("");
392
354
  const [conversionError, setConversionError] = useState<string | null>(null);
@@ -96,6 +96,65 @@ export function canInsertStepOrSnippet(
96
96
  return false;
97
97
  }
98
98
 
99
+ /**
100
+ * Programmatically add an empty step block to the editor.
101
+ * - If a "Steps" heading exists, inserts after the last step/snippet under it.
102
+ * - Otherwise, appends a "Steps" heading + empty step at the end.
103
+ * Returns the inserted step's block ID (for focusing), or null.
104
+ */
105
+ export function addStepsBlock(editor: {
106
+ document: any[];
107
+ insertBlocks: (blocks: any[], referenceId: string, placement: string) => any[];
108
+ }): string | null {
109
+ const allBlocks = editor.document;
110
+ const emptyStep = {
111
+ type: "testStep" as const,
112
+ props: { stepTitle: "", stepData: "", expectedResult: "" },
113
+ children: [],
114
+ };
115
+
116
+ let stepsHeadingIndex = -1;
117
+ for (let i = 0; i < allBlocks.length; i++) {
118
+ const b = allBlocks[i];
119
+ if (b.type !== "heading") continue;
120
+ const text = (Array.isArray(b.content) ? b.content : [])
121
+ .filter((n: any) => n.type === "text")
122
+ .map((n: any) => n.text ?? "")
123
+ .join("")
124
+ .trim()
125
+ .toLowerCase()
126
+ .replace(/[:\-–—]$/, "");
127
+ if (isStepsHeading(text)) {
128
+ stepsHeadingIndex = i;
129
+ break;
130
+ }
131
+ }
132
+
133
+ if (stepsHeadingIndex >= 0) {
134
+ let lastIndex = stepsHeadingIndex;
135
+ for (let i = stepsHeadingIndex + 1; i < allBlocks.length; i++) {
136
+ const b = allBlocks[i];
137
+ if (b.type === "testStep" || b.type === "snippet" || isEmptyParagraph(b)) {
138
+ lastIndex = i;
139
+ continue;
140
+ }
141
+ break;
142
+ }
143
+ const inserted = editor.insertBlocks([emptyStep], allBlocks[lastIndex].id, "after");
144
+ return inserted?.[0]?.id ?? null;
145
+ }
146
+
147
+ const lastBlock = allBlocks[allBlocks.length - 1];
148
+ const stepsHeading = {
149
+ type: "heading" as const,
150
+ props: { level: 3 },
151
+ content: [{ type: "text" as const, text: "Steps" }],
152
+ children: [],
153
+ };
154
+ const inserted = editor.insertBlocks([stepsHeading, emptyStep], lastBlock.id, "after");
155
+ return inserted?.[1]?.id ?? null;
156
+ }
157
+
99
158
  export const stepBlock = createReactBlockSpec(
100
159
  {
101
160
  type: "testStep",
@@ -0,0 +1,62 @@
1
+ import type { BlockNoteEditor } from "@blocknote/core";
2
+ import type { CustomPartialBlock } from "./customMarkdownConverter";
3
+
4
+ type PasteHandlerContext = {
5
+ event: ClipboardEvent;
6
+ editor: BlockNoteEditor<any, any, any>;
7
+ defaultPasteHandler: (context?: {
8
+ prioritizeMarkdownOverHTML?: boolean;
9
+ plainTextAsMarkdown?: boolean;
10
+ }) => boolean | undefined;
11
+ };
12
+
13
+ export function createMarkdownPasteHandler(
14
+ converter: (markdown: string) => CustomPartialBlock[],
15
+ ) {
16
+ return ({ event, editor, defaultPasteHandler }: PasteHandlerContext): boolean | undefined => {
17
+ const types = event.clipboardData?.types ?? [];
18
+
19
+ if (types.includes("blocknote/html")) return defaultPasteHandler();
20
+ if (types.includes("vscode-editor-data")) return defaultPasteHandler();
21
+
22
+ if (types.includes("text/html")) {
23
+ const html = event.clipboardData?.getData("text/html") ?? "";
24
+ if (/<(pre|code)[\s>]/i.test(html)) return defaultPasteHandler();
25
+ }
26
+
27
+ const cursorBlock = editor.getTextCursorPosition().block;
28
+ if (cursorBlock?.type === "codeBlock" || cursorBlock?.type === "quote" || cursorBlock?.type === "table") return defaultPasteHandler();
29
+
30
+ const plainText = event.clipboardData?.getData("text/plain") ?? "";
31
+ if (!plainText.trim()) return defaultPasteHandler();
32
+
33
+ try {
34
+ const parsedBlocks = converter(plainText);
35
+ if (parsedBlocks.length === 0) return defaultPasteHandler();
36
+
37
+ const selection = editor.getSelection();
38
+ const selectedIds = selection?.blocks
39
+ ?.map((block: any) => block.id)
40
+ .filter((id: unknown): id is string => Boolean(id)) ?? [];
41
+
42
+ if (selectedIds.length > 0) {
43
+ editor.replaceBlocks(selectedIds, parsedBlocks);
44
+ } else {
45
+ const cursorBlock = editor.getTextCursorPosition().block;
46
+ if (cursorBlock) {
47
+ editor.replaceBlocks([cursorBlock.id], parsedBlocks);
48
+ } else if (editor.document.length > 0) {
49
+ const reference = editor.document[editor.document.length - 1];
50
+ editor.insertBlocks(parsedBlocks, reference.id, "after");
51
+ } else {
52
+ return defaultPasteHandler();
53
+ }
54
+ }
55
+
56
+ editor.focus();
57
+ return true;
58
+ } catch {
59
+ return defaultPasteHandler();
60
+ }
61
+ };
62
+ }
@@ -578,9 +578,8 @@ function serializeBlocks(blocks: CustomEditorBlock[], ctx: MarkdownContext): str
578
578
  export function blocksToMarkdown(blocks: CustomEditorBlock[]): string {
579
579
  const lines = serializeBlocks(blocks, { listDepth: 0, insideQuote: false });
580
580
  const cleaned = lines
581
- // Collapse excessive blank lines but preserve one extra for empty paragraphs.
582
581
  .join("\n")
583
- .replace(/\n{4,}/g, "\n\n\n")
582
+ .replace(/\n{3,}/g, "\n\n")
584
583
  .trimEnd();
585
584
 
586
585
  return cleaned;
@@ -25,7 +25,7 @@
25
25
  --color-accent-500: #10b981;
26
26
 
27
27
  /* Selection */
28
- --color-selection: #f4d35e;
28
+ --color-selection: rgba(0, 120, 215, 0.3);
29
29
 
30
30
  /* Semantic tokens - these reference the palette above */
31
31
  --text-primary: #262626;
@@ -1162,7 +1162,7 @@ html.dark .bn-step-image-preview__content {
1162
1162
  }
1163
1163
 
1164
1164
  .bn-step-field:focus-within .overtype-wrapper .overtype-input::selection {
1165
- background-color: rgba(244, 211, 94, 0.4) !important;
1165
+ background-color: var(--color-selection) !important;
1166
1166
  }
1167
1167
 
1168
1168
  /* Hide OverType's built-in link tooltip — we use our own */
package/src/index.ts CHANGED
@@ -4,7 +4,7 @@ export {
4
4
  type CustomBlock,
5
5
  type CustomEditor,
6
6
  } from "./editor/customSchema";
7
- export { stepBlock, canInsertStepOrSnippet, isStepsHeading } from "./editor/blocks/step";
7
+ export { stepBlock, canInsertStepOrSnippet, isStepsHeading, addStepsBlock } from "./editor/blocks/step";
8
8
  export { snippetBlock } from "./editor/blocks/snippet";
9
9
  export { markdownToHtml, htmlToMarkdown } from "./editor/blocks/markdown";
10
10
 
@@ -30,4 +30,6 @@ export {
30
30
  type StepImageUploadHandler,
31
31
  } from "./editor/stepImageUpload";
32
32
 
33
+ export { createMarkdownPasteHandler } from "./editor/createMarkdownPasteHandler";
34
+
33
35
  export const testomatioEditorClassName = "markdown testomatio-editor";