testomatio-editor-blocks 0.4.34 → 0.4.36
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/editor/blocks/step.js +0 -29
- package/package/editor/blocks/stepField.js +167 -34
- package/package/editor/customMarkdownConverter.js +47 -25
- package/package/styles.css +8 -1
- package/package.json +1 -1
- package/src/App.tsx +2 -7
- package/src/editor/blocks/step.tsx +0 -37
- package/src/editor/blocks/stepField.tsx +172 -34
- package/src/editor/customMarkdownConverter.test.ts +371 -0
- package/src/editor/customMarkdownConverter.ts +49 -25
- package/src/editor/styles.css +8 -1
|
@@ -193,31 +193,6 @@ export const stepBlock = createReactBlockSpec({
|
|
|
193
193
|
}
|
|
194
194
|
return count;
|
|
195
195
|
}, [block.id, documentVersion, editor.document]);
|
|
196
|
-
// Check if there is a preceding "Steps" heading
|
|
197
|
-
const hasStepsHeading = useMemo(() => {
|
|
198
|
-
const allBlocks = editor.document;
|
|
199
|
-
const blockIndex = allBlocks.findIndex((b) => b.id === block.id);
|
|
200
|
-
if (blockIndex < 0)
|
|
201
|
-
return false;
|
|
202
|
-
for (let i = blockIndex - 1; i >= 0; i--) {
|
|
203
|
-
const b = allBlocks[i];
|
|
204
|
-
if (b.type === "testStep" || b.type === "snippet" || isEmptyParagraph(b)) {
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
if (b.type === "heading") {
|
|
208
|
-
const text = (Array.isArray(b.content) ? b.content : [])
|
|
209
|
-
.filter((n) => n.type === "text")
|
|
210
|
-
.map((n) => { var _a; return (_a = n.text) !== null && _a !== void 0 ? _a : ""; })
|
|
211
|
-
.join("")
|
|
212
|
-
.trim()
|
|
213
|
-
.toLowerCase()
|
|
214
|
-
.replace(/[:\-–—]$/, "");
|
|
215
|
-
return isStepsHeading(text);
|
|
216
|
-
}
|
|
217
|
-
return false;
|
|
218
|
-
}
|
|
219
|
-
return false;
|
|
220
|
-
}, [block.id, documentVersion, editor.document]);
|
|
221
196
|
useEditorChange(() => {
|
|
222
197
|
setDocumentVersion((version) => version + 1);
|
|
223
198
|
}, editor);
|
|
@@ -362,10 +337,6 @@ export const stepBlock = createReactBlockSpec({
|
|
|
362
337
|
}, [expectedHasContent, isExpectedVisible]);
|
|
363
338
|
const canToggleData = !dataHasContent;
|
|
364
339
|
const canToggleExpected = !expectedHasContent;
|
|
365
|
-
// Render as plain text when not under a "Steps" heading
|
|
366
|
-
if (!hasStepsHeading) {
|
|
367
|
-
return (_jsxs("div", { className: "bn-teststep-plain", "data-block-id": block.id, children: [_jsx("span", { children: stepTitle || "(empty step)" }), stepData ? _jsx("span", { className: "bn-teststep-plain__data", children: stepData }) : null, expectedResult ? _jsx("span", { className: "bn-teststep-plain__expected", children: expectedResult }) : null] }));
|
|
368
|
-
}
|
|
369
340
|
if (viewMode === "horizontal") {
|
|
370
341
|
return (_jsx(StepHorizontalView, { blockId: block.id, stepNumber: stepNumber, stepValue: combinedStepValue, expectedResult: expectedResult, onStepChange: handleCombinedStepChange, onExpectedChange: handleExpectedChange, onInsertNextStep: handleInsertNextStep, onFieldFocus: handleFieldFocus, viewToggle: _jsx("button", { type: "button", className: "bn-teststep__view-toggle bn-teststep__view-toggle--horizontal", "data-tooltip": "Switch step view", "aria-label": "Switch step view", onClick: handleToggleView, children: _jsxs("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: [_jsx("mask", { id: "mask-toggle", style: { maskType: "alpha" }, maskUnits: "userSpaceOnUse", x: "0", y: "0", width: "16", height: "16", children: _jsx("rect", { width: "16", height: "16", fill: "#D9D9D9" }) }), _jsx("g", { mask: "url(#mask-toggle)", children: _jsx("path", { d: "M12.6667 2C13.0333 2 13.3472 2.13056 13.6083 2.39167C13.8694 2.65278 14 2.96667 14 3.33333L14 12.6667C14 13.0333 13.8694 13.3472 13.6083 13.6083C13.3472 13.8694 13.0333 14 12.6667 14L10 14C9.63333 14 9.31944 13.8694 9.05833 13.6083C8.79722 13.3472 8.66667 13.0333 8.66667 12.6667L8.66667 3.33333C8.66667 2.96667 8.79722 2.65278 9.05833 2.39167C9.31945 2.13055 9.63333 2 10 2L12.6667 2ZM6 2C6.36667 2 6.68056 2.13055 6.94167 2.39167C7.20278 2.65278 7.33333 2.96667 7.33333 3.33333L7.33333 12.6667C7.33333 13.0333 7.20278 13.3472 6.94167 13.6083C6.68055 13.8694 6.36667 14 6 14L3.33333 14C2.96667 14 2.65278 13.8694 2.39167 13.6083C2.13056 13.3472 2 13.0333 2 12.6667L2 3.33333C2 2.96667 2.13056 2.65278 2.39167 2.39167C2.65278 2.13055 2.96667 2 3.33333 2L6 2ZM3.33333 12.6667L6 12.6667L6 3.33333L3.33333 3.33333L3.33333 12.6667Z", fill: "currentColor" }) })] }) }) }));
|
|
371
342
|
}
|
|
@@ -128,6 +128,32 @@ function stripInlineMarkdown(markdown) {
|
|
|
128
128
|
continue;
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
|
+
// Code block: ```\n...\n``` (triple backticks with newlines)
|
|
132
|
+
if (markdown[i] === "`" && markdown[i + 1] === "`" && markdown[i + 2] === "`") {
|
|
133
|
+
const contentStart = markdown[i + 3] === "\n" ? i + 4 : i + 3;
|
|
134
|
+
const closeIdx = markdown.indexOf("```", contentStart);
|
|
135
|
+
if (closeIdx !== -1) {
|
|
136
|
+
const contentEnd = markdown[closeIdx - 1] === "\n" ? closeIdx - 1 : closeIdx;
|
|
137
|
+
const inner = markdown.slice(contentStart, contentEnd);
|
|
138
|
+
const start = plainText.length;
|
|
139
|
+
plainText += inner;
|
|
140
|
+
formatting.push({ start, end: plainText.length, type: "code" });
|
|
141
|
+
i = closeIdx + 3;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Inline code: `text`
|
|
146
|
+
if (markdown[i] === "`") {
|
|
147
|
+
const closeIdx = markdown.indexOf("`", i + 1);
|
|
148
|
+
if (closeIdx !== -1) {
|
|
149
|
+
const inner = markdown.slice(i + 1, closeIdx);
|
|
150
|
+
const start = plainText.length;
|
|
151
|
+
plainText += inner;
|
|
152
|
+
formatting.push({ start, end: plainText.length, type: "code" });
|
|
153
|
+
i = closeIdx + 1;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
131
157
|
plainText += markdown[i];
|
|
132
158
|
i++;
|
|
133
159
|
}
|
|
@@ -138,13 +164,24 @@ function buildFullMarkdown(plainText, links, formatting) {
|
|
|
138
164
|
return plainText;
|
|
139
165
|
const markers = [];
|
|
140
166
|
for (const fmt of formatting) {
|
|
141
|
-
|
|
167
|
+
let openMarker;
|
|
168
|
+
let closeMarker;
|
|
169
|
+
if (fmt.type === "code") {
|
|
170
|
+
const content = plainText.slice(fmt.start, fmt.end);
|
|
171
|
+
const isMultiline = content.includes("\n");
|
|
172
|
+
openMarker = isMultiline ? "```\n" : "`";
|
|
173
|
+
closeMarker = isMultiline ? "\n```" : "`";
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
openMarker = fmt.type === "bold" ? "**" : "*";
|
|
177
|
+
closeMarker = openMarker;
|
|
178
|
+
}
|
|
142
179
|
// Opening: outer markers (bold) before inner (italic) → bold order=0, italic order=1
|
|
143
180
|
// Closing: inner markers (italic) before outer (bold) → italic order=0, bold order=1
|
|
144
|
-
const openOrder = fmt.type === "bold" ? 0 : 1;
|
|
145
|
-
const closeOrder = fmt.type === "bold" ? 1 : 0;
|
|
146
|
-
markers.push({ pos: fmt.start, text:
|
|
147
|
-
markers.push({ pos: fmt.end, text:
|
|
181
|
+
const openOrder = fmt.type === "bold" ? 0 : fmt.type === "code" ? 2 : 1;
|
|
182
|
+
const closeOrder = fmt.type === "bold" ? 1 : fmt.type === "code" ? -1 : 0;
|
|
183
|
+
markers.push({ pos: fmt.start, text: openMarker, order: openOrder });
|
|
184
|
+
markers.push({ pos: fmt.end, text: closeMarker, order: closeOrder });
|
|
148
185
|
}
|
|
149
186
|
for (const link of links) {
|
|
150
187
|
// Link brackets go outside formatting markers
|
|
@@ -173,14 +210,23 @@ function adjustFormattingForEdit(formatting, editPos, delta) {
|
|
|
173
210
|
})
|
|
174
211
|
.filter((fmt) => fmt.end > fmt.start);
|
|
175
212
|
}
|
|
176
|
-
function getCaretRectInPreview(preview, offset) {
|
|
213
|
+
function getCaretRectInPreview(preview, offset, textareaValue) {
|
|
214
|
+
// Convert textarea-space offset to preview-space (strip newlines)
|
|
215
|
+
let nlCount = 0;
|
|
216
|
+
if (textareaValue) {
|
|
217
|
+
for (let i = 0; i < offset && i < textareaValue.length; i++) {
|
|
218
|
+
if (textareaValue[i] === "\n")
|
|
219
|
+
nlCount++;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const previewOffset = offset - nlCount;
|
|
177
223
|
const walker = document.createTreeWalker(preview, NodeFilter.SHOW_TEXT);
|
|
178
224
|
let currentOffset = 0;
|
|
179
225
|
while (walker.nextNode()) {
|
|
180
226
|
const textNode = walker.currentNode;
|
|
181
227
|
const nodeLen = textNode.length;
|
|
182
|
-
if (
|
|
183
|
-
const localOffset =
|
|
228
|
+
if (previewOffset <= currentOffset + nodeLen) {
|
|
229
|
+
const localOffset = previewOffset - currentOffset;
|
|
184
230
|
try {
|
|
185
231
|
const range = document.createRange();
|
|
186
232
|
range.setStart(textNode, localOffset);
|
|
@@ -201,7 +247,7 @@ function getCaretRectInPreview(preview, offset) {
|
|
|
201
247
|
}
|
|
202
248
|
return null;
|
|
203
249
|
}
|
|
204
|
-
function applyFormattingHighlights(preview, formatting) {
|
|
250
|
+
function applyFormattingHighlights(preview, formatting, textareaValue) {
|
|
205
251
|
if (formatting.length === 0)
|
|
206
252
|
return;
|
|
207
253
|
// Remove previous formatting highlights
|
|
@@ -227,8 +273,38 @@ function applyFormattingHighlights(preview, formatting) {
|
|
|
227
273
|
parent.removeChild(el);
|
|
228
274
|
}
|
|
229
275
|
}
|
|
276
|
+
const existingCode = preview.querySelectorAll("code.step-preview-code");
|
|
277
|
+
for (let i = 0; i < existingCode.length; i++) {
|
|
278
|
+
const el = existingCode[i];
|
|
279
|
+
const parent = el.parentNode;
|
|
280
|
+
if (parent) {
|
|
281
|
+
while (el.firstChild) {
|
|
282
|
+
parent.insertBefore(el.firstChild, el);
|
|
283
|
+
}
|
|
284
|
+
parent.removeChild(el);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// After unwrapping formatting elements, merge adjacent/empty text nodes
|
|
288
|
+
// so the tree walker sees clean text nodes matching the original structure.
|
|
289
|
+
preview.normalize();
|
|
290
|
+
// OverType splits textarea lines into <div> elements, discarding the \n
|
|
291
|
+
// characters. Convert textarea-space positions (with \n) to preview-space
|
|
292
|
+
// positions (without \n) so we can find the correct text nodes.
|
|
293
|
+
function taToPreview(taPos) {
|
|
294
|
+
if (!textareaValue)
|
|
295
|
+
return taPos;
|
|
296
|
+
let nlCount = 0;
|
|
297
|
+
for (let i = 0; i < taPos && i < textareaValue.length; i++) {
|
|
298
|
+
if (textareaValue[i] === "\n")
|
|
299
|
+
nlCount++;
|
|
300
|
+
}
|
|
301
|
+
return taPos - nlCount;
|
|
302
|
+
}
|
|
230
303
|
const sorted = [...formatting].sort((a, b) => b.start - a.start);
|
|
231
304
|
for (const fmt of sorted) {
|
|
305
|
+
const pStart = taToPreview(fmt.start);
|
|
306
|
+
const pEnd = taToPreview(fmt.end);
|
|
307
|
+
// Collect text nodes with their preview-space offsets
|
|
232
308
|
const walker = document.createTreeWalker(preview, NodeFilter.SHOW_TEXT);
|
|
233
309
|
let currentOffset = 0;
|
|
234
310
|
let startNode = null;
|
|
@@ -239,13 +315,13 @@ function applyFormattingHighlights(preview, formatting) {
|
|
|
239
315
|
const textNode = walker.currentNode;
|
|
240
316
|
const nodeStart = currentOffset;
|
|
241
317
|
const nodeEnd = currentOffset + textNode.length;
|
|
242
|
-
if (!startNode &&
|
|
318
|
+
if (!startNode && pStart >= nodeStart && pStart < nodeEnd) {
|
|
243
319
|
startNode = textNode;
|
|
244
|
-
startLocalOffset =
|
|
320
|
+
startLocalOffset = pStart - nodeStart;
|
|
245
321
|
}
|
|
246
|
-
if (!endNode &&
|
|
322
|
+
if (!endNode && pEnd > nodeStart && pEnd <= nodeEnd) {
|
|
247
323
|
endNode = textNode;
|
|
248
|
-
endLocalOffset =
|
|
324
|
+
endLocalOffset = pEnd - nodeStart;
|
|
249
325
|
}
|
|
250
326
|
currentOffset = nodeEnd;
|
|
251
327
|
if (startNode && endNode)
|
|
@@ -253,18 +329,63 @@ function applyFormattingHighlights(preview, formatting) {
|
|
|
253
329
|
}
|
|
254
330
|
if (!startNode || !endNode)
|
|
255
331
|
continue;
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
332
|
+
const tagName = fmt.type === "bold" ? "strong" : fmt.type === "code" ? "code" : "em";
|
|
333
|
+
const className = fmt.type === "bold" ? "step-preview-bold" : fmt.type === "code" ? "step-preview-code" : "step-preview-italic";
|
|
334
|
+
// If start and end are in the same text node, wrap directly
|
|
335
|
+
if (startNode === endNode) {
|
|
336
|
+
try {
|
|
337
|
+
const range = document.createRange();
|
|
338
|
+
range.setStart(startNode, startLocalOffset);
|
|
339
|
+
range.setEnd(endNode, endLocalOffset);
|
|
340
|
+
const wrapper = document.createElement(tagName);
|
|
341
|
+
wrapper.className = className;
|
|
342
|
+
const fragment = range.extractContents();
|
|
343
|
+
wrapper.appendChild(fragment);
|
|
344
|
+
range.insertNode(wrapper);
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
// DOM manipulation can fail if range crosses element boundaries
|
|
348
|
+
}
|
|
265
349
|
}
|
|
266
|
-
|
|
267
|
-
//
|
|
350
|
+
else {
|
|
351
|
+
// Multi-node range (e.g. code spanning multiple lines/divs):
|
|
352
|
+
// collect all text nodes in the range, then wrap each one individually
|
|
353
|
+
const textNodes = [];
|
|
354
|
+
const walker2 = document.createTreeWalker(preview, NodeFilter.SHOW_TEXT);
|
|
355
|
+
let collecting = false;
|
|
356
|
+
while (walker2.nextNode()) {
|
|
357
|
+
const tn = walker2.currentNode;
|
|
358
|
+
if (tn === startNode) {
|
|
359
|
+
collecting = true;
|
|
360
|
+
textNodes.push({ node: tn, localStart: startLocalOffset, localEnd: tn.length });
|
|
361
|
+
}
|
|
362
|
+
else if (tn === endNode) {
|
|
363
|
+
textNodes.push({ node: tn, localStart: 0, localEnd: endLocalOffset });
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
else if (collecting) {
|
|
367
|
+
textNodes.push({ node: tn, localStart: 0, localEnd: tn.length });
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
// Wrap in reverse order to preserve offsets
|
|
371
|
+
for (let ti = textNodes.length - 1; ti >= 0; ti--) {
|
|
372
|
+
const { node, localStart, localEnd } = textNodes[ti];
|
|
373
|
+
if (localStart >= localEnd)
|
|
374
|
+
continue;
|
|
375
|
+
try {
|
|
376
|
+
const range = document.createRange();
|
|
377
|
+
range.setStart(node, localStart);
|
|
378
|
+
range.setEnd(node, localEnd);
|
|
379
|
+
const wrapper = document.createElement(tagName);
|
|
380
|
+
wrapper.className = className;
|
|
381
|
+
const fragment = range.extractContents();
|
|
382
|
+
wrapper.appendChild(fragment);
|
|
383
|
+
range.insertNode(wrapper);
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
// skip nodes that can't be wrapped
|
|
387
|
+
}
|
|
388
|
+
}
|
|
268
389
|
}
|
|
269
390
|
}
|
|
270
391
|
}
|
|
@@ -411,6 +532,7 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
411
532
|
(_a = onChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onChangeRef, markdown);
|
|
412
533
|
}, []);
|
|
413
534
|
useEffect(() => {
|
|
535
|
+
var _a;
|
|
414
536
|
const container = editorContainerRef.current;
|
|
415
537
|
if (!container) {
|
|
416
538
|
return;
|
|
@@ -431,12 +553,13 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
431
553
|
// Monkey-patch updatePreview to add link highlights
|
|
432
554
|
const originalUpdatePreview = instance.updatePreview.bind(instance);
|
|
433
555
|
instance.updatePreview = function () {
|
|
556
|
+
var _a;
|
|
434
557
|
originalUpdatePreview();
|
|
435
|
-
applyFormattingHighlights(this.preview, formattingRef.current);
|
|
558
|
+
applyFormattingHighlights(this.preview, formattingRef.current, (_a = this.textarea) === null || _a === void 0 ? void 0 : _a.value);
|
|
436
559
|
applyLinkHighlights(this.preview, linksRef.current);
|
|
437
560
|
};
|
|
438
561
|
// Apply initial highlights
|
|
439
|
-
applyFormattingHighlights(instance.preview, formattingRef.current);
|
|
562
|
+
applyFormattingHighlights(instance.preview, formattingRef.current, (_a = instance.textarea) === null || _a === void 0 ? void 0 : _a.value);
|
|
440
563
|
applyLinkHighlights(instance.preview, linksRef.current);
|
|
441
564
|
// Create custom caret element inside the wrapper
|
|
442
565
|
const caretEl = document.createElement("div");
|
|
@@ -459,7 +582,7 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
459
582
|
if (!textareaNode || !instance || !caret)
|
|
460
583
|
return;
|
|
461
584
|
const updateCaret = () => {
|
|
462
|
-
var _a, _b;
|
|
585
|
+
var _a, _b, _c;
|
|
463
586
|
const hasFormatting = formattingRef.current.length > 0;
|
|
464
587
|
if (!hasFormatting) {
|
|
465
588
|
caret.style.display = "none";
|
|
@@ -480,7 +603,7 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
480
603
|
caret.style.display = "none";
|
|
481
604
|
return;
|
|
482
605
|
}
|
|
483
|
-
const rect = getCaretRectInPreview(instance.preview, pos);
|
|
606
|
+
const rect = getCaretRectInPreview(instance.preview, pos, (_c = instance.textarea) === null || _c === void 0 ? void 0 : _c.value);
|
|
484
607
|
if (rect) {
|
|
485
608
|
caret.style.display = "block";
|
|
486
609
|
caret.style.top = `${rect.top}px`;
|
|
@@ -530,6 +653,7 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
530
653
|
textareaNode.focus();
|
|
531
654
|
}, [focusSignal, textareaNode]);
|
|
532
655
|
useEffect(() => {
|
|
656
|
+
var _a;
|
|
533
657
|
const instance = editorInstanceRef.current;
|
|
534
658
|
if (!instance) {
|
|
535
659
|
setPlainTextValue((prev) => {
|
|
@@ -549,7 +673,7 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
549
673
|
}
|
|
550
674
|
else {
|
|
551
675
|
// Even if text didn't change, formatting/links might have — re-apply highlights
|
|
552
|
-
applyFormattingHighlights(instance.preview, formatting);
|
|
676
|
+
applyFormattingHighlights(instance.preview, formatting, (_a = instance.textarea) === null || _a === void 0 ? void 0 : _a.value);
|
|
553
677
|
applyLinkHighlights(instance.preview, links);
|
|
554
678
|
}
|
|
555
679
|
setPlainTextValue((prev) => {
|
|
@@ -713,7 +837,7 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
713
837
|
return;
|
|
714
838
|
}
|
|
715
839
|
textareaNode.focus();
|
|
716
|
-
const fmtType = action === "toggleBold" ? "bold" : "italic";
|
|
840
|
+
const fmtType = action === "toggleBold" ? "bold" : action === "toggleCode" ? "code" : "italic";
|
|
717
841
|
const start = (_a = textareaNode.selectionStart) !== null && _a !== void 0 ? _a : 0;
|
|
718
842
|
const end = (_b = textareaNode.selectionEnd) !== null && _b !== void 0 ? _b : 0;
|
|
719
843
|
// Check if selection is already formatted
|
|
@@ -736,7 +860,7 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
736
860
|
(_c = onChangeRef.current) === null || _c === void 0 ? void 0 : _c.call(onChangeRef, markdown);
|
|
737
861
|
setPlainTextValue(markdownToPlainText(markdown));
|
|
738
862
|
// Re-apply highlights
|
|
739
|
-
applyFormattingHighlights(instance.preview, formattingRef.current);
|
|
863
|
+
applyFormattingHighlights(instance.preview, formattingRef.current, textareaNode === null || textareaNode === void 0 ? void 0 : textareaNode.value);
|
|
740
864
|
applyLinkHighlights(instance.preview, linksRef.current);
|
|
741
865
|
}, [textareaNode]);
|
|
742
866
|
const linkPopoverRef = useRef(null);
|
|
@@ -799,7 +923,7 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
799
923
|
requestAnimationFrame(() => textareaNode === null || textareaNode === void 0 ? void 0 : textareaNode.focus());
|
|
800
924
|
}, [textareaNode]);
|
|
801
925
|
const handleRemoveLink = useCallback(() => {
|
|
802
|
-
var _a;
|
|
926
|
+
var _a, _b;
|
|
803
927
|
linksRef.current = linksRef.current.filter((l) => l !== cursorLink);
|
|
804
928
|
setCursorLink(null);
|
|
805
929
|
const instance = editorInstanceRef.current;
|
|
@@ -807,7 +931,7 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
807
931
|
const markdown = buildFullMarkdown(instance.getValue(), linksRef.current, formattingRef.current);
|
|
808
932
|
(_a = onChangeRef.current) === null || _a === void 0 ? void 0 : _a.call(onChangeRef, markdown);
|
|
809
933
|
// Re-apply highlights since links changed
|
|
810
|
-
applyFormattingHighlights(instance.preview, formattingRef.current);
|
|
934
|
+
applyFormattingHighlights(instance.preview, formattingRef.current, (_b = instance.textarea) === null || _b === void 0 ? void 0 : _b.value);
|
|
811
935
|
applyLinkHighlights(instance.preview, linksRef.current);
|
|
812
936
|
}
|
|
813
937
|
}, [cursorLink]);
|
|
@@ -943,6 +1067,12 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
943
1067
|
handleToolbarAction("toggleItalic");
|
|
944
1068
|
return;
|
|
945
1069
|
}
|
|
1070
|
+
if (event.key === "e" || event.key === "E") {
|
|
1071
|
+
event.preventDefault();
|
|
1072
|
+
event.stopImmediatePropagation();
|
|
1073
|
+
handleToolbarAction("toggleCode");
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
946
1076
|
}
|
|
947
1077
|
if (enableAutocomplete && shouldShowAutocomplete) {
|
|
948
1078
|
if (event.key === "ArrowDown") {
|
|
@@ -1038,7 +1168,10 @@ export function StepField({ label, showLabel = true, labelToggle, labelAction, p
|
|
|
1038
1168
|
}, "aria-label": "Bold", tabIndex: -1, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M4 2.66675H8.33333C8.92064 2.66677 9.49502 2.83918 9.98525 3.1626C10.4755 3.48602 10.86 3.94622 11.0911 4.48613C11.3223 5.02604 11.3898 5.62192 11.2855 6.19988C11.1811 6.77783 10.9094 7.31244 10.504 7.73741C11.0752 8.06825 11.5213 8.57823 11.7733 9.18833C12.0252 9.79844 12.0689 10.4746 11.8976 11.1121C11.7263 11.7495 11.3495 12.3127 10.8256 12.7143C10.3018 13.1159 9.66008 13.3335 9 13.3334H4V12.0001H4.66667V4.00008H4V2.66675ZM6 7.33341H8.33333C8.77536 7.33341 9.19928 7.15782 9.51184 6.84526C9.8244 6.5327 10 6.10878 10 5.66675C10 5.22472 9.8244 4.8008 9.51184 4.48824C9.19928 4.17568 8.77536 4.00008 8.33333 4.00008H6V7.33341ZM6 8.66675V12.0001H9C9.44203 12.0001 9.86595 11.8245 10.1785 11.5119C10.4911 11.1994 10.6667 10.7754 10.6667 10.3334C10.6667 9.89139 10.4911 9.46746 10.1785 9.1549C9.86595 8.84234 9.44203 8.66675 9 8.66675H6Z", fill: "currentColor" }) }) }), _jsx("button", { type: "button", className: "bn-step-toolbar__button", "data-tooltip": "Italic", onMouseDown: (event) => {
|
|
1039
1169
|
event.preventDefault();
|
|
1040
1170
|
handleToolbarAction("toggleItalic");
|
|
1041
|
-
}, "aria-label": "Italic", tabIndex: -1, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M8.66699 13.3334H4.66699V12.0001H5.95166L8.69566 4.00008H7.33366V2.66675H11.3337V4.00008H10.049L7.30499 12.0001H8.66699V13.3334Z", fill: "currentColor" }) }) })
|
|
1171
|
+
}, "aria-label": "Italic", tabIndex: -1, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M8.66699 13.3334H4.66699V12.0001H5.95166L8.69566 4.00008H7.33366V2.66675H11.3337V4.00008H10.049L7.30499 12.0001H8.66699V13.3334Z", fill: "currentColor" }) }) }), _jsx("button", { type: "button", className: "bn-step-toolbar__button", "data-tooltip": "Code", onMouseDown: (event) => {
|
|
1172
|
+
event.preventDefault();
|
|
1173
|
+
handleToolbarAction("toggleCode");
|
|
1174
|
+
}, "aria-label": "Code", tabIndex: -1, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: _jsx("path", { d: "M10.333 12.6667L14 8.00008L10.333 3.33341L9.15833 4.28341L12.1583 8.00008L9.15833 11.7167L10.333 12.6667ZM5.66699 12.6667L6.84166 11.7167L3.84166 8.00008L6.84166 4.28341L5.66699 3.33341L2 8.00008L5.66699 12.6667Z", fill: "currentColor" }) }) })] })), enableImageUpload && uploadImage && showImageButton && (_jsx("button", { type: "button", className: "bn-step-toolbar__button", "data-tooltip": "Insert image", onMouseDown: (event) => {
|
|
1042
1175
|
var _a;
|
|
1043
1176
|
event.preventDefault();
|
|
1044
1177
|
(_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click();
|
|
@@ -80,7 +80,6 @@ function unescapeMarkdown(text) {
|
|
|
80
80
|
return stripHtmlWrappers(text).replace(/\\([*_`~\[\]()<>\\])/g, "$1");
|
|
81
81
|
}
|
|
82
82
|
function applyTextStyles(text, styles) {
|
|
83
|
-
var _a;
|
|
84
83
|
if (!styles) {
|
|
85
84
|
return text;
|
|
86
85
|
}
|
|
@@ -116,11 +115,22 @@ function applyTextStyles(text, styles) {
|
|
|
116
115
|
suffix: "</span>",
|
|
117
116
|
});
|
|
118
117
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
118
|
+
// Split on newlines so that style markers wrap each line individually.
|
|
119
|
+
// This prevents <br/> (inserted by table cell formatting) from being
|
|
120
|
+
// trapped inside markers like **bold<br/>text**.
|
|
121
|
+
const segments = result.split("\n");
|
|
122
|
+
const wrapped = segments.map((segment) => {
|
|
123
|
+
var _a;
|
|
124
|
+
if (!segment)
|
|
125
|
+
return segment;
|
|
126
|
+
let s = segment;
|
|
127
|
+
for (const wrapper of wrappers) {
|
|
128
|
+
const suffix = (_a = wrapper.suffix) !== null && _a !== void 0 ? _a : wrapper.prefix;
|
|
129
|
+
s = `${wrapper.prefix}${s}${suffix}`;
|
|
130
|
+
}
|
|
131
|
+
return s;
|
|
132
|
+
});
|
|
133
|
+
return wrapped.join("\n");
|
|
124
134
|
}
|
|
125
135
|
function inlineToMarkdown(content) {
|
|
126
136
|
if (!content || !Array.isArray(content)) {
|
|
@@ -269,7 +279,9 @@ function serializeBlock(block, ctx, orderedIndex, stepIndex) {
|
|
|
269
279
|
}
|
|
270
280
|
return flattenWithBlankLine(lines, true);
|
|
271
281
|
}
|
|
272
|
-
case "file":
|
|
282
|
+
case "file":
|
|
283
|
+
case "video":
|
|
284
|
+
case "audio": {
|
|
273
285
|
const url = block.props.url || "";
|
|
274
286
|
const name = block.props.name || "";
|
|
275
287
|
const caption = block.props.caption || "";
|
|
@@ -307,17 +319,17 @@ function serializeBlock(block, ctx, orderedIndex, stepIndex) {
|
|
|
307
319
|
}
|
|
308
320
|
return flattenWithBlankLine(lines, true);
|
|
309
321
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
322
|
+
const normalizedTitle = stepTitle
|
|
323
|
+
.split(/\r?\n/)
|
|
324
|
+
.map((segment) => segment.trim())
|
|
325
|
+
.filter((segment) => segment.length > 0)
|
|
326
|
+
.join(" ");
|
|
327
|
+
const normalizedExpectedForCheck = stripExpectedPrefix(expectedResult).trim();
|
|
328
|
+
const hasContent = stepData.length > 0 || normalizedExpectedForCheck.length > 0;
|
|
329
|
+
if (normalizedTitle.length > 0 || hasContent) {
|
|
330
|
+
const listStyle = (_m = block.props.listStyle) !== null && _m !== void 0 ? _m : "bullet";
|
|
331
|
+
const prefix = listStyle === "ordered" ? `${(stepIndex !== null && stepIndex !== void 0 ? stepIndex : 0) + 1}.` : "*";
|
|
332
|
+
lines.push(normalizedTitle.length > 0 ? `${prefix} ${normalizedTitle}` : `${prefix} `);
|
|
321
333
|
}
|
|
322
334
|
if (stepData.length > 0) {
|
|
323
335
|
const dataLines = stepData.split(/\r?\n/);
|
|
@@ -411,7 +423,11 @@ function serializeBlock(block, ctx, orderedIndex, stepIndex) {
|
|
|
411
423
|
return cellTexts;
|
|
412
424
|
};
|
|
413
425
|
const formattedRows = rows.map(normalizeRow);
|
|
414
|
-
const formatCell = (value) =>
|
|
426
|
+
const formatCell = (value) => {
|
|
427
|
+
if (!value.length)
|
|
428
|
+
return " ";
|
|
429
|
+
return value.replace(/\n/g, "<br/>");
|
|
430
|
+
};
|
|
415
431
|
const toAlignmentToken = (alignment) => {
|
|
416
432
|
switch (alignment) {
|
|
417
433
|
case "center":
|
|
@@ -571,6 +587,12 @@ function parseInlineMarkdown(text) {
|
|
|
571
587
|
continue;
|
|
572
588
|
}
|
|
573
589
|
}
|
|
590
|
+
const brMatch = cleaned.slice(i).match(/^<br\s*\/?\s*>/i);
|
|
591
|
+
if (brMatch) {
|
|
592
|
+
buffer += "\n";
|
|
593
|
+
i += brMatch[0].length;
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
574
596
|
buffer += cleaned[i];
|
|
575
597
|
i += 1;
|
|
576
598
|
}
|
|
@@ -594,7 +616,7 @@ function detectListType(trimmed) {
|
|
|
594
616
|
if (/^\d+[.)]\s+/.test(trimmed)) {
|
|
595
617
|
return "numbered";
|
|
596
618
|
}
|
|
597
|
-
if (/^[-*+]\s
|
|
619
|
+
if (/^[-*+](\s+|$)/.test(trimmed)) {
|
|
598
620
|
return "bullet";
|
|
599
621
|
}
|
|
600
622
|
return null;
|
|
@@ -683,7 +705,7 @@ function parseList(lines, startIndex, listType, indentLevel, allowEmptySteps = f
|
|
|
683
705
|
}
|
|
684
706
|
else {
|
|
685
707
|
const bulletMatch = trimmed.match(/^[-*+]\s+(.*)$/);
|
|
686
|
-
const text = (_d = bulletMatch === null || bulletMatch === void 0 ? void 0 : bulletMatch[1]) !== null && _d !== void 0 ? _d : trimmed.slice(2);
|
|
708
|
+
const text = (_d = bulletMatch === null || bulletMatch === void 0 ? void 0 : bulletMatch[1]) !== null && _d !== void 0 ? _d : (trimmed.length <= 1 ? "" : trimmed.slice(2));
|
|
687
709
|
items.push({
|
|
688
710
|
type: "bulletListItem",
|
|
689
711
|
props: cloneBaseProps(),
|
|
@@ -709,7 +731,7 @@ function isLikelyStep(lines, index) {
|
|
|
709
731
|
if (hasIndent)
|
|
710
732
|
return true;
|
|
711
733
|
// Stop at new list items, headings, or other block-level elements (only if not indented)
|
|
712
|
-
if (/^[-*+]\s/.test(trimmed) || /^\d+[.)]\s/.test(trimmed))
|
|
734
|
+
if (/^[-*+](\s|$)/.test(trimmed) || /^\d+[.)]\s/.test(trimmed))
|
|
713
735
|
break;
|
|
714
736
|
if (trimmed.startsWith("#") || trimmed.startsWith(">") || trimmed.startsWith("|") || trimmed.startsWith("```") || trimmed.startsWith(":::"))
|
|
715
737
|
break;
|
|
@@ -724,7 +746,7 @@ function isLikelyStep(lines, index) {
|
|
|
724
746
|
function parseTestStep(lines, index, allowEmpty = false, snippetId) {
|
|
725
747
|
const current = lines[index];
|
|
726
748
|
const trimmed = current.trim();
|
|
727
|
-
const isBullet = trimmed.startsWith("* ") || trimmed.startsWith("- ");
|
|
749
|
+
const isBullet = trimmed.startsWith("* ") || trimmed.startsWith("- ") || trimmed === "*" || trimmed === "-";
|
|
728
750
|
const isNumbered = /^\d+[.)]\s+/.test(trimmed);
|
|
729
751
|
if (!isBullet && !isNumbered) {
|
|
730
752
|
return null;
|
|
@@ -737,7 +759,7 @@ function parseTestStep(lines, index, allowEmpty = false, snippetId) {
|
|
|
737
759
|
}
|
|
738
760
|
let rawTitle;
|
|
739
761
|
if (isBullet) {
|
|
740
|
-
rawTitle = unescapeMarkdown(trimmed.slice(2)).trim();
|
|
762
|
+
rawTitle = unescapeMarkdown((trimmed.startsWith("* ") || trimmed.startsWith("- ")) ? trimmed.slice(2) : trimmed.slice(1)).trim();
|
|
741
763
|
}
|
|
742
764
|
else {
|
|
743
765
|
// For numbered lists, remove the number and delimiter
|
|
@@ -781,7 +803,7 @@ function parseTestStep(lines, index, allowEmpty = false, snippetId) {
|
|
|
781
803
|
continue;
|
|
782
804
|
}
|
|
783
805
|
const isNumberedStep = NUMBERED_STEP_REGEX.test(rawTrimmed);
|
|
784
|
-
const isNewStep = (!hasIndent && (rawTrimmed.startsWith("* ") || rawTrimmed.startsWith("- "))) ||
|
|
806
|
+
const isNewStep = (!hasIndent && (rawTrimmed.startsWith("* ") || rawTrimmed.startsWith("- ") || rawTrimmed === "*" || rawTrimmed === "-")) ||
|
|
785
807
|
(!hasIndent && isNumberedStep);
|
|
786
808
|
if (isNewStep) {
|
|
787
809
|
break;
|
package/package/styles.css
CHANGED
|
@@ -1074,7 +1074,8 @@ html.dark .bn-step-image-preview__content {
|
|
|
1074
1074
|
}
|
|
1075
1075
|
|
|
1076
1076
|
.bn-step-editor .overtype-wrapper .overtype-preview strong.step-preview-bold {
|
|
1077
|
-
|
|
1077
|
+
-webkit-text-stroke: 0.5px currentColor;
|
|
1078
|
+
font-weight: inherit !important;
|
|
1078
1079
|
color: inherit !important;
|
|
1079
1080
|
}
|
|
1080
1081
|
|
|
@@ -1083,6 +1084,12 @@ html.dark .bn-step-image-preview__content {
|
|
|
1083
1084
|
color: inherit !important;
|
|
1084
1085
|
}
|
|
1085
1086
|
|
|
1087
|
+
.bn-step-editor .overtype-wrapper .overtype-preview code.step-preview-code {
|
|
1088
|
+
background-color: rgba(135, 131, 120, 0.15) !important;
|
|
1089
|
+
border-radius: 3px !important;
|
|
1090
|
+
color: inherit !important;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1086
1093
|
.bn-step-custom-caret {
|
|
1087
1094
|
display: none;
|
|
1088
1095
|
position: absolute;
|
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -22,7 +22,6 @@ import { customSchema, type CustomEditor } from "./editor/customSchema";
|
|
|
22
22
|
import { setStepsFetcher, type StepJsonApiDocument } from "./editor/stepAutocomplete";
|
|
23
23
|
import { setSnippetFetcher, type SnippetJsonApiDocument } from "./editor/snippetAutocomplete";
|
|
24
24
|
import { setImageUploadHandler } from "./editor/stepImageUpload";
|
|
25
|
-
import { canInsertStepOrSnippet } from "./editor/blocks/step";
|
|
26
25
|
import "./App.css";
|
|
27
26
|
|
|
28
27
|
const focusStepField = (
|
|
@@ -335,11 +334,7 @@ function CustomSlashMenu() {
|
|
|
335
334
|
},
|
|
336
335
|
};
|
|
337
336
|
|
|
338
|
-
|
|
339
|
-
const canInsert = cursorBlock ? canInsertStepOrSnippet(editor, cursorBlock.id) : false;
|
|
340
|
-
const customItems = canInsert ? [stepItem, snippetItem] : [];
|
|
341
|
-
|
|
342
|
-
return filterSuggestionItems([...defaultItems, ...customItems], query);
|
|
337
|
+
return filterSuggestionItems([...defaultItems, stepItem, snippetItem], query);
|
|
343
338
|
};
|
|
344
339
|
|
|
345
340
|
return <SuggestionMenuController triggerCharacter="/" getItems={getItems} />;
|
|
@@ -489,7 +484,7 @@ function App() {
|
|
|
489
484
|
const fallbackBlock = documentBlocks[documentBlocks.length - 1];
|
|
490
485
|
const referenceId = selectedBlock?.id ?? fallbackBlock?.id;
|
|
491
486
|
|
|
492
|
-
if (!referenceId
|
|
487
|
+
if (!referenceId) {
|
|
493
488
|
return;
|
|
494
489
|
}
|
|
495
490
|
|