testomatio-editor-blocks 0.1.1 → 0.2.0
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/README.md +13 -11
- package/package/editor/blocks/markdown.d.ts +5 -0
- package/package/editor/blocks/markdown.js +160 -0
- package/package/editor/blocks/snippet.d.ts +38 -0
- package/package/editor/blocks/snippet.js +65 -0
- package/package/editor/blocks/step.d.ts +32 -0
- package/package/editor/blocks/step.js +97 -0
- package/package/editor/blocks/stepField.d.ts +26 -0
- package/package/editor/blocks/stepField.js +316 -0
- package/package/editor/customMarkdownConverter.js +111 -80
- package/package/editor/customSchema.d.ts +31 -45
- package/package/editor/customSchema.js +6 -616
- package/package/editor/snippetAutocomplete.d.ts +28 -0
- package/package/editor/snippetAutocomplete.js +94 -0
- package/package/editor/stepAutocomplete.d.ts +1 -1
- package/package/editor/stepAutocomplete.js +15 -2
- package/package/editor/stepImageUpload.d.ts +1 -1
- package/package/editor/stepImageUpload.js +4 -9
- package/package/index.d.ts +2 -2
- package/package/index.js +2 -2
- package/package/styles.css +57 -0
- package/package.json +1 -1
- package/src/App.tsx +161 -45
- package/src/editor/blocks/blocks.test.ts +22 -0
- package/src/editor/blocks/markdown.ts +199 -0
- package/src/editor/blocks/snippet.tsx +109 -0
- package/src/editor/blocks/step.tsx +175 -0
- package/src/editor/blocks/stepField.tsx +487 -0
- package/src/editor/customMarkdownConverter.test.ts +121 -36
- package/src/editor/customMarkdownConverter.ts +128 -85
- package/src/editor/customSchema.tsx +6 -935
- package/src/editor/snippetAutocomplete.test.ts +54 -0
- package/src/editor/snippetAutocomplete.ts +133 -0
- package/src/editor/stepAutocomplete.test.ts +20 -0
- package/src/editor/stepAutocomplete.tsx +15 -2
- package/src/editor/stepImageUpload.test.ts +25 -0
- package/src/editor/stepImageUpload.ts +11 -0
- package/src/editor/styles.css +57 -0
- package/src/index.ts +2 -2
- package/src/editor/customSchema.test.ts +0 -47
- package/src/editor/stepImageUpload.tsx +0 -19
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useStepAutocomplete } from "../stepAutocomplete";
|
|
3
|
+
import { useStepImageUpload } from "../stepImageUpload";
|
|
4
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
5
|
+
import { escapeHtml, escapeMarkdownText, htmlToMarkdown, markdownToHtml, normalizePlainText, } from "./markdown";
|
|
6
|
+
export function StepField({ label, value, placeholder, onChange, autoFocus, multiline = false, enableAutocomplete = false, fieldName, suggestionFilter, suggestionsOverride, onSuggestionSelect, readOnly = false, showSuggestionsOnFocus = false, enableImageUpload = false, onImageFile, rightAction, showFormattingButtons = false, showImageButton = false, }) {
|
|
7
|
+
const editorRef = useRef(null);
|
|
8
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
9
|
+
const autoFocusRef = useRef(false);
|
|
10
|
+
const [plainTextValue, setPlainTextValue] = useState("");
|
|
11
|
+
const [activeSuggestionIndex, setActiveSuggestionIndex] = useState(0);
|
|
12
|
+
const [showAllSuggestions, setShowAllSuggestions] = useState(false);
|
|
13
|
+
const stepSuggestions = useStepAutocomplete();
|
|
14
|
+
const suggestions = suggestionsOverride !== null && suggestionsOverride !== void 0 ? suggestionsOverride : stepSuggestions;
|
|
15
|
+
const uploadImage = useStepImageUpload();
|
|
16
|
+
const fileInputRef = useRef(null);
|
|
17
|
+
const [isUploading, setIsUploading] = useState(false);
|
|
18
|
+
const normalizedQuery = normalizePlainText(plainTextValue);
|
|
19
|
+
const suggestionPool = useMemo(() => {
|
|
20
|
+
if (!suggestionFilter) {
|
|
21
|
+
return suggestions;
|
|
22
|
+
}
|
|
23
|
+
const filtered = suggestions.filter(suggestionFilter);
|
|
24
|
+
return filtered.length > 0 ? filtered : suggestions;
|
|
25
|
+
}, [suggestionFilter, suggestions]);
|
|
26
|
+
const filteredSuggestions = useMemo(() => {
|
|
27
|
+
if (!enableAutocomplete) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
const pool = showAllSuggestions || !normalizedQuery
|
|
31
|
+
? suggestionPool
|
|
32
|
+
: suggestionPool.filter((item) => normalizePlainText(item.title).startsWith(normalizedQuery));
|
|
33
|
+
return pool.slice(0, 8);
|
|
34
|
+
}, [enableAutocomplete, normalizedQuery, showAllSuggestions, suggestionPool]);
|
|
35
|
+
const hasExactMatch = filteredSuggestions.some((item) => normalizePlainText(item.title) === normalizedQuery);
|
|
36
|
+
const shouldShowAutocomplete = enableAutocomplete &&
|
|
37
|
+
isFocused &&
|
|
38
|
+
filteredSuggestions.length > 0 &&
|
|
39
|
+
(!hasExactMatch || showAllSuggestions) &&
|
|
40
|
+
(showAllSuggestions || normalizedQuery.length >= 1);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
setActiveSuggestionIndex(0);
|
|
43
|
+
}, [normalizedQuery, filteredSuggestions.length, showAllSuggestions]);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (normalizedQuery.length > 0) {
|
|
46
|
+
setShowAllSuggestions(false);
|
|
47
|
+
}
|
|
48
|
+
}, [normalizedQuery]);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
var _a;
|
|
51
|
+
const element = editorRef.current;
|
|
52
|
+
if (!element || isFocused) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (value.trim().length === 0) {
|
|
56
|
+
element.innerHTML = "";
|
|
57
|
+
setPlainTextValue("");
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
element.innerHTML = markdownToHtml(value);
|
|
61
|
+
setPlainTextValue((_a = element.textContent) !== null && _a !== void 0 ? _a : "");
|
|
62
|
+
}
|
|
63
|
+
}, [value, isFocused]);
|
|
64
|
+
const syncValue = useCallback(() => {
|
|
65
|
+
var _a;
|
|
66
|
+
const element = editorRef.current;
|
|
67
|
+
if (!element) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const markdown = htmlToMarkdown(element.innerHTML);
|
|
71
|
+
if (markdown !== value) {
|
|
72
|
+
onChange(markdown);
|
|
73
|
+
}
|
|
74
|
+
setPlainTextValue((_a = element.innerText) !== null && _a !== void 0 ? _a : "");
|
|
75
|
+
if (!markdown && element.innerHTML !== "") {
|
|
76
|
+
element.innerHTML = "";
|
|
77
|
+
}
|
|
78
|
+
}, [onChange, value]);
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (!autoFocus || autoFocusRef.current || !editorRef.current) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
autoFocusRef.current = true;
|
|
84
|
+
const element = editorRef.current;
|
|
85
|
+
const focusElement = () => {
|
|
86
|
+
var _a;
|
|
87
|
+
element.focus();
|
|
88
|
+
setIsFocused(true);
|
|
89
|
+
if (showSuggestionsOnFocus && enableAutocomplete) {
|
|
90
|
+
setShowAllSuggestions(true);
|
|
91
|
+
}
|
|
92
|
+
const selection = typeof window !== "undefined" ? (_a = window.getSelection) === null || _a === void 0 ? void 0 : _a.call(window) : null;
|
|
93
|
+
if (selection) {
|
|
94
|
+
selection.selectAllChildren(element);
|
|
95
|
+
selection.collapseToEnd();
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
if (typeof requestAnimationFrame === "function") {
|
|
99
|
+
const frame = requestAnimationFrame(focusElement);
|
|
100
|
+
return () => cancelAnimationFrame(frame);
|
|
101
|
+
}
|
|
102
|
+
const timeout = setTimeout(focusElement, 0);
|
|
103
|
+
return () => clearTimeout(timeout);
|
|
104
|
+
}, [autoFocus, enableAutocomplete, showSuggestionsOnFocus]);
|
|
105
|
+
const ensureCaretInEditor = useCallback(() => {
|
|
106
|
+
var _a;
|
|
107
|
+
const element = editorRef.current;
|
|
108
|
+
if (!element) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
const selection = (_a = window.getSelection) === null || _a === void 0 ? void 0 : _a.call(window);
|
|
112
|
+
if (!selection) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
if (selection.rangeCount === 0 || !element.contains(selection.anchorNode)) {
|
|
116
|
+
const range = document.createRange();
|
|
117
|
+
range.selectNodeContents(element);
|
|
118
|
+
range.collapse(false);
|
|
119
|
+
selection.removeAllRanges();
|
|
120
|
+
selection.addRange(range);
|
|
121
|
+
}
|
|
122
|
+
element.focus();
|
|
123
|
+
return true;
|
|
124
|
+
}, []);
|
|
125
|
+
const handlePaste = useCallback(async (event) => {
|
|
126
|
+
var _a, _b, _c, _d, _e;
|
|
127
|
+
if ((enableImageUpload && uploadImage) || onImageFile) {
|
|
128
|
+
const items = Array.from((_a = event.clipboardData.items) !== null && _a !== void 0 ? _a : []);
|
|
129
|
+
const imageItem = items.find((item) => item.kind === "file" && item.type.startsWith("image/"));
|
|
130
|
+
const file = imageItem === null || imageItem === void 0 ? void 0 : imageItem.getAsFile();
|
|
131
|
+
if (file) {
|
|
132
|
+
event.preventDefault();
|
|
133
|
+
if (onImageFile) {
|
|
134
|
+
await onImageFile(file);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (enableImageUpload && uploadImage) {
|
|
138
|
+
try {
|
|
139
|
+
const result = await uploadImage(file);
|
|
140
|
+
if (result === null || result === void 0 ? void 0 : result.url) {
|
|
141
|
+
ensureCaretInEditor();
|
|
142
|
+
const needsBreak = ((_c = (_b = editorRef.current) === null || _b === void 0 ? void 0 : _b.innerHTML) !== null && _c !== void 0 ? _c : "").trim().length > 0;
|
|
143
|
+
const imgHtml = (needsBreak ? "<br />" : "") +
|
|
144
|
+
`<img src="${escapeHtml(result.url)}" alt="" class="bn-inline-image" contenteditable="false" draggable="false" />`;
|
|
145
|
+
document.execCommand("insertHTML", false, imgHtml);
|
|
146
|
+
syncValue();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
console.error("Failed to upload image from paste", error);
|
|
151
|
+
}
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
event.preventDefault();
|
|
157
|
+
const text = (_e = (_d = event.clipboardData) === null || _d === void 0 ? void 0 : _d.getData("text/plain")) !== null && _e !== void 0 ? _e : "";
|
|
158
|
+
const html = markdownToHtml(text);
|
|
159
|
+
ensureCaretInEditor();
|
|
160
|
+
document.execCommand("insertHTML", false, html);
|
|
161
|
+
syncValue();
|
|
162
|
+
}, [enableImageUpload, ensureCaretInEditor, onImageFile, syncValue, uploadImage]);
|
|
163
|
+
const applySuggestion = useCallback((suggestion) => {
|
|
164
|
+
var _a;
|
|
165
|
+
const escaped = escapeMarkdownText(suggestion.title);
|
|
166
|
+
onChange(escaped);
|
|
167
|
+
onSuggestionSelect === null || onSuggestionSelect === void 0 ? void 0 : onSuggestionSelect(suggestion);
|
|
168
|
+
setPlainTextValue(suggestion.title);
|
|
169
|
+
setActiveSuggestionIndex(0);
|
|
170
|
+
setShowAllSuggestions(false);
|
|
171
|
+
if (editorRef.current) {
|
|
172
|
+
editorRef.current.innerHTML = markdownToHtml(escaped);
|
|
173
|
+
editorRef.current.focus();
|
|
174
|
+
const selection = typeof window !== "undefined" ? (_a = window.getSelection) === null || _a === void 0 ? void 0 : _a.call(window) : null;
|
|
175
|
+
if (selection && editorRef.current.firstChild) {
|
|
176
|
+
const range = document.createRange();
|
|
177
|
+
range.selectNodeContents(editorRef.current);
|
|
178
|
+
range.collapse(false);
|
|
179
|
+
selection.removeAllRanges();
|
|
180
|
+
selection.addRange(range);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}, [onChange, onSuggestionSelect]);
|
|
184
|
+
return (_jsxs("div", { className: "bn-step-field", children: [_jsxs("div", { className: "bn-step-field__top", children: [_jsxs("span", { className: "bn-step-field__label", children: [label, enableAutocomplete && (_jsx("button", { type: "button", className: "bn-step-toolbar__button", onMouseDown: (event) => {
|
|
185
|
+
var _a;
|
|
186
|
+
event.preventDefault();
|
|
187
|
+
setShowAllSuggestions(true);
|
|
188
|
+
(_a = editorRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
189
|
+
}, "aria-label": "Show suggestions", tabIndex: -1, children: "\u2304" }))] }), _jsxs("div", { className: "bn-step-toolbar", "aria-label": `${label} controls`, children: [showFormattingButtons && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "bn-step-toolbar__button", onMouseDown: (event) => {
|
|
190
|
+
var _a;
|
|
191
|
+
event.preventDefault();
|
|
192
|
+
(_a = editorRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
193
|
+
document.execCommand("bold");
|
|
194
|
+
syncValue();
|
|
195
|
+
}, "aria-label": "Bold", tabIndex: -1, children: "B" }), _jsx("button", { type: "button", className: "bn-step-toolbar__button", onMouseDown: (event) => {
|
|
196
|
+
var _a;
|
|
197
|
+
event.preventDefault();
|
|
198
|
+
(_a = editorRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
199
|
+
document.execCommand("italic");
|
|
200
|
+
syncValue();
|
|
201
|
+
}, "aria-label": "Italic", tabIndex: -1, children: "I" })] })), enableImageUpload && uploadImage && showImageButton && (_jsx("button", { type: "button", className: "bn-step-toolbar__button", onMouseDown: (event) => {
|
|
202
|
+
event.preventDefault();
|
|
203
|
+
const input = fileInputRef.current;
|
|
204
|
+
if (input) {
|
|
205
|
+
input.click();
|
|
206
|
+
}
|
|
207
|
+
}, "aria-label": "Insert image", tabIndex: -1, disabled: isUploading, children: "Img" })), rightAction] })] }), enableImageUpload && (_jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", style: { display: "none" }, onChange: async (event) => {
|
|
208
|
+
var _a;
|
|
209
|
+
const file = (_a = event.target.files) === null || _a === void 0 ? void 0 : _a[0];
|
|
210
|
+
if (!file || !uploadImage) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
setIsUploading(true);
|
|
215
|
+
const response = await uploadImage(file);
|
|
216
|
+
if (response === null || response === void 0 ? void 0 : response.url) {
|
|
217
|
+
const element = editorRef.current;
|
|
218
|
+
if (element) {
|
|
219
|
+
const escapedUrl = escapeHtml(response.url);
|
|
220
|
+
const needsBreak = element.innerHTML.trim().length > 0;
|
|
221
|
+
const imgHtml = (needsBreak ? "<br />" : "") +
|
|
222
|
+
`<img src="${escapedUrl}" alt="" class="bn-inline-image" contenteditable="false" draggable="false" />`;
|
|
223
|
+
element.focus();
|
|
224
|
+
ensureCaretInEditor();
|
|
225
|
+
document.execCommand("insertHTML", false, imgHtml);
|
|
226
|
+
syncValue();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
console.error("Failed to upload image", error);
|
|
232
|
+
}
|
|
233
|
+
finally {
|
|
234
|
+
setIsUploading(false);
|
|
235
|
+
event.target.value = "";
|
|
236
|
+
}
|
|
237
|
+
} })), _jsx("div", { ref: editorRef, className: "bn-step-editor", suppressContentEditableWarning: true, "data-placeholder": placeholder, "data-multiline": multiline ? "true" : "false", "data-step-field": fieldName, contentEditable: readOnly ? "false" : "true", onFocus: () => {
|
|
238
|
+
var _a, _b;
|
|
239
|
+
setIsFocused(true);
|
|
240
|
+
if (showSuggestionsOnFocus && enableAutocomplete) {
|
|
241
|
+
setShowAllSuggestions(true);
|
|
242
|
+
}
|
|
243
|
+
setPlainTextValue((_b = (_a = editorRef.current) === null || _a === void 0 ? void 0 : _a.innerText) !== null && _b !== void 0 ? _b : "");
|
|
244
|
+
}, onBlur: () => {
|
|
245
|
+
setIsFocused(false);
|
|
246
|
+
syncValue();
|
|
247
|
+
}, onInput: readOnly ? undefined : syncValue, onPaste: readOnly ? (event) => event.preventDefault() : handlePaste, onKeyDown: (event) => {
|
|
248
|
+
var _a, _b;
|
|
249
|
+
if (readOnly) {
|
|
250
|
+
const allowedKeys = new Set([
|
|
251
|
+
"ArrowDown",
|
|
252
|
+
"ArrowUp",
|
|
253
|
+
"Enter",
|
|
254
|
+
"Tab",
|
|
255
|
+
]);
|
|
256
|
+
const openKeys = enableAutocomplete && (event.metaKey || event.ctrlKey) && (event.code === "Space" || event.key === "" || event.key === " ");
|
|
257
|
+
if (!allowedKeys.has(event.key) && !openKeys) {
|
|
258
|
+
event.preventDefault();
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if ((event.key === "a" || event.key === "A") && (event.metaKey || event.ctrlKey)) {
|
|
263
|
+
event.preventDefault();
|
|
264
|
+
const selection = (_a = window.getSelection) === null || _a === void 0 ? void 0 : _a.call(window);
|
|
265
|
+
const node = editorRef.current;
|
|
266
|
+
if (selection && node) {
|
|
267
|
+
const range = document.createRange();
|
|
268
|
+
range.selectNodeContents(node);
|
|
269
|
+
selection.removeAllRanges();
|
|
270
|
+
selection.addRange(range);
|
|
271
|
+
}
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (enableAutocomplete && shouldShowAutocomplete) {
|
|
275
|
+
if (event.key === "ArrowDown") {
|
|
276
|
+
event.preventDefault();
|
|
277
|
+
setActiveSuggestionIndex((prev) => prev + 1 >= filteredSuggestions.length ? 0 : prev + 1);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (event.key === "ArrowUp") {
|
|
281
|
+
event.preventDefault();
|
|
282
|
+
setActiveSuggestionIndex((prev) => prev - 1 < 0 ? filteredSuggestions.length - 1 : prev - 1);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (event.key === "Enter" || event.key === "Tab") {
|
|
286
|
+
event.preventDefault();
|
|
287
|
+
const suggestion = (_b = filteredSuggestions[activeSuggestionIndex]) !== null && _b !== void 0 ? _b : filteredSuggestions[0];
|
|
288
|
+
if (suggestion) {
|
|
289
|
+
applySuggestion(suggestion);
|
|
290
|
+
}
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (enableAutocomplete && (event.metaKey || event.ctrlKey) && (event.code === "Space" || event.key === "" || event.key === " ")) {
|
|
295
|
+
event.preventDefault();
|
|
296
|
+
setShowAllSuggestions(true);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (event.key === "Enter") {
|
|
300
|
+
event.preventDefault();
|
|
301
|
+
if (multiline && event.shiftKey) {
|
|
302
|
+
document.execCommand("insertLineBreak");
|
|
303
|
+
document.execCommand("insertLineBreak");
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
document.execCommand("insertLineBreak");
|
|
307
|
+
}
|
|
308
|
+
syncValue();
|
|
309
|
+
}
|
|
310
|
+
} }), shouldShowAutocomplete && (_jsx("div", { className: "bn-step-suggestions", role: "listbox", "aria-label": `${label} suggestions`, children: filteredSuggestions.map((suggestion, index) => (_jsxs("button", { type: "button", role: "option", "aria-selected": index === activeSuggestionIndex, className: index === activeSuggestionIndex
|
|
311
|
+
? "bn-step-suggestion bn-step-suggestion--active"
|
|
312
|
+
: "bn-step-suggestion", onMouseDown: (event) => {
|
|
313
|
+
event.preventDefault();
|
|
314
|
+
applySuggestion(suggestion);
|
|
315
|
+
}, tabIndex: -1, children: [_jsx("span", { className: "bn-step-suggestion__title", children: suggestion.title }), typeof suggestion.usageCount === "number" && suggestion.usageCount > 0 && (_jsxs("span", { className: "bn-step-suggestion__meta", children: [suggestion.usageCount, " uses"] }))] }, suggestion.id))) }))] }));
|
|
316
|
+
}
|
|
@@ -20,13 +20,6 @@ const headingPrefixes = {
|
|
|
20
20
|
5: "#####",
|
|
21
21
|
6: "######",
|
|
22
22
|
};
|
|
23
|
-
const STEP_STATUSES = new Set(["draft", "ready", "blocked"]);
|
|
24
|
-
function normalizeStatus(value) {
|
|
25
|
-
if (value && STEP_STATUSES.has(value)) {
|
|
26
|
-
return value;
|
|
27
|
-
}
|
|
28
|
-
return "draft";
|
|
29
|
-
}
|
|
30
23
|
const SPECIAL_CHAR_REGEX = /([*_`~\[\]()<>\\])/g;
|
|
31
24
|
const HTML_SPAN_REGEX = /<\/?span[^>]*>/g;
|
|
32
25
|
const HTML_UNDERLINE_REGEX = /<\/?u>/g;
|
|
@@ -163,7 +156,7 @@ function flattenWithBlankLine(lines, appendBlank = false) {
|
|
|
163
156
|
return lines;
|
|
164
157
|
}
|
|
165
158
|
function serializeBlock(block, ctx, orderedIndex) {
|
|
166
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
159
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
167
160
|
const lines = [];
|
|
168
161
|
const indent = ctx.listDepth > 0 ? " ".repeat(ctx.listDepth) : "";
|
|
169
162
|
switch (block.type) {
|
|
@@ -229,25 +222,34 @@ function serializeBlock(block, ctx, orderedIndex) {
|
|
|
229
222
|
lines.push(...serializeChildren(block, ctx));
|
|
230
223
|
return lines;
|
|
231
224
|
}
|
|
232
|
-
case "
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
225
|
+
case "testStep":
|
|
226
|
+
case "snippet": {
|
|
227
|
+
const isSnippet = block.type === "snippet";
|
|
228
|
+
const snippetId = isSnippet ? ((_e = block.props.snippetId) !== null && _e !== void 0 ? _e : "").trim() : "";
|
|
229
|
+
const stepTitle = isSnippet
|
|
230
|
+
? ((_f = block.props.snippetTitle) !== null && _f !== void 0 ? _f : "").trim()
|
|
231
|
+
: ((_g = block.props.stepTitle) !== null && _g !== void 0 ? _g : "").trim();
|
|
232
|
+
const stepData = isSnippet
|
|
233
|
+
? ((_h = block.props.snippetData) !== null && _h !== void 0 ? _h : "").trim()
|
|
234
|
+
: ((_j = block.props.stepData) !== null && _j !== void 0 ? _j : "").trim();
|
|
235
|
+
const expectedResult = isSnippet
|
|
236
|
+
? ((_k = block.props.snippetExpectedResult) !== null && _k !== void 0 ? _k : "").trim()
|
|
237
|
+
: ((_l = block.props.expectedResult) !== null && _l !== void 0 ? _l : "").trim();
|
|
238
|
+
if (isSnippet) {
|
|
239
|
+
if (snippetId) {
|
|
240
|
+
lines.push(`<!-- begin snippet #${snippetId} -->`);
|
|
241
|
+
}
|
|
242
|
+
const dataLines = stepData
|
|
243
|
+
.split(/\r?\n/)
|
|
244
|
+
.filter((line) => !/^<!--\s*(begin|end)\s+snippet/i.test(line.trim()));
|
|
245
|
+
if (dataLines.length > 0) {
|
|
246
|
+
lines.push(...dataLines);
|
|
247
|
+
}
|
|
248
|
+
if (snippetId) {
|
|
249
|
+
lines.push(`<!-- end snippet #${snippetId} -->`);
|
|
250
|
+
}
|
|
251
|
+
return flattenWithBlankLine(lines, true);
|
|
243
252
|
}
|
|
244
|
-
lines.push(":::");
|
|
245
|
-
return flattenWithBlankLine(lines, true);
|
|
246
|
-
}
|
|
247
|
-
case "testStep": {
|
|
248
|
-
const stepTitle = ((_f = block.props.stepTitle) !== null && _f !== void 0 ? _f : "").trim();
|
|
249
|
-
const stepData = ((_g = block.props.stepData) !== null && _g !== void 0 ? _g : "").trim();
|
|
250
|
-
const expectedResult = ((_h = block.props.expectedResult) !== null && _h !== void 0 ? _h : "").trim();
|
|
251
253
|
if (stepTitle.length > 0) {
|
|
252
254
|
const normalizedTitle = stepTitle
|
|
253
255
|
.split(/\r?\n/)
|
|
@@ -311,7 +313,7 @@ function serializeBlock(block, ctx, orderedIndex) {
|
|
|
311
313
|
return flattenWithBlankLine(lines, true);
|
|
312
314
|
}
|
|
313
315
|
const headerRowCount = rows.length
|
|
314
|
-
? Math.min(rows.length, Math.max((
|
|
316
|
+
? Math.min(rows.length, Math.max((_m = tableContent.headerRows) !== null && _m !== void 0 ? _m : 1, 1))
|
|
315
317
|
: 0;
|
|
316
318
|
const columnAlignments = new Array(columnCount).fill("left");
|
|
317
319
|
const getCellAlignment = (cell) => {
|
|
@@ -605,13 +607,19 @@ function parseList(lines, startIndex, listType, indentLevel) {
|
|
|
605
607
|
}
|
|
606
608
|
return { items, nextIndex: index };
|
|
607
609
|
}
|
|
608
|
-
function parseTestStep(lines, index) {
|
|
610
|
+
function parseTestStep(lines, index, snippetId) {
|
|
609
611
|
const current = lines[index];
|
|
610
612
|
const trimmed = current.trim();
|
|
611
613
|
if (!trimmed.startsWith("* ") && !trimmed.startsWith("- ")) {
|
|
612
614
|
return null;
|
|
613
615
|
}
|
|
614
|
-
|
|
616
|
+
let rawTitle = unescapeMarkdown(trimmed.slice(2)).trim();
|
|
617
|
+
let blockType = "testStep";
|
|
618
|
+
const snippetMatch = rawTitle.match(/^snippet\s*[:\-–—]?\s*(.*)$/i);
|
|
619
|
+
if (snippetMatch) {
|
|
620
|
+
blockType = "snippet";
|
|
621
|
+
rawTitle = snippetMatch[1].trim();
|
|
622
|
+
}
|
|
615
623
|
const titleImages = [];
|
|
616
624
|
const titleWithPlaceholders = rawTitle
|
|
617
625
|
.replace(/!\[[^\]]*\]\(([^)]+)\)/g, (match) => {
|
|
@@ -620,7 +628,9 @@ function parseTestStep(lines, index) {
|
|
|
620
628
|
})
|
|
621
629
|
.replace(/\s{2,}/g, " ")
|
|
622
630
|
.trim();
|
|
623
|
-
const isLikelyStep =
|
|
631
|
+
const isLikelyStep = blockType === "snippet" ||
|
|
632
|
+
/^step\b/i.test(titleWithPlaceholders) ||
|
|
633
|
+
titleImages.length > 0;
|
|
624
634
|
const stepDataLines = [];
|
|
625
635
|
let expectedResult = "";
|
|
626
636
|
let next = index + 1;
|
|
@@ -693,50 +703,25 @@ function parseTestStep(lines, index) {
|
|
|
693
703
|
const stepDataWithImages = [stepData, titleImages.join("\n")]
|
|
694
704
|
.filter(Boolean)
|
|
695
705
|
.join(stepData ? "\n" : "");
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
706
|
+
const blockProps = blockType === "snippet"
|
|
707
|
+
? {
|
|
708
|
+
snippetId: snippetId !== null && snippetId !== void 0 ? snippetId : "",
|
|
709
|
+
snippetTitle: titleWithPlaceholders,
|
|
710
|
+
snippetData: stepDataWithImages,
|
|
711
|
+
snippetExpectedResult: expectedResult,
|
|
712
|
+
}
|
|
713
|
+
: {
|
|
714
|
+
stepTitle: titleWithPlaceholders,
|
|
715
|
+
stepData: stepDataWithImages,
|
|
716
|
+
expectedResult,
|
|
717
|
+
};
|
|
718
|
+
const parsedBlock = {
|
|
719
|
+
type: blockType,
|
|
720
|
+
props: blockProps,
|
|
721
|
+
children: [],
|
|
707
722
|
};
|
|
708
|
-
}
|
|
709
|
-
function parseTestCase(lines, index) {
|
|
710
|
-
var _a;
|
|
711
|
-
const trimmed = lines[index].trim();
|
|
712
|
-
if (!trimmed.startsWith(":::test-case")) {
|
|
713
|
-
return null;
|
|
714
|
-
}
|
|
715
|
-
const statusMatch = trimmed.match(/status="([^"]*)"/);
|
|
716
|
-
const referenceMatch = trimmed.match(/reference="([^"]*)"/);
|
|
717
|
-
let bodyLines = [];
|
|
718
|
-
let next = index + 1;
|
|
719
|
-
while (next < lines.length && lines[next].trim() !== ":::") {
|
|
720
|
-
bodyLines.push(lines[next]);
|
|
721
|
-
next += 1;
|
|
722
|
-
}
|
|
723
|
-
if (next < lines.length && lines[next].trim() === ":::") {
|
|
724
|
-
next += 1;
|
|
725
|
-
}
|
|
726
|
-
const contentText = bodyLines.join("\n").trim();
|
|
727
723
|
return {
|
|
728
|
-
block:
|
|
729
|
-
type: "testCase",
|
|
730
|
-
props: {
|
|
731
|
-
...cloneBaseProps(),
|
|
732
|
-
status: normalizeStatus(statusMatch === null || statusMatch === void 0 ? void 0 : statusMatch[1]),
|
|
733
|
-
reference: (_a = referenceMatch === null || referenceMatch === void 0 ? void 0 : referenceMatch[1]) !== null && _a !== void 0 ? _a : "",
|
|
734
|
-
},
|
|
735
|
-
content: contentText
|
|
736
|
-
? [{ type: "text", text: unescapeMarkdown(contentText), styles: {} }]
|
|
737
|
-
: undefined,
|
|
738
|
-
children: [],
|
|
739
|
-
},
|
|
724
|
+
block: parsedBlock,
|
|
740
725
|
nextIndex: next,
|
|
741
726
|
};
|
|
742
727
|
}
|
|
@@ -868,6 +853,52 @@ function parseParagraph(lines, index) {
|
|
|
868
853
|
nextIndex: next,
|
|
869
854
|
};
|
|
870
855
|
}
|
|
856
|
+
function parseSnippetWrapper(lines, index) {
|
|
857
|
+
const trimmed = lines[index].trim();
|
|
858
|
+
const startMatch = trimmed.match(/^<!--\s*begin snippet\s*#?([^\s>]+)\s*-->/i);
|
|
859
|
+
if (!startMatch) {
|
|
860
|
+
return null;
|
|
861
|
+
}
|
|
862
|
+
const snippetId = startMatch[1];
|
|
863
|
+
const innerLines = [];
|
|
864
|
+
let next = index + 1;
|
|
865
|
+
while (next < lines.length) {
|
|
866
|
+
const maybeEnd = lines[next].trim();
|
|
867
|
+
const endMatch = maybeEnd.match(/^<!--\s*end snippet\s*#?([^\s>]+)?\s*-->/i);
|
|
868
|
+
if (endMatch) {
|
|
869
|
+
const endId = endMatch[1];
|
|
870
|
+
if (!endId || endId === snippetId) {
|
|
871
|
+
next += 1;
|
|
872
|
+
break;
|
|
873
|
+
}
|
|
874
|
+
// Ignore unrelated snippet end markers but keep scanning.
|
|
875
|
+
next += 1;
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
const otherStart = maybeEnd.match(/^<!--\s*begin snippet\s*#?([^\s>]+)\s*-->/i);
|
|
879
|
+
if (otherStart) {
|
|
880
|
+
// Skip nested snippet wrappers from the body entirely.
|
|
881
|
+
next += 1;
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
innerLines.push(lines[next]);
|
|
885
|
+
next += 1;
|
|
886
|
+
}
|
|
887
|
+
const snippetBlock = {
|
|
888
|
+
type: "snippet",
|
|
889
|
+
props: {
|
|
890
|
+
snippetId,
|
|
891
|
+
snippetTitle: "",
|
|
892
|
+
snippetData: innerLines.join("\n").trim(),
|
|
893
|
+
snippetExpectedResult: "",
|
|
894
|
+
},
|
|
895
|
+
children: [],
|
|
896
|
+
};
|
|
897
|
+
return {
|
|
898
|
+
block: snippetBlock,
|
|
899
|
+
nextIndex: next,
|
|
900
|
+
};
|
|
901
|
+
}
|
|
871
902
|
export function markdownToBlocks(markdown) {
|
|
872
903
|
const normalized = markdown.replace(/\r\n/g, "\n");
|
|
873
904
|
const lines = normalized.split("\n");
|
|
@@ -879,16 +910,16 @@ export function markdownToBlocks(markdown) {
|
|
|
879
910
|
index += 1;
|
|
880
911
|
continue;
|
|
881
912
|
}
|
|
882
|
-
const
|
|
883
|
-
if (
|
|
884
|
-
blocks.push(
|
|
885
|
-
index =
|
|
913
|
+
const snippetWrapper = parseSnippetWrapper(lines, index);
|
|
914
|
+
if (snippetWrapper) {
|
|
915
|
+
blocks.push(snippetWrapper.block);
|
|
916
|
+
index = snippetWrapper.nextIndex;
|
|
886
917
|
continue;
|
|
887
918
|
}
|
|
888
|
-
const
|
|
889
|
-
if (
|
|
890
|
-
blocks.push(
|
|
891
|
-
index =
|
|
919
|
+
const stepLikeBlock = parseTestStep(lines, index);
|
|
920
|
+
if (stepLikeBlock) {
|
|
921
|
+
blocks.push(stepLikeBlock.block);
|
|
922
|
+
index = stepLikeBlock.nextIndex;
|
|
892
923
|
continue;
|
|
893
924
|
}
|
|
894
925
|
const table = parseTable(lines, index);
|