testomatio-editor-blocks 0.4.33 → 0.4.35
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/stepField.js +167 -34
- package/package/editor/customMarkdownConverter.d.ts +5 -1
- package/package/editor/customMarkdownConverter.js +20 -3
- package/package/index.d.ts +1 -1
- package/package/styles.css +8 -1
- package/package.json +1 -1
- package/src/editor/blocks/stepField.tsx +172 -34
- package/src/editor/customMarkdownConverter.test.ts +304 -0
- package/src/editor/customMarkdownConverter.ts +25 -3
- package/src/editor/styles.css +8 -1
- package/src/index.ts +1 -0
|
@@ -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();
|
|
@@ -5,5 +5,9 @@ export type CustomEditorBlock = Block<Schema["blockSchema"], Schema["inlineConte
|
|
|
5
5
|
export type CustomPartialBlock = PartialBlock<Schema["blockSchema"], Schema["inlineContentSchema"], Schema["styleSchema"]>;
|
|
6
6
|
export declare function blocksToMarkdown(blocks: CustomEditorBlock[]): string;
|
|
7
7
|
export declare function fixMalformedImageBlocks(blocks: CustomPartialBlock[]): CustomPartialBlock[];
|
|
8
|
-
export
|
|
8
|
+
export interface MarkdownToBlocksOptions {
|
|
9
|
+
/** When true, every blank line produces an empty paragraph block. */
|
|
10
|
+
preserveBlankLines?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function markdownToBlocks(markdown: string, options?: MarkdownToBlocksOptions): CustomPartialBlock[];
|
|
9
13
|
export {};
|
|
@@ -269,7 +269,9 @@ function serializeBlock(block, ctx, orderedIndex, stepIndex) {
|
|
|
269
269
|
}
|
|
270
270
|
return flattenWithBlankLine(lines, true);
|
|
271
271
|
}
|
|
272
|
-
case "file":
|
|
272
|
+
case "file":
|
|
273
|
+
case "video":
|
|
274
|
+
case "audio": {
|
|
273
275
|
const url = block.props.url || "";
|
|
274
276
|
const name = block.props.name || "";
|
|
275
277
|
const caption = block.props.caption || "";
|
|
@@ -411,7 +413,11 @@ function serializeBlock(block, ctx, orderedIndex, stepIndex) {
|
|
|
411
413
|
return cellTexts;
|
|
412
414
|
};
|
|
413
415
|
const formattedRows = rows.map(normalizeRow);
|
|
414
|
-
const formatCell = (value) =>
|
|
416
|
+
const formatCell = (value) => {
|
|
417
|
+
if (!value.length)
|
|
418
|
+
return " ";
|
|
419
|
+
return value.replace(/\n/g, "<br/>");
|
|
420
|
+
};
|
|
415
421
|
const toAlignmentToken = (alignment) => {
|
|
416
422
|
switch (alignment) {
|
|
417
423
|
case "center":
|
|
@@ -571,6 +577,12 @@ function parseInlineMarkdown(text) {
|
|
|
571
577
|
continue;
|
|
572
578
|
}
|
|
573
579
|
}
|
|
580
|
+
const brMatch = cleaned.slice(i).match(/^<br\s*\/?\s*>/i);
|
|
581
|
+
if (brMatch) {
|
|
582
|
+
buffer += "\n";
|
|
583
|
+
i += brMatch[0].length;
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
574
586
|
buffer += cleaned[i];
|
|
575
587
|
i += 1;
|
|
576
588
|
}
|
|
@@ -1147,7 +1159,7 @@ export function fixMalformedImageBlocks(blocks) {
|
|
|
1147
1159
|
}
|
|
1148
1160
|
return result;
|
|
1149
1161
|
}
|
|
1150
|
-
export function markdownToBlocks(markdown) {
|
|
1162
|
+
export function markdownToBlocks(markdown, options) {
|
|
1151
1163
|
var _a, _b, _c;
|
|
1152
1164
|
const normalized = markdown.replace(/\r\n/g, "\n");
|
|
1153
1165
|
const lines = normalized.split("\n");
|
|
@@ -1157,6 +1169,11 @@ export function markdownToBlocks(markdown) {
|
|
|
1157
1169
|
while (index < lines.length) {
|
|
1158
1170
|
const line = lines[index];
|
|
1159
1171
|
if (!line.trim()) {
|
|
1172
|
+
if (options === null || options === void 0 ? void 0 : options.preserveBlankLines) {
|
|
1173
|
+
blocks.push({ type: "paragraph", content: [], children: [] });
|
|
1174
|
+
index += 1;
|
|
1175
|
+
continue;
|
|
1176
|
+
}
|
|
1160
1177
|
index += 1;
|
|
1161
1178
|
// Count consecutive blank lines
|
|
1162
1179
|
let blankCount = 1;
|
package/package/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { customSchema, type CustomSchema, type CustomBlock, type CustomEditor, }
|
|
|
2
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
|
-
export { blocksToMarkdown, markdownToBlocks, type CustomEditorBlock, type CustomPartialBlock, } from "./editor/customMarkdownConverter";
|
|
5
|
+
export { blocksToMarkdown, markdownToBlocks, type CustomEditorBlock, type CustomPartialBlock, type MarkdownToBlocksOptions, } 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
8
|
export { setFileDisplayUrlResolver, resolveFileDisplayUrl, type FileDisplayUrlResolver, } from "./editor/fileDisplayUrl";
|
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
|
@@ -80,7 +80,7 @@ type ExtractedImage = {
|
|
|
80
80
|
};
|
|
81
81
|
|
|
82
82
|
type LinkMeta = { start: number; end: number; url: string };
|
|
83
|
-
type FormattingMeta = { start: number; end: number; type: "bold" | "italic" };
|
|
83
|
+
type FormattingMeta = { start: number; end: number; type: "bold" | "italic" | "code" };
|
|
84
84
|
|
|
85
85
|
|
|
86
86
|
function stripInlineMarkdown(markdown: string): {
|
|
@@ -207,6 +207,34 @@ function stripInlineMarkdown(markdown: string): {
|
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
// Code block: ```\n...\n``` (triple backticks with newlines)
|
|
211
|
+
if (markdown[i] === "`" && markdown[i + 1] === "`" && markdown[i + 2] === "`") {
|
|
212
|
+
const contentStart = markdown[i + 3] === "\n" ? i + 4 : i + 3;
|
|
213
|
+
const closeIdx = markdown.indexOf("```", contentStart);
|
|
214
|
+
if (closeIdx !== -1) {
|
|
215
|
+
const contentEnd = markdown[closeIdx - 1] === "\n" ? closeIdx - 1 : closeIdx;
|
|
216
|
+
const inner = markdown.slice(contentStart, contentEnd);
|
|
217
|
+
const start = plainText.length;
|
|
218
|
+
plainText += inner;
|
|
219
|
+
formatting.push({ start, end: plainText.length, type: "code" });
|
|
220
|
+
i = closeIdx + 3;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Inline code: `text`
|
|
226
|
+
if (markdown[i] === "`") {
|
|
227
|
+
const closeIdx = markdown.indexOf("`", i + 1);
|
|
228
|
+
if (closeIdx !== -1) {
|
|
229
|
+
const inner = markdown.slice(i + 1, closeIdx);
|
|
230
|
+
const start = plainText.length;
|
|
231
|
+
plainText += inner;
|
|
232
|
+
formatting.push({ start, end: plainText.length, type: "code" });
|
|
233
|
+
i = closeIdx + 1;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
210
238
|
plainText += markdown[i];
|
|
211
239
|
i++;
|
|
212
240
|
}
|
|
@@ -224,13 +252,23 @@ function buildFullMarkdown(plainText: string, links: LinkMeta[], formatting: For
|
|
|
224
252
|
const markers: Marker[] = [];
|
|
225
253
|
|
|
226
254
|
for (const fmt of formatting) {
|
|
227
|
-
|
|
255
|
+
let openMarker: string;
|
|
256
|
+
let closeMarker: string;
|
|
257
|
+
if (fmt.type === "code") {
|
|
258
|
+
const content = plainText.slice(fmt.start, fmt.end);
|
|
259
|
+
const isMultiline = content.includes("\n");
|
|
260
|
+
openMarker = isMultiline ? "```\n" : "`";
|
|
261
|
+
closeMarker = isMultiline ? "\n```" : "`";
|
|
262
|
+
} else {
|
|
263
|
+
openMarker = fmt.type === "bold" ? "**" : "*";
|
|
264
|
+
closeMarker = openMarker;
|
|
265
|
+
}
|
|
228
266
|
// Opening: outer markers (bold) before inner (italic) → bold order=0, italic order=1
|
|
229
267
|
// Closing: inner markers (italic) before outer (bold) → italic order=0, bold order=1
|
|
230
|
-
const openOrder = fmt.type === "bold" ? 0 : 1;
|
|
231
|
-
const closeOrder = fmt.type === "bold" ? 1 : 0;
|
|
232
|
-
markers.push({ pos: fmt.start, text:
|
|
233
|
-
markers.push({ pos: fmt.end, text:
|
|
268
|
+
const openOrder = fmt.type === "bold" ? 0 : fmt.type === "code" ? 2 : 1;
|
|
269
|
+
const closeOrder = fmt.type === "bold" ? 1 : fmt.type === "code" ? -1 : 0;
|
|
270
|
+
markers.push({ pos: fmt.start, text: openMarker, order: openOrder });
|
|
271
|
+
markers.push({ pos: fmt.end, text: closeMarker, order: closeOrder });
|
|
234
272
|
}
|
|
235
273
|
|
|
236
274
|
for (const link of links) {
|
|
@@ -265,7 +303,16 @@ function adjustFormattingForEdit(formatting: FormattingMeta[], editPos: number,
|
|
|
265
303
|
.filter((fmt) => fmt.end > fmt.start);
|
|
266
304
|
}
|
|
267
305
|
|
|
268
|
-
function getCaretRectInPreview(preview: HTMLElement, offset: number): { top: number; left: number; height: number } | null {
|
|
306
|
+
function getCaretRectInPreview(preview: HTMLElement, offset: number, textareaValue?: string): { top: number; left: number; height: number } | null {
|
|
307
|
+
// Convert textarea-space offset to preview-space (strip newlines)
|
|
308
|
+
let nlCount = 0;
|
|
309
|
+
if (textareaValue) {
|
|
310
|
+
for (let i = 0; i < offset && i < textareaValue.length; i++) {
|
|
311
|
+
if (textareaValue[i] === "\n") nlCount++;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const previewOffset = offset - nlCount;
|
|
315
|
+
|
|
269
316
|
const walker = document.createTreeWalker(preview, NodeFilter.SHOW_TEXT);
|
|
270
317
|
let currentOffset = 0;
|
|
271
318
|
|
|
@@ -273,8 +320,8 @@ function getCaretRectInPreview(preview: HTMLElement, offset: number): { top: num
|
|
|
273
320
|
const textNode = walker.currentNode as Text;
|
|
274
321
|
const nodeLen = textNode.length;
|
|
275
322
|
|
|
276
|
-
if (
|
|
277
|
-
const localOffset =
|
|
323
|
+
if (previewOffset <= currentOffset + nodeLen) {
|
|
324
|
+
const localOffset = previewOffset - currentOffset;
|
|
278
325
|
try {
|
|
279
326
|
const range = document.createRange();
|
|
280
327
|
range.setStart(textNode, localOffset);
|
|
@@ -297,7 +344,7 @@ function getCaretRectInPreview(preview: HTMLElement, offset: number): { top: num
|
|
|
297
344
|
return null;
|
|
298
345
|
}
|
|
299
346
|
|
|
300
|
-
function applyFormattingHighlights(preview: HTMLElement, formatting: FormattingMeta[]) {
|
|
347
|
+
function applyFormattingHighlights(preview: HTMLElement, formatting: FormattingMeta[], textareaValue?: string) {
|
|
301
348
|
if (formatting.length === 0) return;
|
|
302
349
|
|
|
303
350
|
// Remove previous formatting highlights
|
|
@@ -323,10 +370,41 @@ function applyFormattingHighlights(preview: HTMLElement, formatting: FormattingM
|
|
|
323
370
|
parent.removeChild(el);
|
|
324
371
|
}
|
|
325
372
|
}
|
|
373
|
+
const existingCode = preview.querySelectorAll("code.step-preview-code");
|
|
374
|
+
for (let i = 0; i < existingCode.length; i++) {
|
|
375
|
+
const el = existingCode[i];
|
|
376
|
+
const parent = el.parentNode;
|
|
377
|
+
if (parent) {
|
|
378
|
+
while (el.firstChild) {
|
|
379
|
+
parent.insertBefore(el.firstChild, el);
|
|
380
|
+
}
|
|
381
|
+
parent.removeChild(el);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// After unwrapping formatting elements, merge adjacent/empty text nodes
|
|
386
|
+
// so the tree walker sees clean text nodes matching the original structure.
|
|
387
|
+
preview.normalize();
|
|
388
|
+
|
|
389
|
+
// OverType splits textarea lines into <div> elements, discarding the \n
|
|
390
|
+
// characters. Convert textarea-space positions (with \n) to preview-space
|
|
391
|
+
// positions (without \n) so we can find the correct text nodes.
|
|
392
|
+
function taToPreview(taPos: number): number {
|
|
393
|
+
if (!textareaValue) return taPos;
|
|
394
|
+
let nlCount = 0;
|
|
395
|
+
for (let i = 0; i < taPos && i < textareaValue.length; i++) {
|
|
396
|
+
if (textareaValue[i] === "\n") nlCount++;
|
|
397
|
+
}
|
|
398
|
+
return taPos - nlCount;
|
|
399
|
+
}
|
|
326
400
|
|
|
327
401
|
const sorted = [...formatting].sort((a, b) => b.start - a.start);
|
|
328
402
|
|
|
329
403
|
for (const fmt of sorted) {
|
|
404
|
+
const pStart = taToPreview(fmt.start);
|
|
405
|
+
const pEnd = taToPreview(fmt.end);
|
|
406
|
+
|
|
407
|
+
// Collect text nodes with their preview-space offsets
|
|
330
408
|
const walker = document.createTreeWalker(preview, NodeFilter.SHOW_TEXT);
|
|
331
409
|
let currentOffset = 0;
|
|
332
410
|
let startNode: Text | null = null;
|
|
@@ -339,13 +417,13 @@ function applyFormattingHighlights(preview: HTMLElement, formatting: FormattingM
|
|
|
339
417
|
const nodeStart = currentOffset;
|
|
340
418
|
const nodeEnd = currentOffset + textNode.length;
|
|
341
419
|
|
|
342
|
-
if (!startNode &&
|
|
420
|
+
if (!startNode && pStart >= nodeStart && pStart < nodeEnd) {
|
|
343
421
|
startNode = textNode;
|
|
344
|
-
startLocalOffset =
|
|
422
|
+
startLocalOffset = pStart - nodeStart;
|
|
345
423
|
}
|
|
346
|
-
if (!endNode &&
|
|
424
|
+
if (!endNode && pEnd > nodeStart && pEnd <= nodeEnd) {
|
|
347
425
|
endNode = textNode;
|
|
348
|
-
endLocalOffset =
|
|
426
|
+
endLocalOffset = pEnd - nodeStart;
|
|
349
427
|
}
|
|
350
428
|
|
|
351
429
|
currentOffset = nodeEnd;
|
|
@@ -354,19 +432,58 @@ function applyFormattingHighlights(preview: HTMLElement, formatting: FormattingM
|
|
|
354
432
|
|
|
355
433
|
if (!startNode || !endNode) continue;
|
|
356
434
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
range.setStart(startNode, startLocalOffset);
|
|
360
|
-
range.setEnd(endNode, endLocalOffset);
|
|
435
|
+
const tagName = fmt.type === "bold" ? "strong" : fmt.type === "code" ? "code" : "em";
|
|
436
|
+
const className = fmt.type === "bold" ? "step-preview-bold" : fmt.type === "code" ? "step-preview-code" : "step-preview-italic";
|
|
361
437
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
438
|
+
// If start and end are in the same text node, wrap directly
|
|
439
|
+
if (startNode === endNode) {
|
|
440
|
+
try {
|
|
441
|
+
const range = document.createRange();
|
|
442
|
+
range.setStart(startNode, startLocalOffset);
|
|
443
|
+
range.setEnd(endNode, endLocalOffset);
|
|
444
|
+
const wrapper = document.createElement(tagName);
|
|
445
|
+
wrapper.className = className;
|
|
446
|
+
const fragment = range.extractContents();
|
|
447
|
+
wrapper.appendChild(fragment);
|
|
448
|
+
range.insertNode(wrapper);
|
|
449
|
+
} catch {
|
|
450
|
+
// DOM manipulation can fail if range crosses element boundaries
|
|
451
|
+
}
|
|
452
|
+
} else {
|
|
453
|
+
// Multi-node range (e.g. code spanning multiple lines/divs):
|
|
454
|
+
// collect all text nodes in the range, then wrap each one individually
|
|
455
|
+
const textNodes: { node: Text; localStart: number; localEnd: number }[] = [];
|
|
456
|
+
const walker2 = document.createTreeWalker(preview, NodeFilter.SHOW_TEXT);
|
|
457
|
+
let collecting = false;
|
|
458
|
+
while (walker2.nextNode()) {
|
|
459
|
+
const tn = walker2.currentNode as Text;
|
|
460
|
+
if (tn === startNode) {
|
|
461
|
+
collecting = true;
|
|
462
|
+
textNodes.push({ node: tn, localStart: startLocalOffset, localEnd: tn.length });
|
|
463
|
+
} else if (tn === endNode) {
|
|
464
|
+
textNodes.push({ node: tn, localStart: 0, localEnd: endLocalOffset });
|
|
465
|
+
break;
|
|
466
|
+
} else if (collecting) {
|
|
467
|
+
textNodes.push({ node: tn, localStart: 0, localEnd: tn.length });
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
// Wrap in reverse order to preserve offsets
|
|
471
|
+
for (let ti = textNodes.length - 1; ti >= 0; ti--) {
|
|
472
|
+
const { node, localStart, localEnd } = textNodes[ti];
|
|
473
|
+
if (localStart >= localEnd) continue;
|
|
474
|
+
try {
|
|
475
|
+
const range = document.createRange();
|
|
476
|
+
range.setStart(node, localStart);
|
|
477
|
+
range.setEnd(node, localEnd);
|
|
478
|
+
const wrapper = document.createElement(tagName);
|
|
479
|
+
wrapper.className = className;
|
|
480
|
+
const fragment = range.extractContents();
|
|
481
|
+
wrapper.appendChild(fragment);
|
|
482
|
+
range.insertNode(wrapper);
|
|
483
|
+
} catch {
|
|
484
|
+
// skip nodes that can't be wrapped
|
|
485
|
+
}
|
|
486
|
+
}
|
|
370
487
|
}
|
|
371
488
|
}
|
|
372
489
|
}
|
|
@@ -576,11 +693,11 @@ export function StepField({
|
|
|
576
693
|
const originalUpdatePreview = instance.updatePreview.bind(instance);
|
|
577
694
|
instance.updatePreview = function () {
|
|
578
695
|
originalUpdatePreview();
|
|
579
|
-
applyFormattingHighlights(this.preview, formattingRef.current);
|
|
696
|
+
applyFormattingHighlights(this.preview, formattingRef.current, this.textarea?.value);
|
|
580
697
|
applyLinkHighlights(this.preview, linksRef.current);
|
|
581
698
|
};
|
|
582
699
|
// Apply initial highlights
|
|
583
|
-
applyFormattingHighlights(instance.preview, formattingRef.current);
|
|
700
|
+
applyFormattingHighlights(instance.preview, formattingRef.current, instance.textarea?.value);
|
|
584
701
|
applyLinkHighlights(instance.preview, linksRef.current);
|
|
585
702
|
|
|
586
703
|
// Create custom caret element inside the wrapper
|
|
@@ -633,7 +750,7 @@ export function StepField({
|
|
|
633
750
|
return;
|
|
634
751
|
}
|
|
635
752
|
|
|
636
|
-
const rect = getCaretRectInPreview(instance.preview, pos);
|
|
753
|
+
const rect = getCaretRectInPreview(instance.preview, pos, instance.textarea?.value);
|
|
637
754
|
if (rect) {
|
|
638
755
|
caret.style.display = "block";
|
|
639
756
|
caret.style.top = `${rect.top}px`;
|
|
@@ -712,7 +829,7 @@ export function StepField({
|
|
|
712
829
|
isSyncingRef.current = false;
|
|
713
830
|
} else {
|
|
714
831
|
// Even if text didn't change, formatting/links might have — re-apply highlights
|
|
715
|
-
applyFormattingHighlights(instance.preview, formatting);
|
|
832
|
+
applyFormattingHighlights(instance.preview, formatting, instance.textarea?.value);
|
|
716
833
|
applyLinkHighlights(instance.preview, links);
|
|
717
834
|
}
|
|
718
835
|
|
|
@@ -899,14 +1016,14 @@ export function StepField({
|
|
|
899
1016
|
}, [enableImageUpload, insertImageMarkdown, onImageFile, textareaNode, uploadImage]);
|
|
900
1017
|
|
|
901
1018
|
const handleToolbarAction = useCallback(
|
|
902
|
-
(action: "toggleBold" | "toggleItalic") => {
|
|
1019
|
+
(action: "toggleBold" | "toggleItalic" | "toggleCode") => {
|
|
903
1020
|
const instance = editorInstanceRef.current;
|
|
904
1021
|
if (!textareaNode || !instance) {
|
|
905
1022
|
return;
|
|
906
1023
|
}
|
|
907
1024
|
textareaNode.focus();
|
|
908
1025
|
|
|
909
|
-
const fmtType: "bold" | "italic" = action === "toggleBold" ? "bold" : "italic";
|
|
1026
|
+
const fmtType: "bold" | "italic" | "code" = action === "toggleBold" ? "bold" : action === "toggleCode" ? "code" : "italic";
|
|
910
1027
|
const start = textareaNode.selectionStart ?? 0;
|
|
911
1028
|
const end = textareaNode.selectionEnd ?? 0;
|
|
912
1029
|
|
|
@@ -934,7 +1051,7 @@ export function StepField({
|
|
|
934
1051
|
setPlainTextValue(markdownToPlainText(markdown));
|
|
935
1052
|
|
|
936
1053
|
// Re-apply highlights
|
|
937
|
-
applyFormattingHighlights(instance.preview, formattingRef.current);
|
|
1054
|
+
applyFormattingHighlights(instance.preview, formattingRef.current, textareaNode?.value);
|
|
938
1055
|
applyLinkHighlights(instance.preview, linksRef.current);
|
|
939
1056
|
},
|
|
940
1057
|
[textareaNode],
|
|
@@ -1022,7 +1139,7 @@ export function StepField({
|
|
|
1022
1139
|
const markdown = buildFullMarkdown(instance.getValue(), linksRef.current, formattingRef.current);
|
|
1023
1140
|
onChangeRef.current?.(markdown);
|
|
1024
1141
|
// Re-apply highlights since links changed
|
|
1025
|
-
applyFormattingHighlights(instance.preview, formattingRef.current);
|
|
1142
|
+
applyFormattingHighlights(instance.preview, formattingRef.current, instance.textarea?.value);
|
|
1026
1143
|
applyLinkHighlights(instance.preview, linksRef.current);
|
|
1027
1144
|
}
|
|
1028
1145
|
}, [cursorLink]);
|
|
@@ -1187,6 +1304,12 @@ export function StepField({
|
|
|
1187
1304
|
handleToolbarAction("toggleItalic");
|
|
1188
1305
|
return;
|
|
1189
1306
|
}
|
|
1307
|
+
if (event.key === "e" || event.key === "E") {
|
|
1308
|
+
event.preventDefault();
|
|
1309
|
+
event.stopImmediatePropagation();
|
|
1310
|
+
handleToolbarAction("toggleCode");
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1190
1313
|
}
|
|
1191
1314
|
|
|
1192
1315
|
if (enableAutocomplete && shouldShowAutocomplete) {
|
|
@@ -1394,6 +1517,21 @@ export function StepField({
|
|
|
1394
1517
|
<path d="M8.66699 13.3334H4.66699V12.0001H5.95166L8.69566 4.00008H7.33366V2.66675H11.3337V4.00008H10.049L7.30499 12.0001H8.66699V13.3334Z" fill="currentColor"/>
|
|
1395
1518
|
</svg>
|
|
1396
1519
|
</button>
|
|
1520
|
+
<button
|
|
1521
|
+
type="button"
|
|
1522
|
+
className="bn-step-toolbar__button"
|
|
1523
|
+
data-tooltip="Code"
|
|
1524
|
+
onMouseDown={(event) => {
|
|
1525
|
+
event.preventDefault();
|
|
1526
|
+
handleToolbarAction("toggleCode");
|
|
1527
|
+
}}
|
|
1528
|
+
aria-label="Code"
|
|
1529
|
+
tabIndex={-1}
|
|
1530
|
+
>
|
|
1531
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
|
1532
|
+
<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"/>
|
|
1533
|
+
</svg>
|
|
1534
|
+
</button>
|
|
1397
1535
|
</>
|
|
1398
1536
|
)}
|
|
1399
1537
|
{enableImageUpload && uploadImage && showImageButton && (
|
|
@@ -328,6 +328,60 @@ describe("blocksToMarkdown", () => {
|
|
|
328
328
|
);
|
|
329
329
|
});
|
|
330
330
|
|
|
331
|
+
it("serializes table cells containing newlines as <br/>", () => {
|
|
332
|
+
const blocks: CustomEditorBlock[] = [
|
|
333
|
+
{
|
|
334
|
+
id: "tbl2",
|
|
335
|
+
type: "table",
|
|
336
|
+
props: { textColor: "default" },
|
|
337
|
+
content: {
|
|
338
|
+
type: "tableContent",
|
|
339
|
+
columnWidths: [undefined, undefined],
|
|
340
|
+
headerRows: 1,
|
|
341
|
+
rows: [
|
|
342
|
+
{
|
|
343
|
+
cells: [
|
|
344
|
+
{
|
|
345
|
+
type: "tableCell",
|
|
346
|
+
props: cellProps,
|
|
347
|
+
content: [{ type: "text", text: "Steps", styles: {} }],
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
type: "tableCell",
|
|
351
|
+
props: cellProps,
|
|
352
|
+
content: [{ type: "text", text: "Expected Results", styles: {} }],
|
|
353
|
+
},
|
|
354
|
+
],
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
cells: [
|
|
358
|
+
{
|
|
359
|
+
type: "tableCell",
|
|
360
|
+
props: cellProps,
|
|
361
|
+
content: [{ type: "text", text: "line1\nline2", styles: {} }],
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
type: "tableCell",
|
|
365
|
+
props: cellProps,
|
|
366
|
+
content: [{ type: "text", text: "ok", styles: {} }],
|
|
367
|
+
},
|
|
368
|
+
],
|
|
369
|
+
},
|
|
370
|
+
],
|
|
371
|
+
},
|
|
372
|
+
children: [],
|
|
373
|
+
},
|
|
374
|
+
];
|
|
375
|
+
|
|
376
|
+
expect(blocksToMarkdown(blocks)).toBe(
|
|
377
|
+
[
|
|
378
|
+
"| Steps | Expected Results |",
|
|
379
|
+
"| --- | --- |",
|
|
380
|
+
"| line1<br/>line2 | ok |",
|
|
381
|
+
].join("\n"),
|
|
382
|
+
);
|
|
383
|
+
});
|
|
384
|
+
|
|
331
385
|
it("parses a test step with inline image in the title, moving the image to step data", () => {
|
|
332
386
|
const markdown = [
|
|
333
387
|
"## Steps",
|
|
@@ -995,6 +1049,105 @@ describe("markdownToBlocks", () => {
|
|
|
995
1049
|
});
|
|
996
1050
|
});
|
|
997
1051
|
|
|
1052
|
+
it("parses all bullet items as steps when blank line follows Steps heading", () => {
|
|
1053
|
+
const markdown = [
|
|
1054
|
+
"### Requirements",
|
|
1055
|
+
"",
|
|
1056
|
+
"### Steps",
|
|
1057
|
+
"",
|
|
1058
|
+
"* next",
|
|
1059
|
+
" *Expected*: expected",
|
|
1060
|
+
"* next 22",
|
|
1061
|
+
"* next 3",
|
|
1062
|
+
].join("\n");
|
|
1063
|
+
|
|
1064
|
+
const blocks = markdownToBlocks(markdown);
|
|
1065
|
+
const stepBlocks = blocks.filter((block) => block.type === "testStep");
|
|
1066
|
+
|
|
1067
|
+
expect(stepBlocks).toHaveLength(3);
|
|
1068
|
+
expect(stepBlocks[0].props).toMatchObject({ stepTitle: "next", expectedResult: "expected" });
|
|
1069
|
+
expect(stepBlocks[1].props).toMatchObject({ stepTitle: "next 22" });
|
|
1070
|
+
expect(stepBlocks[2].props).toMatchObject({ stepTitle: "next 3" });
|
|
1071
|
+
|
|
1072
|
+
// Ensure no bullet list items leaked
|
|
1073
|
+
const bulletBlocks = blocks.filter((block) => block.type === "bulletListItem");
|
|
1074
|
+
expect(bulletBlocks).toHaveLength(0);
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
it("parses all bullet items as steps WITHOUT blank line after Steps heading", () => {
|
|
1078
|
+
const markdown = [
|
|
1079
|
+
"### Requirements",
|
|
1080
|
+
"",
|
|
1081
|
+
"### Steps",
|
|
1082
|
+
"* next",
|
|
1083
|
+
" *Expected*: expected",
|
|
1084
|
+
"* next 22",
|
|
1085
|
+
"* next 3",
|
|
1086
|
+
].join("\n");
|
|
1087
|
+
|
|
1088
|
+
const blocks = markdownToBlocks(markdown);
|
|
1089
|
+
const stepBlocks = blocks.filter((block) => block.type === "testStep");
|
|
1090
|
+
|
|
1091
|
+
expect(stepBlocks).toHaveLength(3);
|
|
1092
|
+
expect(stepBlocks[0].props).toMatchObject({ stepTitle: "next", expectedResult: "expected" });
|
|
1093
|
+
expect(stepBlocks[1].props).toMatchObject({ stepTitle: "next 22" });
|
|
1094
|
+
expect(stepBlocks[2].props).toMatchObject({ stepTitle: "next 3" });
|
|
1095
|
+
|
|
1096
|
+
const bulletBlocks = blocks.filter((block) => block.type === "bulletListItem");
|
|
1097
|
+
expect(bulletBlocks).toHaveLength(0);
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
it("round-trips steps with blank line after Steps heading", () => {
|
|
1101
|
+
const markdown = [
|
|
1102
|
+
"### Requirements",
|
|
1103
|
+
"",
|
|
1104
|
+
"### Steps",
|
|
1105
|
+
"",
|
|
1106
|
+
"* next",
|
|
1107
|
+
" *Expected*: expected",
|
|
1108
|
+
"* next 22",
|
|
1109
|
+
"* next 3",
|
|
1110
|
+
].join("\n");
|
|
1111
|
+
|
|
1112
|
+
const blocks = markdownToBlocks(markdown);
|
|
1113
|
+
const md = blocksToMarkdown(blocks as CustomEditorBlock[]);
|
|
1114
|
+
const blocks2 = markdownToBlocks(md);
|
|
1115
|
+
const stepBlocks2 = blocks2.filter((block) => block.type === "testStep");
|
|
1116
|
+
expect(stepBlocks2).toHaveLength(3);
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
it("preserveBlankLines: creates empty paragraphs for each blank line", () => {
|
|
1120
|
+
const markdown = [
|
|
1121
|
+
"### Requirements",
|
|
1122
|
+
"",
|
|
1123
|
+
"### Steps",
|
|
1124
|
+
"",
|
|
1125
|
+
"* next",
|
|
1126
|
+
" *Expected*: expected",
|
|
1127
|
+
"* next 22",
|
|
1128
|
+
"* next 3",
|
|
1129
|
+
].join("\n");
|
|
1130
|
+
|
|
1131
|
+
const blocks = markdownToBlocks(markdown, { preserveBlankLines: true });
|
|
1132
|
+
const stepBlocks = blocks.filter((block) => block.type === "testStep");
|
|
1133
|
+
const emptyParas = blocks.filter(
|
|
1134
|
+
(block) => block.type === "paragraph" && (!block.content || (block.content as any[]).length === 0),
|
|
1135
|
+
);
|
|
1136
|
+
|
|
1137
|
+
// All 3 items should be test steps
|
|
1138
|
+
expect(stepBlocks).toHaveLength(3);
|
|
1139
|
+
expect(stepBlocks[0].props).toMatchObject({ stepTitle: "next" });
|
|
1140
|
+
expect(stepBlocks[1].props).toMatchObject({ stepTitle: "next 22" });
|
|
1141
|
+
expect(stepBlocks[2].props).toMatchObject({ stepTitle: "next 3" });
|
|
1142
|
+
|
|
1143
|
+
// Each blank line should produce an empty paragraph
|
|
1144
|
+
expect(emptyParas.length).toBeGreaterThanOrEqual(2);
|
|
1145
|
+
|
|
1146
|
+
// No bullet list items
|
|
1147
|
+
const bulletBlocks = blocks.filter((block) => block.type === "bulletListItem");
|
|
1148
|
+
expect(bulletBlocks).toHaveLength(0);
|
|
1149
|
+
});
|
|
1150
|
+
|
|
998
1151
|
it("round-trips simple blocks", () => {
|
|
999
1152
|
const blocks: CustomEditorBlock[] = [
|
|
1000
1153
|
{
|
|
@@ -1113,6 +1266,114 @@ describe("markdownToBlocks", () => {
|
|
|
1113
1266
|
]);
|
|
1114
1267
|
});
|
|
1115
1268
|
|
|
1269
|
+
it("parses <br/> in table cells back to newline", () => {
|
|
1270
|
+
const markdown = [
|
|
1271
|
+
"| A | B |",
|
|
1272
|
+
"| --- | --- |",
|
|
1273
|
+
"| line1<br/>line2 | ok |",
|
|
1274
|
+
].join("\n");
|
|
1275
|
+
|
|
1276
|
+
const blocks = markdownToBlocks(markdown);
|
|
1277
|
+
expect(blocks).toEqual([
|
|
1278
|
+
{
|
|
1279
|
+
type: "table",
|
|
1280
|
+
props: { textColor: "default" },
|
|
1281
|
+
content: {
|
|
1282
|
+
type: "tableContent",
|
|
1283
|
+
columnWidths: [undefined, undefined],
|
|
1284
|
+
headerRows: 1,
|
|
1285
|
+
rows: [
|
|
1286
|
+
{
|
|
1287
|
+
cells: [
|
|
1288
|
+
{
|
|
1289
|
+
type: "tableCell",
|
|
1290
|
+
props: cellProps,
|
|
1291
|
+
content: [{ type: "text", text: "A", styles: {} }],
|
|
1292
|
+
},
|
|
1293
|
+
{
|
|
1294
|
+
type: "tableCell",
|
|
1295
|
+
props: cellProps,
|
|
1296
|
+
content: [{ type: "text", text: "B", styles: {} }],
|
|
1297
|
+
},
|
|
1298
|
+
],
|
|
1299
|
+
},
|
|
1300
|
+
{
|
|
1301
|
+
cells: [
|
|
1302
|
+
{
|
|
1303
|
+
type: "tableCell",
|
|
1304
|
+
props: cellProps,
|
|
1305
|
+
content: [{ type: "text", text: "line1\nline2", styles: {} }],
|
|
1306
|
+
},
|
|
1307
|
+
{
|
|
1308
|
+
type: "tableCell",
|
|
1309
|
+
props: cellProps,
|
|
1310
|
+
content: [{ type: "text", text: "ok", styles: {} }],
|
|
1311
|
+
},
|
|
1312
|
+
],
|
|
1313
|
+
},
|
|
1314
|
+
],
|
|
1315
|
+
},
|
|
1316
|
+
children: [],
|
|
1317
|
+
},
|
|
1318
|
+
]);
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1321
|
+
it("round-trips newlines in table cells", () => {
|
|
1322
|
+
const blocks: CustomEditorBlock[] = [
|
|
1323
|
+
{
|
|
1324
|
+
id: "tbl3",
|
|
1325
|
+
type: "table",
|
|
1326
|
+
props: { textColor: "default" },
|
|
1327
|
+
content: {
|
|
1328
|
+
type: "tableContent",
|
|
1329
|
+
columnWidths: [undefined, undefined],
|
|
1330
|
+
headerRows: 1,
|
|
1331
|
+
rows: [
|
|
1332
|
+
{
|
|
1333
|
+
cells: [
|
|
1334
|
+
{
|
|
1335
|
+
type: "tableCell",
|
|
1336
|
+
props: cellProps,
|
|
1337
|
+
content: [{ type: "text", text: "Header", styles: {} }],
|
|
1338
|
+
},
|
|
1339
|
+
{
|
|
1340
|
+
type: "tableCell",
|
|
1341
|
+
props: cellProps,
|
|
1342
|
+
content: [{ type: "text", text: "Info", styles: {} }],
|
|
1343
|
+
},
|
|
1344
|
+
],
|
|
1345
|
+
},
|
|
1346
|
+
{
|
|
1347
|
+
cells: [
|
|
1348
|
+
{
|
|
1349
|
+
type: "tableCell",
|
|
1350
|
+
props: cellProps,
|
|
1351
|
+
content: [{ type: "text", text: "first\nsecond\nthird", styles: {} }],
|
|
1352
|
+
},
|
|
1353
|
+
{
|
|
1354
|
+
type: "tableCell",
|
|
1355
|
+
props: cellProps,
|
|
1356
|
+
content: [{ type: "text", text: "value", styles: {} }],
|
|
1357
|
+
},
|
|
1358
|
+
],
|
|
1359
|
+
},
|
|
1360
|
+
],
|
|
1361
|
+
},
|
|
1362
|
+
children: [],
|
|
1363
|
+
},
|
|
1364
|
+
];
|
|
1365
|
+
|
|
1366
|
+
const markdown = blocksToMarkdown(blocks);
|
|
1367
|
+
expect(markdown).toContain("first<br/>second<br/>third");
|
|
1368
|
+
|
|
1369
|
+
const parsed = markdownToBlocks(markdown);
|
|
1370
|
+
const row = (parsed[0] as any).content.rows[1];
|
|
1371
|
+
const cellContent = row.cells[0].content;
|
|
1372
|
+
expect(cellContent).toEqual([
|
|
1373
|
+
{ type: "text", text: "first\nsecond\nthird", styles: {} },
|
|
1374
|
+
]);
|
|
1375
|
+
});
|
|
1376
|
+
|
|
1116
1377
|
it("parses expected result lines written with bold 'Expected Result' prefix for compatibility", () => {
|
|
1117
1378
|
const markdown = [
|
|
1118
1379
|
"* Step 1: Send a chat message to the user.",
|
|
@@ -1724,3 +1985,46 @@ describe("file block parsing", () => {
|
|
|
1724
1985
|
expect(md).toBe(markdown);
|
|
1725
1986
|
});
|
|
1726
1987
|
});
|
|
1988
|
+
|
|
1989
|
+
describe("video/audio block serialization", () => {
|
|
1990
|
+
it("serializes a video block using the file format", () => {
|
|
1991
|
+
const blocks: CustomEditorBlock[] = [
|
|
1992
|
+
{
|
|
1993
|
+
id: "1",
|
|
1994
|
+
type: "video",
|
|
1995
|
+
props: {
|
|
1996
|
+
...baseProps,
|
|
1997
|
+
url: "https://example.com/video.mp4",
|
|
1998
|
+
name: "recording.mp4",
|
|
1999
|
+
caption: "/images/file-type-icons/mp4.svg",
|
|
2000
|
+
showPreview: true,
|
|
2001
|
+
previewWidth: 512,
|
|
2002
|
+
},
|
|
2003
|
+
content: undefined as any,
|
|
2004
|
+
children: [],
|
|
2005
|
+
},
|
|
2006
|
+
];
|
|
2007
|
+
const md = blocksToMarkdown(blocks);
|
|
2008
|
+
expect(md).toBe("[](https://example.com/video.mp4)");
|
|
2009
|
+
});
|
|
2010
|
+
|
|
2011
|
+
it("serializes an audio block using the file format", () => {
|
|
2012
|
+
const blocks: CustomEditorBlock[] = [
|
|
2013
|
+
{
|
|
2014
|
+
id: "1",
|
|
2015
|
+
type: "audio",
|
|
2016
|
+
props: {
|
|
2017
|
+
...baseProps,
|
|
2018
|
+
url: "https://example.com/sound.mp3",
|
|
2019
|
+
name: "sound.mp3",
|
|
2020
|
+
caption: "/images/file-type-icons/file.svg",
|
|
2021
|
+
showPreview: true,
|
|
2022
|
+
},
|
|
2023
|
+
content: undefined as any,
|
|
2024
|
+
children: [],
|
|
2025
|
+
},
|
|
2026
|
+
];
|
|
2027
|
+
const md = blocksToMarkdown(blocks);
|
|
2028
|
+
expect(md).toBe("[](https://example.com/sound.mp3)");
|
|
2029
|
+
});
|
|
2030
|
+
});
|
|
@@ -347,7 +347,9 @@ function serializeBlock(
|
|
|
347
347
|
}
|
|
348
348
|
return flattenWithBlankLine(lines, true);
|
|
349
349
|
}
|
|
350
|
-
case "file":
|
|
350
|
+
case "file":
|
|
351
|
+
case "video":
|
|
352
|
+
case "audio": {
|
|
351
353
|
const url = (block.props as any).url || "";
|
|
352
354
|
const name = (block.props as any).name || "";
|
|
353
355
|
const caption = (block.props as any).caption || "";
|
|
@@ -511,7 +513,10 @@ function serializeBlock(
|
|
|
511
513
|
};
|
|
512
514
|
|
|
513
515
|
const formattedRows = rows.map(normalizeRow);
|
|
514
|
-
const formatCell = (value: string) =>
|
|
516
|
+
const formatCell = (value: string) => {
|
|
517
|
+
if (!value.length) return " ";
|
|
518
|
+
return value.replace(/\n/g, "<br/>");
|
|
519
|
+
};
|
|
515
520
|
const toAlignmentToken = (alignment: string) => {
|
|
516
521
|
switch (alignment) {
|
|
517
522
|
case "center":
|
|
@@ -694,6 +699,13 @@ function parseInlineMarkdown(text: string): EditorInline[] {
|
|
|
694
699
|
}
|
|
695
700
|
}
|
|
696
701
|
|
|
702
|
+
const brMatch = cleaned.slice(i).match(/^<br\s*\/?\s*>/i);
|
|
703
|
+
if (brMatch) {
|
|
704
|
+
buffer += "\n";
|
|
705
|
+
i += brMatch[0].length;
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
|
|
697
709
|
buffer += cleaned[i];
|
|
698
710
|
i += 1;
|
|
699
711
|
}
|
|
@@ -1372,7 +1384,12 @@ export function fixMalformedImageBlocks(blocks: CustomPartialBlock[]): CustomPar
|
|
|
1372
1384
|
return result;
|
|
1373
1385
|
}
|
|
1374
1386
|
|
|
1375
|
-
export
|
|
1387
|
+
export interface MarkdownToBlocksOptions {
|
|
1388
|
+
/** When true, every blank line produces an empty paragraph block. */
|
|
1389
|
+
preserveBlankLines?: boolean;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
export function markdownToBlocks(markdown: string, options?: MarkdownToBlocksOptions): CustomPartialBlock[] {
|
|
1376
1393
|
const normalized = markdown.replace(/\r\n/g, "\n");
|
|
1377
1394
|
const lines = normalized.split("\n");
|
|
1378
1395
|
const blocks: CustomPartialBlock[] = [];
|
|
@@ -1382,6 +1399,11 @@ export function markdownToBlocks(markdown: string): CustomPartialBlock[] {
|
|
|
1382
1399
|
while (index < lines.length) {
|
|
1383
1400
|
const line = lines[index];
|
|
1384
1401
|
if (!line.trim()) {
|
|
1402
|
+
if (options?.preserveBlankLines) {
|
|
1403
|
+
blocks.push({ type: "paragraph", content: [], children: [] } as CustomPartialBlock);
|
|
1404
|
+
index += 1;
|
|
1405
|
+
continue;
|
|
1406
|
+
}
|
|
1385
1407
|
index += 1;
|
|
1386
1408
|
// Count consecutive blank lines
|
|
1387
1409
|
let blankCount = 1;
|
package/src/editor/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;
|