testomatio-editor-blocks 0.3.0 → 0.4.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testomatio-editor-blocks",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
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",
@@ -29,7 +29,7 @@
29
29
  "build:package": "node scripts/build-package.mjs",
30
30
  "lint": "eslint .",
31
31
  "test": "vitest",
32
- "test:run": "vitest run",
32
+ "test:run": "vitest run --reporter=verbose",
33
33
  "preview": "vite preview"
34
34
  },
35
35
  "keywords": [
@@ -39,6 +39,10 @@
39
39
  "testcases",
40
40
  "test-automation"
41
41
  ],
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/testomatio/block-editor"
45
+ },
42
46
  "peerDependencies": {
43
47
  "@blocknote/core": "^0.31.3",
44
48
  "@blocknote/react": "^0.31.3",
@@ -64,5 +68,8 @@
64
68
  "typescript-eslint": "^8.45.0",
65
69
  "vite": "^7.1.7",
66
70
  "vitest": "^3.2.4"
71
+ },
72
+ "dependencies": {
73
+ "overtype": "^2.0.6"
67
74
  }
68
75
  }
package/src/App.tsx CHANGED
@@ -664,7 +664,7 @@ function App() {
664
664
  <h2>Autocomplete Steps</h2>
665
665
  </div>
666
666
  <p className="app__panel-text">
667
- Start typing in the Step Title field to filter this list instantly.
667
+ Markdown format supported
668
668
  </p>
669
669
  <ol className="app__step-list">
670
670
  {(DEMO_STEP_FIXTURES.data ?? []).map((step) => (
@@ -1,5 +1,7 @@
1
1
  const IMAGE_MARKDOWN_REGEX = /!\[([^\]]*)\]\(([^)]+)\)/g;
2
2
  const MARKDOWN_ESCAPE_REGEX = /([*_\\])/g;
3
+ const INLINE_SEGMENT_REGEX =
4
+ /(\*\*\*[^*]+\*\*\*|___[^_]+___|\*\*[^*]+\*\*|__[^_]+__|\*[^*]+\*|_[^_]+_|<u>[^<]+<\/u>)/;
3
5
 
4
6
  export function escapeHtml(text: string): string {
5
7
  return text
@@ -29,13 +31,19 @@ function parseInlineMarkdown(text: string): InlineSegment[] {
29
31
  }
30
32
 
31
33
  const normalized = text.replace(/\\([*_`~])/g, "\uE000$1");
32
- const rawSegments = normalized
33
- .split(/(\*\*[^*]+\*\*|__[^_]+__|\*[^*]+\*|_[^_]+_|<u>[^<]+<\/u>)/)
34
- .filter(Boolean);
34
+ const rawSegments = normalized.split(INLINE_SEGMENT_REGEX).filter(Boolean);
35
35
 
36
36
  return rawSegments.map((segment) => {
37
37
  const baseStyles = { bold: false, italic: false, underline: false };
38
38
 
39
+ if (/^\*\*\*(.+)\*\*\*$/.test(segment) || /^___(.+)___$/.test(segment)) {
40
+ const content = segment.slice(3, -3);
41
+ return {
42
+ text: restoreEscapes(content),
43
+ styles: { bold: true, italic: true, underline: false },
44
+ };
45
+ }
46
+
39
47
  if (/^\*\*(.+)\*\*$/.test(segment) || /^__(.+)__$/.test(segment)) {
40
48
  const content = segment.slice(2, -2);
41
49
  return {
@@ -200,10 +208,22 @@ export function htmlToMarkdown(html: string): string {
200
208
  }
201
209
 
202
210
  function cleanupEscapedFormatting(markdown: string): string {
203
- return markdown.replace(/\\([*_])([^]*?)\\\1/g, (match, marker, inner) => {
204
- if (inner.includes("\\")) {
205
- return match;
211
+ return markdown.replace(/(\\+)([*_]+)/g, (_match, slashes, markers) => {
212
+ if (markers.length === 0) {
213
+ return slashes + markers;
214
+ }
215
+ const shouldClean =
216
+ markers.length === 3 ||
217
+ markers.length === 2 ||
218
+ markers.length === 1;
219
+ if (!shouldClean) {
220
+ return slashes + markers;
206
221
  }
207
- return `${marker}${inner}${marker}`;
222
+ const hasPrintable = slashes.length % 2 === 0;
223
+ return hasPrintable ? markers : slashes + markers;
208
224
  });
209
225
  }
226
+
227
+ export const __markdownStringUtils = {
228
+ cleanupEscapedFormatting,
229
+ };
@@ -1,8 +1,105 @@
1
1
  import { createReactBlockSpec } from "@blocknote/react";
2
- import { useCallback } from "react";
3
- import { StepField } from "./stepField";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ import type { ChangeEvent } from "react";
4
4
  import { useSnippetAutocomplete, type SnippetSuggestion } from "../snippetAutocomplete";
5
- import type { StepSuggestion } from "../stepAutocomplete";
5
+
6
+ type SnippetDropdownProps = {
7
+ value: string;
8
+ placeholder: string;
9
+ suggestions: SnippetSuggestion[];
10
+ onSelect: (suggestion: SnippetSuggestion) => void;
11
+ };
12
+
13
+ function SnippetDropdown({ value, placeholder, suggestions, onSelect }: SnippetDropdownProps) {
14
+ const [isOpen, setIsOpen] = useState(false);
15
+ const [search, setSearch] = useState("");
16
+ const containerRef = useRef<HTMLDivElement>(null);
17
+ const searchRef = useRef<HTMLInputElement>(null);
18
+
19
+ useEffect(() => {
20
+ if (!isOpen) return;
21
+ const handleMouseDown = (event: MouseEvent) => {
22
+ if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
23
+ setIsOpen(false);
24
+ }
25
+ };
26
+ document.addEventListener("mousedown", handleMouseDown);
27
+ return () => document.removeEventListener("mousedown", handleMouseDown);
28
+ }, [isOpen]);
29
+
30
+ useEffect(() => {
31
+ if (isOpen) {
32
+ setSearch("");
33
+ requestAnimationFrame(() => searchRef.current?.focus());
34
+ }
35
+ }, [isOpen]);
36
+
37
+ const filtered = useMemo(() => {
38
+ const snippets = suggestions.filter((s) => s.isSnippet === true);
39
+ if (!search) return snippets;
40
+ const lower = search.toLowerCase();
41
+ return snippets.filter((s) => s.title.toLowerCase().includes(lower));
42
+ }, [suggestions, search]);
43
+
44
+ const handleSearchChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
45
+ setSearch(event.target.value);
46
+ }, []);
47
+
48
+ return (
49
+ <div className="bn-snippet-dropdown" ref={containerRef}>
50
+ <button
51
+ type="button"
52
+ className="bn-snippet-dropdown__trigger"
53
+ onClick={() => setIsOpen((prev) => !prev)}
54
+ >
55
+ <span className="bn-snippet-dropdown__text">
56
+ {value || placeholder}
57
+ </span>
58
+ <svg className="bn-snippet-dropdown__chevron" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
59
+ <path d="M4 6L8 10L12 6" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
60
+ </svg>
61
+ </button>
62
+ {isOpen && (
63
+ <div className="bn-snippet-dropdown__panel" role="listbox">
64
+ <div className="bn-snippet-dropdown__search">
65
+ <svg className="bn-snippet-dropdown__search-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
66
+ <path d="M15.5 14H14.71L14.43 13.73C15.41 12.59 16 11.11 16 9.5C16 5.91 13.09 3 9.5 3C5.91 3 3 5.91 3 9.5C3 13.09 5.91 16 9.5 16C11.11 16 12.59 15.41 13.73 14.43L14 14.71V15.5L19 20.49L20.49 19L15.5 14ZM9.5 14C7.01 14 5 11.99 5 9.5C5 7.01 7.01 5 9.5 5C11.99 5 14 7.01 14 9.5C14 11.99 11.99 14 9.5 14Z" fill="currentColor"/>
67
+ </svg>
68
+ <input
69
+ ref={searchRef}
70
+ type="text"
71
+ className="bn-snippet-dropdown__search-input"
72
+ placeholder="Search"
73
+ value={search}
74
+ onChange={handleSearchChange}
75
+ />
76
+ </div>
77
+ <div className="bn-snippet-dropdown__list">
78
+ {filtered.map((suggestion) => (
79
+ <button
80
+ type="button"
81
+ key={suggestion.id}
82
+ role="option"
83
+ className="bn-snippet-dropdown__item"
84
+ onMouseDown={(event) => {
85
+ event.preventDefault();
86
+ onSelect(suggestion);
87
+ setIsOpen(false);
88
+ }}
89
+ tabIndex={-1}
90
+ >
91
+ {suggestion.title}
92
+ </button>
93
+ ))}
94
+ {filtered.length === 0 && (
95
+ <div className="bn-snippet-dropdown__empty">No snippets found</div>
96
+ )}
97
+ </div>
98
+ </div>
99
+ )}
100
+ </div>
101
+ );
102
+ }
6
103
 
7
104
  export const snippetBlock = createReactBlockSpec(
8
105
  {
@@ -27,42 +124,14 @@ export const snippetBlock = createReactBlockSpec(
27
124
  render: ({ block, editor }) => {
28
125
  const snippetTitle = (block.props.snippetTitle as string) || "";
29
126
  const snippetData = (block.props.snippetData as string) || "";
127
+ const snippetId = (block.props.snippetId as string) || "";
30
128
  const snippetSuggestions = useSnippetAutocomplete();
31
129
  const hasSnippets = snippetSuggestions.length > 0;
32
-
33
- const handleSnippetChange = useCallback(
34
- (nextTitle: string) => {
35
- if (nextTitle === snippetTitle) {
36
- return;
37
- }
38
-
39
- editor.updateBlock(block.id, {
40
- props: {
41
- snippetTitle: nextTitle,
42
- },
43
- });
44
- },
45
- [block.id, editor, snippetTitle],
46
- );
47
-
48
- const handleSnippetDataChange = useCallback(
49
- (next: string) => {
50
- if (next === snippetData) {
51
- return;
52
- }
53
-
54
- editor.updateBlock(block.id, {
55
- props: {
56
- snippetData: next,
57
- },
58
- });
59
- },
60
- [editor, block.id, snippetData],
61
- );
130
+ const isSnippetSelected = snippetId.length > 0;
62
131
 
63
132
  const handleSnippetSelect = useCallback(
64
- (suggestion: SnippetSuggestion | StepSuggestion) => {
65
- const rawBody = (suggestion as SnippetSuggestion).body ?? "";
133
+ (suggestion: SnippetSuggestion) => {
134
+ const rawBody = suggestion.body ?? "";
66
135
  const sanitizedBody = rawBody
67
136
  .split(/\r?\n/)
68
137
  .filter((line) => !/^<!--\s*(begin|end)\s+snippet/i.test(line.trim()))
@@ -91,32 +160,19 @@ export const snippetBlock = createReactBlockSpec(
91
160
  }
92
161
 
93
162
  return (
94
- <div className="bn-teststep bn-snippet" data-block-id={block.id}>
95
- <StepField
96
- label="Snippet Title"
97
- value={snippetTitle}
98
- placeholder="Describe the reusable action"
99
- onChange={handleSnippetChange}
100
- autoFocus={snippetTitle.length === 0}
101
- enableAutocomplete
102
- suggestionFilter={(suggestion) => (suggestion as SnippetSuggestion).isSnippet === true}
103
- suggestionsOverride={snippetSuggestions as unknown as StepSuggestion[]}
104
- onSuggestionSelect={handleSnippetSelect}
105
- fieldName="snippet-title"
106
- showSuggestionsOnFocus
107
- enableImageUpload={false}
108
- onFieldFocus={handleFieldFocus}
109
- />
110
- <StepField
111
- label="Snippet Data"
112
- value={snippetData}
113
- placeholder="Add optional data or assets for the snippet"
114
- onChange={handleSnippetDataChange}
115
- multiline
116
- fieldName="snippet-data"
117
- enableImageUpload
118
- onFieldFocus={handleFieldFocus}
119
- />
163
+ <div className="bn-teststep bn-snippet" data-block-id={block.id} onFocus={handleFieldFocus}>
164
+ <div className="bn-snippet__header">
165
+ <span className="bn-snippet__label">Snippet</span>
166
+ <SnippetDropdown
167
+ value={snippetTitle}
168
+ placeholder="Select Snippet"
169
+ suggestions={snippetSuggestions}
170
+ onSelect={handleSnippetSelect}
171
+ />
172
+ </div>
173
+ {isSnippetSelected && (
174
+ <div className="bn-snippet__content">{snippetData}</div>
175
+ )}
120
176
  </div>
121
177
  );
122
178
  },