testomatio-editor-blocks 0.4.73 → 0.4.74

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.
@@ -43,8 +43,46 @@ function escapeMarkdown(text) {
43
43
  result += text.slice(lastIndex).replace(SPECIAL_CHAR_REGEX, "\\$1");
44
44
  return result;
45
45
  }
46
- function escapeStepContent(text) {
47
- return text.replace(/\./g, "\\.");
46
+ // Creates a stateful escaper that escapes dots outside Markdown code while
47
+ // leaving code spans (inline `…`, fenced ``` … ```) verbatim — backslash
48
+ // escapes are ignored inside code, so `\.` would render literally.
49
+ //
50
+ // The state (an open, not-yet-closed backtick run) carries across calls so a
51
+ // fence can be opened in one segment and closed in another. This matters
52
+ // because the step editor splits a multi-line code block across props: the
53
+ // opening ``` lands in `stepTitle` while the body and closing ``` land in
54
+ // `stepData`.
55
+ function makeStepEscaper() {
56
+ let fence = null;
57
+ return (text) => {
58
+ let result = "";
59
+ let i = 0;
60
+ while (i < text.length) {
61
+ if (text[i] === "`") {
62
+ let ticks = 0;
63
+ while (text[i + ticks] === "`")
64
+ ticks++;
65
+ const run = "`".repeat(ticks);
66
+ result += run;
67
+ i += ticks;
68
+ if (fence === null) {
69
+ fence = run; // open a code span/fence
70
+ }
71
+ else if (fence === run) {
72
+ fence = null; // matching run closes it
73
+ }
74
+ continue;
75
+ }
76
+ if (fence !== null) {
77
+ result += text[i]; // inside code — copy verbatim
78
+ i++;
79
+ continue;
80
+ }
81
+ result += text[i] === "." ? "\\." : text[i];
82
+ i++;
83
+ }
84
+ return result;
85
+ };
48
86
  }
49
87
  function stripHtmlWrappers(text) {
50
88
  return text
@@ -380,53 +418,35 @@ function serializeBlock(block, ctx, orderedIndex, stepIndex) {
380
418
  .join(" ");
381
419
  const normalizedExpectedForCheck = stripExpectedPrefix(expectedResult).trim();
382
420
  const hasContent = stepData.length > 0 || normalizedExpectedForCheck.length > 0;
421
+ // One escaper threads code-fence state from the title into the data: the
422
+ // editor splits a multi-line code block so the opening ``` sits in the
423
+ // title and the body + closing ``` sit in the data. Both must be treated
424
+ // as a single code region so their dots stay literal.
425
+ const escapeBody = makeStepEscaper();
383
426
  if (normalizedTitle.length > 0 || hasContent) {
384
427
  const listStyle = (_o = block.props.listStyle) !== null && _o !== void 0 ? _o : "bullet";
385
428
  const prefix = listStyle === "ordered" ? `${(stepIndex !== null && stepIndex !== void 0 ? stepIndex : 0) + 1}.` : "*";
386
- lines.push(normalizedTitle.length > 0 ? `${prefix} ${escapeStepContent(normalizedTitle)}` : `${prefix} `);
429
+ lines.push(normalizedTitle.length > 0 ? `${prefix} ${escapeBody(normalizedTitle)}` : `${prefix} `);
387
430
  }
388
431
  if (stepData.length > 0) {
389
- const dataLines = stepData.split(/\r?\n/);
390
- let insideCodeFence = false;
391
- dataLines.forEach((dataLine) => {
432
+ const escaped = escapeBody(stepData);
433
+ escaped.split(/\r?\n/).forEach((dataLine) => {
392
434
  const trimmedLine = dataLine.trim();
393
- if (trimmedLine.length === 0) {
394
- lines.push(" ");
395
- return;
396
- }
397
- // Don't escape dots inside fenced code blocks (or on the fence lines
398
- // themselves) — Markdown ignores backslash escapes there, so `\.`
399
- // would render literally.
400
- const isFence = trimmedLine.startsWith("```");
401
- const content = insideCodeFence || isFence ? trimmedLine : escapeStepContent(trimmedLine);
402
- lines.push(` ${content}`);
403
- if (isFence) {
404
- insideCodeFence = !insideCodeFence;
405
- }
435
+ lines.push(trimmedLine.length === 0 ? " " : ` ${trimmedLine}`);
406
436
  });
407
437
  }
408
438
  const normalizedExpected = stripExpectedPrefix(expectedResult).trim();
409
439
  if (normalizedExpected.length > 0) {
410
- const expectedLines = normalizedExpected.split(/\r?\n/);
440
+ const escaped = makeStepEscaper()(normalizedExpected);
411
441
  const label = "*Expected result*";
412
- let insideCodeFence = false;
413
- expectedLines.forEach((expectedLine, index) => {
442
+ let isFirst = true;
443
+ escaped.split(/\r?\n/).forEach((expectedLine) => {
414
444
  const trimmedLine = expectedLine.trim();
415
445
  if (trimmedLine.length === 0) {
416
446
  return;
417
447
  }
418
- // As with step data, leave dots untouched inside fenced code blocks.
419
- const isFence = trimmedLine.startsWith("```");
420
- const content = insideCodeFence || isFence ? trimmedLine : escapeStepContent(trimmedLine);
421
- if (index === 0) {
422
- lines.push(` ${label}: ${content}`);
423
- }
424
- else {
425
- lines.push(` ${content}`);
426
- }
427
- if (isFence) {
428
- insideCodeFence = !insideCodeFence;
429
- }
448
+ lines.push(isFirst ? ` ${label}: ${trimmedLine}` : ` ${trimmedLine}`);
449
+ isFirst = false;
430
450
  });
431
451
  }
432
452
  return lines;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testomatio-editor-blocks",
3
- "version": "0.4.73",
3
+ "version": "0.4.74",
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",
@@ -1296,6 +1296,65 @@ describe("markdownToBlocks", () => {
1296
1296
  expect(roundTrip).toContain(" -b 'gclau=1.1.1681594017.1781077572;'");
1297
1297
  });
1298
1298
 
1299
+ it("does not escape dots inside inline code in step data", () => {
1300
+ const markdown = blocksToMarkdown([
1301
+ {
1302
+ id: "step1",
1303
+ type: "testStep",
1304
+ props: {
1305
+ stepTitle: "Run the request.",
1306
+ stepData: "`curl https://example.com/api`",
1307
+ expectedResult: "",
1308
+ listStyle: "bullet",
1309
+ },
1310
+ content: undefined,
1311
+ children: [],
1312
+ },
1313
+ ]);
1314
+
1315
+ expect(markdown).toBe(
1316
+ [
1317
+ // Title outside code is still escaped.
1318
+ "* Run the request\\.",
1319
+ // Dots inside the inline code span stay literal.
1320
+ " `curl https://example.com/api`",
1321
+ ].join("\n"),
1322
+ );
1323
+ });
1324
+
1325
+ it("does not escape dots when a fenced block opens in the title and the body lands in step data", () => {
1326
+ // The step editor splits a multi-line code block: the opening ``` becomes
1327
+ // the title and the body + closing ``` become the step data.
1328
+ const markdown = blocksToMarkdown([
1329
+ {
1330
+ id: "step1",
1331
+ type: "testStep",
1332
+ props: {
1333
+ stepTitle: "```",
1334
+ stepData: [
1335
+ "curl --location 'https://example.com.ua' \\",
1336
+ "--data-raw '{ \"method\": \"url.method\" }'",
1337
+ "```",
1338
+ ].join("\n"),
1339
+ expectedResult: "",
1340
+ listStyle: "bullet",
1341
+ },
1342
+ content: undefined,
1343
+ children: [],
1344
+ },
1345
+ ]);
1346
+
1347
+ expect(markdown).toBe(
1348
+ [
1349
+ "* ```",
1350
+ // Dots inside the fence stay literal even though it opened in the title.
1351
+ " curl --location 'https://example.com.ua' \\",
1352
+ " --data-raw '{ \"method\": \"url.method\" }'",
1353
+ " ```",
1354
+ ].join("\n"),
1355
+ );
1356
+ });
1357
+
1299
1358
  it("does not include content after a blank line in step data", () => {
1300
1359
  const markdown = [
1301
1360
  "### Steps",
@@ -84,8 +84,44 @@ function escapeMarkdown(text: string): string {
84
84
  return result;
85
85
  }
86
86
 
87
- function escapeStepContent(text: string): string {
88
- return text.replace(/\./g, "\\.");
87
+ // Creates a stateful escaper that escapes dots outside Markdown code while
88
+ // leaving code spans (inline `…`, fenced ``` … ```) verbatim — backslash
89
+ // escapes are ignored inside code, so `\.` would render literally.
90
+ //
91
+ // The state (an open, not-yet-closed backtick run) carries across calls so a
92
+ // fence can be opened in one segment and closed in another. This matters
93
+ // because the step editor splits a multi-line code block across props: the
94
+ // opening ``` lands in `stepTitle` while the body and closing ``` land in
95
+ // `stepData`.
96
+ function makeStepEscaper(): (text: string) => string {
97
+ let fence: string | null = null;
98
+ return (text: string): string => {
99
+ let result = "";
100
+ let i = 0;
101
+ while (i < text.length) {
102
+ if (text[i] === "`") {
103
+ let ticks = 0;
104
+ while (text[i + ticks] === "`") ticks++;
105
+ const run = "`".repeat(ticks);
106
+ result += run;
107
+ i += ticks;
108
+ if (fence === null) {
109
+ fence = run; // open a code span/fence
110
+ } else if (fence === run) {
111
+ fence = null; // matching run closes it
112
+ }
113
+ continue;
114
+ }
115
+ if (fence !== null) {
116
+ result += text[i]; // inside code — copy verbatim
117
+ i++;
118
+ continue;
119
+ }
120
+ result += text[i] === "." ? "\\." : text[i];
121
+ i++;
122
+ }
123
+ return result;
124
+ };
89
125
  }
90
126
 
91
127
  function stripHtmlWrappers(text: string): string {
@@ -461,57 +497,38 @@ function serializeBlock(
461
497
  const normalizedExpectedForCheck = stripExpectedPrefix(expectedResult).trim();
462
498
  const hasContent = stepData.length > 0 || normalizedExpectedForCheck.length > 0;
463
499
 
500
+ // One escaper threads code-fence state from the title into the data: the
501
+ // editor splits a multi-line code block so the opening ``` sits in the
502
+ // title and the body + closing ``` sit in the data. Both must be treated
503
+ // as a single code region so their dots stay literal.
504
+ const escapeBody = makeStepEscaper();
505
+
464
506
  if (normalizedTitle.length > 0 || hasContent) {
465
507
  const listStyle = (block.props as any).listStyle ?? "bullet";
466
508
  const prefix = listStyle === "ordered" ? `${(stepIndex ?? 0) + 1}.` : "*";
467
- lines.push(normalizedTitle.length > 0 ? `${prefix} ${escapeStepContent(normalizedTitle)}` : `${prefix} `);
509
+ lines.push(normalizedTitle.length > 0 ? `${prefix} ${escapeBody(normalizedTitle)}` : `${prefix} `);
468
510
  }
469
511
 
470
512
  if (stepData.length > 0) {
471
- const dataLines = stepData.split(/\r?\n/);
472
- let insideCodeFence = false;
473
- dataLines.forEach((dataLine: string) => {
513
+ const escaped = escapeBody(stepData);
514
+ escaped.split(/\r?\n/).forEach((dataLine: string) => {
474
515
  const trimmedLine = dataLine.trim();
475
- if (trimmedLine.length === 0) {
476
- lines.push(" ");
477
- return;
478
- }
479
- // Don't escape dots inside fenced code blocks (or on the fence lines
480
- // themselves) — Markdown ignores backslash escapes there, so `\.`
481
- // would render literally.
482
- const isFence = trimmedLine.startsWith("```");
483
- const content =
484
- insideCodeFence || isFence ? trimmedLine : escapeStepContent(trimmedLine);
485
- lines.push(` ${content}`);
486
- if (isFence) {
487
- insideCodeFence = !insideCodeFence;
488
- }
516
+ lines.push(trimmedLine.length === 0 ? " " : ` ${trimmedLine}`);
489
517
  });
490
518
  }
491
519
 
492
520
  const normalizedExpected = stripExpectedPrefix(expectedResult).trim();
493
521
  if (normalizedExpected.length > 0) {
494
- const expectedLines = normalizedExpected.split(/\r?\n/);
522
+ const escaped = makeStepEscaper()(normalizedExpected);
495
523
  const label = "*Expected result*";
496
- let insideCodeFence = false;
497
- expectedLines.forEach((expectedLine: string, index: number) => {
524
+ let isFirst = true;
525
+ escaped.split(/\r?\n/).forEach((expectedLine: string) => {
498
526
  const trimmedLine = expectedLine.trim();
499
527
  if (trimmedLine.length === 0) {
500
528
  return;
501
529
  }
502
-
503
- // As with step data, leave dots untouched inside fenced code blocks.
504
- const isFence = trimmedLine.startsWith("```");
505
- const content =
506
- insideCodeFence || isFence ? trimmedLine : escapeStepContent(trimmedLine);
507
- if (index === 0) {
508
- lines.push(` ${label}: ${content}`);
509
- } else {
510
- lines.push(` ${content}`);
511
- }
512
- if (isFence) {
513
- insideCodeFence = !insideCodeFence;
514
- }
530
+ lines.push(isFirst ? ` ${label}: ${trimmedLine}` : ` ${trimmedLine}`);
531
+ isFirst = false;
515
532
  });
516
533
  }
517
534