testomatio-editor-blocks 0.4.34 → 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.
@@ -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
- const marker = fmt.type === "bold" ? "**" : "*";
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: marker, order: openOrder });
147
- markers.push({ pos: fmt.end, text: marker, order: closeOrder });
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 (offset <= currentOffset + nodeLen) {
183
- const localOffset = offset - currentOffset;
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 && fmt.start >= nodeStart && fmt.start < nodeEnd) {
318
+ if (!startNode && pStart >= nodeStart && pStart < nodeEnd) {
243
319
  startNode = textNode;
244
- startLocalOffset = fmt.start - nodeStart;
320
+ startLocalOffset = pStart - nodeStart;
245
321
  }
246
- if (!endNode && fmt.end > nodeStart && fmt.end <= nodeEnd) {
322
+ if (!endNode && pEnd > nodeStart && pEnd <= nodeEnd) {
247
323
  endNode = textNode;
248
- endLocalOffset = fmt.end - nodeStart;
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
- try {
257
- const range = document.createRange();
258
- range.setStart(startNode, startLocalOffset);
259
- range.setEnd(endNode, endLocalOffset);
260
- const wrapper = document.createElement(fmt.type === "bold" ? "strong" : "em");
261
- wrapper.className = fmt.type === "bold" ? "step-preview-bold" : "step-preview-italic";
262
- const fragment = range.extractContents();
263
- wrapper.appendChild(fragment);
264
- range.insertNode(wrapper);
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
- catch {
267
- // DOM manipulation can fail if range crosses element boundaries
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" }) }) })] })), enableImageUpload && uploadImage && showImageButton && (_jsx("button", { type: "button", className: "bn-step-toolbar__button", "data-tooltip": "Insert image", onMouseDown: (event) => {
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();
@@ -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) => (value.length ? 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
  }
@@ -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
- font-weight: bold !important;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testomatio-editor-blocks",
3
- "version": "0.4.34",
3
+ "version": "0.4.35",
4
4
  "description": "Custom BlockNote schema, markdown conversion helpers, and UI for Testomatio-style test cases and steps.",
5
5
  "type": "module",
6
6
  "main": "./package/index.js",
@@ -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
- const marker = fmt.type === "bold" ? "**" : "*";
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: marker, order: openOrder });
233
- markers.push({ pos: fmt.end, text: marker, order: closeOrder });
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 (offset <= currentOffset + nodeLen) {
277
- const localOffset = offset - currentOffset;
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 && fmt.start >= nodeStart && fmt.start < nodeEnd) {
420
+ if (!startNode && pStart >= nodeStart && pStart < nodeEnd) {
343
421
  startNode = textNode;
344
- startLocalOffset = fmt.start - nodeStart;
422
+ startLocalOffset = pStart - nodeStart;
345
423
  }
346
- if (!endNode && fmt.end > nodeStart && fmt.end <= nodeEnd) {
424
+ if (!endNode && pEnd > nodeStart && pEnd <= nodeEnd) {
347
425
  endNode = textNode;
348
- endLocalOffset = fmt.end - nodeStart;
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
- try {
358
- const range = document.createRange();
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
- const wrapper = document.createElement(fmt.type === "bold" ? "strong" : "em");
363
- wrapper.className = fmt.type === "bold" ? "step-preview-bold" : "step-preview-italic";
364
-
365
- const fragment = range.extractContents();
366
- wrapper.appendChild(fragment);
367
- range.insertNode(wrapper);
368
- } catch {
369
- // DOM manipulation can fail if range crosses element boundaries
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",
@@ -1212,6 +1266,114 @@ describe("markdownToBlocks", () => {
1212
1266
  ]);
1213
1267
  });
1214
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
+
1215
1377
  it("parses expected result lines written with bold 'Expected Result' prefix for compatibility", () => {
1216
1378
  const markdown = [
1217
1379
  "* Step 1: Send a chat message to the user.",
@@ -1823,3 +1985,46 @@ describe("file block parsing", () => {
1823
1985
  expect(md).toBe(markdown);
1824
1986
  });
1825
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("[![recording.mp4](/images/file-type-icons/mp4.svg)](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("[![sound.mp3](/images/file-type-icons/file.svg)](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) => (value.length ? value : " ");
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
  }
@@ -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
- font-weight: bold !important;
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;