testomatio-editor-blocks 0.4.72 → 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,38 +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
- dataLines.forEach((dataLine) => {
432
+ const escaped = escapeBody(stepData);
433
+ escaped.split(/\r?\n/).forEach((dataLine) => {
391
434
  const trimmedLine = dataLine.trim();
392
- if (trimmedLine.length > 0) {
393
- lines.push(` ${escapeStepContent(trimmedLine)}`);
394
- }
395
- else {
396
- lines.push(" ");
397
- }
435
+ lines.push(trimmedLine.length === 0 ? " " : ` ${trimmedLine}`);
398
436
  });
399
437
  }
400
438
  const normalizedExpected = stripExpectedPrefix(expectedResult).trim();
401
439
  if (normalizedExpected.length > 0) {
402
- const expectedLines = normalizedExpected.split(/\r?\n/);
440
+ const escaped = makeStepEscaper()(normalizedExpected);
403
441
  const label = "*Expected result*";
404
- expectedLines.forEach((expectedLine, index) => {
442
+ let isFirst = true;
443
+ escaped.split(/\r?\n/).forEach((expectedLine) => {
405
444
  const trimmedLine = expectedLine.trim();
406
445
  if (trimmedLine.length === 0) {
407
446
  return;
408
447
  }
409
- if (index === 0) {
410
- lines.push(` ${label}: ${escapeStepContent(trimmedLine)}`);
411
- }
412
- else {
413
- lines.push(` ${escapeStepContent(trimmedLine)}`);
414
- }
448
+ lines.push(isFirst ? ` ${label}: ${trimmedLine}` : ` ${trimmedLine}`);
449
+ isFirst = false;
415
450
  });
416
451
  }
417
452
  return lines;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testomatio-editor-blocks",
3
- "version": "0.4.72",
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",
@@ -1249,6 +1249,112 @@ describe("markdownToBlocks", () => {
1249
1249
  );
1250
1250
  });
1251
1251
 
1252
+ it("does not escape dots inside fenced code blocks in step data", () => {
1253
+ const stepData = [
1254
+ "```",
1255
+ "curl 'https://stable.testomat.io/api/runs/54?page=1&entry=' \\",
1256
+ " -H 'accept-language: en-US,en;q=0.9' \\",
1257
+ " -b 'gclau=1.1.1681594017.1781077572;'",
1258
+ "```",
1259
+ ].join("\n");
1260
+
1261
+ const markdown = blocksToMarkdown([
1262
+ {
1263
+ id: "step1",
1264
+ type: "testStep",
1265
+ props: {
1266
+ stepTitle: "Run the request.",
1267
+ stepData,
1268
+ expectedResult: "",
1269
+ listStyle: "bullet",
1270
+ },
1271
+ content: undefined,
1272
+ children: [],
1273
+ },
1274
+ ]);
1275
+
1276
+ expect(markdown).toBe(
1277
+ [
1278
+ // Title outside the fence is still escaped.
1279
+ "* Run the request\\.",
1280
+ // Dots inside the fence stay literal.
1281
+ " ```",
1282
+ " curl 'https://stable.testomat.io/api/runs/54?page=1&entry=' \\",
1283
+ " -H 'accept-language: en-US,en;q=0.9' \\",
1284
+ " -b 'gclau=1.1.1681594017.1781077572;'",
1285
+ " ```",
1286
+ ].join("\n"),
1287
+ );
1288
+
1289
+ // Round-trip stays stable.
1290
+ const roundTrip = blocksToMarkdown(
1291
+ markdownToBlocks(["### Steps", "", markdown].join("\n")) as CustomEditorBlock[],
1292
+ );
1293
+ expect(roundTrip).toContain(
1294
+ " curl 'https://stable.testomat.io/api/runs/54?page=1&entry=' \\",
1295
+ );
1296
+ expect(roundTrip).toContain(" -b 'gclau=1.1.1681594017.1781077572;'");
1297
+ });
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
+
1252
1358
  it("does not include content after a blank line in step data", () => {
1253
1359
  const markdown = [
1254
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,39 +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
- dataLines.forEach((dataLine: string) => {
513
+ const escaped = escapeBody(stepData);
514
+ escaped.split(/\r?\n/).forEach((dataLine: string) => {
473
515
  const trimmedLine = dataLine.trim();
474
- if (trimmedLine.length > 0) {
475
- lines.push(` ${escapeStepContent(trimmedLine)}`);
476
- } else {
477
- lines.push(" ");
478
- }
516
+ lines.push(trimmedLine.length === 0 ? " " : ` ${trimmedLine}`);
479
517
  });
480
518
  }
481
519
 
482
520
  const normalizedExpected = stripExpectedPrefix(expectedResult).trim();
483
521
  if (normalizedExpected.length > 0) {
484
- const expectedLines = normalizedExpected.split(/\r?\n/);
522
+ const escaped = makeStepEscaper()(normalizedExpected);
485
523
  const label = "*Expected result*";
486
- expectedLines.forEach((expectedLine: string, index: number) => {
524
+ let isFirst = true;
525
+ escaped.split(/\r?\n/).forEach((expectedLine: string) => {
487
526
  const trimmedLine = expectedLine.trim();
488
527
  if (trimmedLine.length === 0) {
489
528
  return;
490
529
  }
491
-
492
- if (index === 0) {
493
- lines.push(` ${label}: ${escapeStepContent(trimmedLine)}`);
494
- } else {
495
- lines.push(` ${escapeStepContent(trimmedLine)}`);
496
- }
530
+ lines.push(isFirst ? ` ${label}: ${trimmedLine}` : ` ${trimmedLine}`);
531
+ isFirst = false;
497
532
  });
498
533
  }
499
534