testomatio-editor-blocks 0.4.24 → 0.4.25
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/snippet.js +6 -6
- package/package/editor/customMarkdownConverter.js +44 -10
- package/package/index.d.ts +1 -1
- package/package/index.js +1 -1
- package/package.json +1 -1
- package/src/editor/blocks/snippet.tsx +15 -10
- package/src/editor/customMarkdownConverter.test.ts +26 -88
- package/src/editor/customMarkdownConverter.ts +45 -12
- package/src/index.ts +1 -1
|
@@ -110,11 +110,11 @@ export const snippetBlock = createReactBlockSpec({
|
|
|
110
110
|
if (!hasSnippets) {
|
|
111
111
|
return (_jsx("div", { className: "bn-teststep bn-snippet", "data-block-id": block.id, children: _jsx("p", { className: "bn-snippet__empty", children: "No snippets in this project." }) }));
|
|
112
112
|
}
|
|
113
|
-
return (_jsxs("div", { className: "bn-teststep bn-snippet", "data-block-id": block.id, onFocus: handleFieldFocus, children: [_jsxs("div", { className: "bn-snippet__header", children: [_jsx("span", { className: "bn-snippet__label", children: "Snippet" }), _jsx(SnippetDropdown, { value: resolvedTitle, placeholder: "Select Snippet", suggestions: snippetSuggestions, selectedId: snippetId, onSelect: handleSnippetSelect })] }), isSnippetSelected &&
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
113
|
+
return (_jsxs("div", { className: "bn-teststep bn-snippet", "data-block-id": block.id, onFocus: handleFieldFocus, children: [_jsxs("div", { className: "bn-snippet__header", children: [_jsx("span", { className: "bn-snippet__label", children: "Snippet" }), _jsx(SnippetDropdown, { value: resolvedTitle, placeholder: "Select Snippet", suggestions: snippetSuggestions, selectedId: snippetId, onSelect: handleSnippetSelect })] }), isSnippetSelected && (_jsx("div", { className: "bn-snippet__content", children: snippetData ? (_jsx("span", { dangerouslySetInnerHTML: {
|
|
114
|
+
__html: snippetData
|
|
115
|
+
.replace(/&/g, "&")
|
|
116
|
+
.replace(/</g, "<")
|
|
117
|
+
.replace(/>/g, ">"),
|
|
118
|
+
} })) : (_jsx("span", { className: "bn-snippet__empty", children: "No content here. Please update the snippet." })) }))] }));
|
|
119
119
|
},
|
|
120
120
|
});
|
|
@@ -257,6 +257,14 @@ function serializeBlock(block, ctx, orderedIndex, stepIndex) {
|
|
|
257
257
|
lines.push(...serializeChildren(block, ctx));
|
|
258
258
|
return lines;
|
|
259
259
|
}
|
|
260
|
+
case "image": {
|
|
261
|
+
const url = block.props.url || "";
|
|
262
|
+
const caption = block.props.caption || "";
|
|
263
|
+
if (url) {
|
|
264
|
+
lines.push(``);
|
|
265
|
+
}
|
|
266
|
+
return flattenWithBlankLine(lines, true);
|
|
267
|
+
}
|
|
260
268
|
case "testStep":
|
|
261
269
|
case "snippet": {
|
|
262
270
|
const isSnippet = block.type === "snippet";
|
|
@@ -455,9 +463,9 @@ function serializeBlocks(blocks, ctx) {
|
|
|
455
463
|
export function blocksToMarkdown(blocks) {
|
|
456
464
|
const lines = serializeBlocks(blocks, { listDepth: 0, insideQuote: false });
|
|
457
465
|
const cleaned = lines
|
|
458
|
-
// Collapse
|
|
466
|
+
// Collapse excessive blank lines but preserve one extra for empty paragraphs.
|
|
459
467
|
.join("\n")
|
|
460
|
-
.replace(/\n{
|
|
468
|
+
.replace(/\n{4,}/g, "\n\n\n")
|
|
461
469
|
.trimEnd();
|
|
462
470
|
return cleaned;
|
|
463
471
|
}
|
|
@@ -1086,12 +1094,6 @@ export function fixMalformedImageBlocks(blocks) {
|
|
|
1086
1094
|
while (i < blocks.length) {
|
|
1087
1095
|
const current = blocks[i];
|
|
1088
1096
|
const next = blocks[i + 1];
|
|
1089
|
-
// Skip empty paragraphs
|
|
1090
|
-
if (current.type === "paragraph" &&
|
|
1091
|
-
(!current.content || !Array.isArray(current.content) || current.content.length === 0)) {
|
|
1092
|
-
i += 1;
|
|
1093
|
-
continue;
|
|
1094
|
-
}
|
|
1095
1097
|
// Check if current is a paragraph with just "!" - this is definitely a malformed image
|
|
1096
1098
|
if (current.type === "paragraph" &&
|
|
1097
1099
|
current.content &&
|
|
@@ -1133,7 +1135,7 @@ export function fixMalformedImageBlocks(blocks) {
|
|
|
1133
1135
|
return result;
|
|
1134
1136
|
}
|
|
1135
1137
|
export function markdownToBlocks(markdown) {
|
|
1136
|
-
var _a, _b;
|
|
1138
|
+
var _a, _b, _c;
|
|
1137
1139
|
const normalized = markdown.replace(/\r\n/g, "\n");
|
|
1138
1140
|
const lines = normalized.split("\n");
|
|
1139
1141
|
const blocks = [];
|
|
@@ -1143,6 +1145,16 @@ export function markdownToBlocks(markdown) {
|
|
|
1143
1145
|
const line = lines[index];
|
|
1144
1146
|
if (!line.trim()) {
|
|
1145
1147
|
index += 1;
|
|
1148
|
+
// Count consecutive blank lines
|
|
1149
|
+
let blankCount = 1;
|
|
1150
|
+
while (index < lines.length && !lines[index].trim()) {
|
|
1151
|
+
blankCount++;
|
|
1152
|
+
index++;
|
|
1153
|
+
}
|
|
1154
|
+
// Create empty paragraph for each extra blank line beyond the first
|
|
1155
|
+
for (let i = 1; i < blankCount; i++) {
|
|
1156
|
+
blocks.push({ type: "paragraph", content: [], children: [] });
|
|
1157
|
+
}
|
|
1146
1158
|
continue;
|
|
1147
1159
|
}
|
|
1148
1160
|
const snippetWrapper = parseSnippetWrapper(lines, index);
|
|
@@ -1202,11 +1214,33 @@ export function markdownToBlocks(markdown) {
|
|
|
1202
1214
|
index = nextIndex;
|
|
1203
1215
|
continue;
|
|
1204
1216
|
}
|
|
1217
|
+
const imageMatch = line.trim().match(/^!\[([^\]]*)\]\(([^)]+)\)$/);
|
|
1218
|
+
if (imageMatch) {
|
|
1219
|
+
blocks.push({
|
|
1220
|
+
type: "image",
|
|
1221
|
+
props: {
|
|
1222
|
+
url: imageMatch[2],
|
|
1223
|
+
caption: imageMatch[1] || "",
|
|
1224
|
+
name: "",
|
|
1225
|
+
},
|
|
1226
|
+
children: [],
|
|
1227
|
+
});
|
|
1228
|
+
index += 1;
|
|
1229
|
+
continue;
|
|
1230
|
+
}
|
|
1205
1231
|
const paragraph = parseParagraph(lines, index);
|
|
1206
1232
|
blocks.push(paragraph.block);
|
|
1207
1233
|
index = paragraph.nextIndex;
|
|
1208
1234
|
}
|
|
1209
|
-
|
|
1235
|
+
// Insert empty paragraphs between consecutive headings so users can type between them
|
|
1236
|
+
const result = [];
|
|
1237
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
1238
|
+
result.push(blocks[i]);
|
|
1239
|
+
if (blocks[i].type === "heading" && ((_c = blocks[i + 1]) === null || _c === void 0 ? void 0 : _c.type) === "heading") {
|
|
1240
|
+
result.push({ type: "paragraph", content: [], children: [] });
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
return fixMalformedImageBlocks(result);
|
|
1210
1244
|
}
|
|
1211
1245
|
function splitTableRow(line) {
|
|
1212
1246
|
let value = line.trim();
|
package/package/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { customSchema, type CustomSchema, type CustomBlock, type CustomEditor, } from "./editor/customSchema";
|
|
2
|
-
export { stepBlock } from "./editor/blocks/step";
|
|
2
|
+
export { stepBlock, canInsertStepOrSnippet } from "./editor/blocks/step";
|
|
3
3
|
export { snippetBlock } from "./editor/blocks/snippet";
|
|
4
4
|
export { markdownToHtml, htmlToMarkdown } from "./editor/blocks/markdown";
|
|
5
5
|
export { blocksToMarkdown, markdownToBlocks, type CustomEditorBlock, type CustomPartialBlock, } from "./editor/customMarkdownConverter";
|
package/package/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { customSchema, } from "./editor/customSchema";
|
|
2
|
-
export { stepBlock } from "./editor/blocks/step";
|
|
2
|
+
export { stepBlock, canInsertStepOrSnippet } from "./editor/blocks/step";
|
|
3
3
|
export { snippetBlock } from "./editor/blocks/snippet";
|
|
4
4
|
export { markdownToHtml, htmlToMarkdown } from "./editor/blocks/markdown";
|
|
5
5
|
export { blocksToMarkdown, markdownToBlocks, } from "./editor/customMarkdownConverter";
|
package/package.json
CHANGED
|
@@ -200,16 +200,21 @@ export const snippetBlock = createReactBlockSpec(
|
|
|
200
200
|
onSelect={handleSnippetSelect}
|
|
201
201
|
/>
|
|
202
202
|
</div>
|
|
203
|
-
{isSnippetSelected &&
|
|
204
|
-
<div
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
203
|
+
{isSnippetSelected && (
|
|
204
|
+
<div className="bn-snippet__content">
|
|
205
|
+
{snippetData ? (
|
|
206
|
+
<span
|
|
207
|
+
dangerouslySetInnerHTML={{
|
|
208
|
+
__html: snippetData
|
|
209
|
+
.replace(/&/g, "&")
|
|
210
|
+
.replace(/</g, "<")
|
|
211
|
+
.replace(/>/g, ">"),
|
|
212
|
+
}}
|
|
213
|
+
/>
|
|
214
|
+
) : (
|
|
215
|
+
<span className="bn-snippet__empty">No content here. Please update the snippet.</span>
|
|
216
|
+
)}
|
|
217
|
+
</div>
|
|
213
218
|
)}
|
|
214
219
|
</div>
|
|
215
220
|
);
|
|
@@ -1173,36 +1173,17 @@ describe("markdownToBlocks", () => {
|
|
|
1173
1173
|
|
|
1174
1174
|
const blocks = markdownToBlocks(markdown);
|
|
1175
1175
|
|
|
1176
|
-
// Find the
|
|
1177
|
-
const imageBlocks = blocks.filter(block =>
|
|
1178
|
-
block.type === "paragraph" &&
|
|
1179
|
-
block.content &&
|
|
1180
|
-
Array.isArray(block.content) &&
|
|
1181
|
-
block.content.some((item: any) =>
|
|
1182
|
-
(item.type === "text" && item.text === "!") ||
|
|
1183
|
-
(item.type === "link" && item.href && item.href.includes("/attachments/"))
|
|
1184
|
-
)
|
|
1185
|
-
);
|
|
1176
|
+
// Find the image blocks
|
|
1177
|
+
const imageBlocks = blocks.filter(block => block.type === "image");
|
|
1186
1178
|
|
|
1187
|
-
// Should have two
|
|
1179
|
+
// Should have two image blocks
|
|
1188
1180
|
expect(imageBlocks.length).toBe(2);
|
|
1189
1181
|
|
|
1190
|
-
// Check
|
|
1191
|
-
|
|
1192
|
-
imageBlocks.
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
if (link) {
|
|
1196
|
-
imageLinks.push(link);
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
});
|
|
1200
|
-
|
|
1201
|
-
expect(imageLinks).toHaveLength(2);
|
|
1202
|
-
expect(imageLinks[0].href).toBe("/attachments/se2n8jaGon.png");
|
|
1203
|
-
expect(imageLinks[0].content).toEqual([{ type: "text", text: "logs", styles: {} }]);
|
|
1204
|
-
expect(imageLinks[1].href).toBe("/attachments/p5DgklVeMg.png");
|
|
1205
|
-
expect(imageLinks[1].content).toEqual([{ type: "text", text: "", styles: {} }]);
|
|
1182
|
+
// Check image block props
|
|
1183
|
+
expect((imageBlocks[0].props as any).url).toBe("/attachments/se2n8jaGon.png");
|
|
1184
|
+
expect((imageBlocks[0].props as any).caption).toBe("logs");
|
|
1185
|
+
expect((imageBlocks[1].props as any).url).toBe("/attachments/p5DgklVeMg.png");
|
|
1186
|
+
expect((imageBlocks[1].props as any).caption).toBe("");
|
|
1206
1187
|
|
|
1207
1188
|
// Test round-trip conversion
|
|
1208
1189
|
const roundTripMarkdown = blocksToMarkdown(blocks as CustomEditorBlock[]);
|
|
@@ -1311,47 +1292,19 @@ describe("markdownToBlocks", () => {
|
|
|
1311
1292
|
|
|
1312
1293
|
const blocks = markdownToBlocks(markdown);
|
|
1313
1294
|
|
|
1314
|
-
// Find image
|
|
1315
|
-
const
|
|
1316
|
-
block.type === "paragraph" &&
|
|
1317
|
-
block.content &&
|
|
1318
|
-
Array.isArray(block.content) &&
|
|
1319
|
-
block.content.some((item: any) => item.type === "link")
|
|
1320
|
-
);
|
|
1295
|
+
// Find image blocks
|
|
1296
|
+
const imageBlocks = blocks.filter(block => block.type === "image");
|
|
1321
1297
|
|
|
1322
|
-
// Should have exactly 2 image
|
|
1323
|
-
expect(
|
|
1298
|
+
// Should have exactly 2 image blocks
|
|
1299
|
+
expect(imageBlocks).toHaveLength(2);
|
|
1324
1300
|
|
|
1325
1301
|
// First image with alt text
|
|
1326
|
-
expect(
|
|
1327
|
-
|
|
1328
|
-
text: "!",
|
|
1329
|
-
styles: {}
|
|
1330
|
-
});
|
|
1331
|
-
expect(imageParagraphs[0].content).toContainEqual({
|
|
1332
|
-
type: "link",
|
|
1333
|
-
href: "/attachments/se2n8jaGon.png",
|
|
1334
|
-
content: [{ type: "text", text: "logs", styles: {} }]
|
|
1335
|
-
});
|
|
1302
|
+
expect((imageBlocks[0].props as any).url).toBe("/attachments/se2n8jaGon.png");
|
|
1303
|
+
expect((imageBlocks[0].props as any).caption).toBe("logs");
|
|
1336
1304
|
|
|
1337
1305
|
// Second image without alt text
|
|
1338
|
-
expect(
|
|
1339
|
-
|
|
1340
|
-
text: "!",
|
|
1341
|
-
styles: {}
|
|
1342
|
-
});
|
|
1343
|
-
expect(imageParagraphs[1].content).toContainEqual({
|
|
1344
|
-
type: "link",
|
|
1345
|
-
href: "/attachments/p5DgklVeMg.png",
|
|
1346
|
-
content: [{ type: "text", text: "", styles: {} }]
|
|
1347
|
-
});
|
|
1348
|
-
|
|
1349
|
-
// No extra empty paragraphs
|
|
1350
|
-
const emptyParagraphs = blocks.filter(block =>
|
|
1351
|
-
block.type === "paragraph" &&
|
|
1352
|
-
(!block.content || block.content.length === 0)
|
|
1353
|
-
);
|
|
1354
|
-
expect(emptyParagraphs).toHaveLength(0);
|
|
1306
|
+
expect((imageBlocks[1].props as any).url).toBe("/attachments/p5DgklVeMg.png");
|
|
1307
|
+
expect((imageBlocks[1].props as any).caption).toBe("");
|
|
1355
1308
|
|
|
1356
1309
|
// Test round-trip conversion
|
|
1357
1310
|
const roundTripMarkdown = blocksToMarkdown(blocks as CustomEditorBlock[]);
|
|
@@ -1374,18 +1327,9 @@ describe("markdownToBlocks", () => {
|
|
|
1374
1327
|
|
|
1375
1328
|
const blocks = markdownToBlocks(markdown);
|
|
1376
1329
|
|
|
1377
|
-
// Should have exactly 2 image
|
|
1378
|
-
const
|
|
1379
|
-
|
|
1380
|
-
block.content &&
|
|
1381
|
-
Array.isArray(block.content) &&
|
|
1382
|
-
block.content.some((item: any) => item.type === "link")
|
|
1383
|
-
);
|
|
1384
|
-
|
|
1385
|
-
const emptyParagraphs = blocks.filter(block =>
|
|
1386
|
-
block.type === "paragraph" &&
|
|
1387
|
-
(!block.content || block.content.length === 0)
|
|
1388
|
-
);
|
|
1330
|
+
// Should have exactly 2 image blocks
|
|
1331
|
+
const imageBlocks = blocks.filter(block => block.type === "image");
|
|
1332
|
+
expect(imageBlocks).toHaveLength(2);
|
|
1389
1333
|
|
|
1390
1334
|
// Check for malformed image blocks (paragraphs with just "!" but no link)
|
|
1391
1335
|
const malformedBlocks = blocks.filter(block =>
|
|
@@ -1395,9 +1339,6 @@ describe("markdownToBlocks", () => {
|
|
|
1395
1339
|
block.content.some((item: any) => item.type === "text" && item.text === "!") &&
|
|
1396
1340
|
!block.content.some((item: any) => item.type === "link")
|
|
1397
1341
|
);
|
|
1398
|
-
|
|
1399
|
-
expect(imageParagraphs).toHaveLength(2);
|
|
1400
|
-
expect(emptyParagraphs).toHaveLength(0);
|
|
1401
1342
|
expect(malformedBlocks).toHaveLength(0);
|
|
1402
1343
|
|
|
1403
1344
|
// Test round-trip conversion
|
|
@@ -1489,8 +1430,8 @@ describe("markdownToBlocks", () => {
|
|
|
1489
1430
|
// Apply the fixMalformedImageBlocks function
|
|
1490
1431
|
const fixedBlocks = fixMalformedImageBlocks(malformedBlocks);
|
|
1491
1432
|
|
|
1492
|
-
// Should have removed the malformed
|
|
1493
|
-
expect(fixedBlocks.length).toBe(
|
|
1433
|
+
// Should have removed the malformed "!" only block but kept the empty paragraph and image block
|
|
1434
|
+
expect(fixedBlocks.length).toBe(3);
|
|
1494
1435
|
expect(fixedBlocks[0].type).toBe("heading");
|
|
1495
1436
|
expect(fixedBlocks[1].type).toBe("paragraph");
|
|
1496
1437
|
expect(fixedBlocks[1].content).toContainEqual(
|
|
@@ -1499,6 +1440,8 @@ describe("markdownToBlocks", () => {
|
|
|
1499
1440
|
expect(fixedBlocks[1].content).toContainEqual(
|
|
1500
1441
|
{ type: "link", href: "/attachments/se2n8jaGon.png", content: [{ type: "text", text: "logs", styles: {} }] }
|
|
1501
1442
|
);
|
|
1443
|
+
expect(fixedBlocks[2].type).toBe("paragraph");
|
|
1444
|
+
expect(fixedBlocks[2].content).toHaveLength(0);
|
|
1502
1445
|
});
|
|
1503
1446
|
|
|
1504
1447
|
it("reproduces the exact Unsplash URL issue", () => {
|
|
@@ -1525,14 +1468,9 @@ describe("markdownToBlocks", () => {
|
|
|
1525
1468
|
// Should have at least 3 blocks
|
|
1526
1469
|
expect(blocks.length).toBeGreaterThanOrEqual(3);
|
|
1527
1470
|
|
|
1528
|
-
// Should have
|
|
1529
|
-
const imageBlocks = blocks.filter(b =>
|
|
1530
|
-
|
|
1531
|
-
b.content &&
|
|
1532
|
-
Array.isArray(b.content) &&
|
|
1533
|
-
b.content.some((item: any) => item.type === "link")
|
|
1534
|
-
);
|
|
1535
|
-
expect(imageBlocks.length).toBeGreaterThan(0);
|
|
1471
|
+
// Should have image blocks
|
|
1472
|
+
const imageBlocks = blocks.filter(b => b.type === "image");
|
|
1473
|
+
expect(imageBlocks.length).toBe(2);
|
|
1536
1474
|
|
|
1537
1475
|
// Test round-trip conversion - check that we get the images back
|
|
1538
1476
|
const roundTripMarkdown = blocksToMarkdown(blocks as CustomEditorBlock[]);
|
|
@@ -335,6 +335,14 @@ function serializeBlock(
|
|
|
335
335
|
lines.push(...serializeChildren(block, ctx));
|
|
336
336
|
return lines;
|
|
337
337
|
}
|
|
338
|
+
case "image": {
|
|
339
|
+
const url = (block.props as any).url || "";
|
|
340
|
+
const caption = (block.props as any).caption || "";
|
|
341
|
+
if (url) {
|
|
342
|
+
lines.push(``);
|
|
343
|
+
}
|
|
344
|
+
return flattenWithBlankLine(lines, true);
|
|
345
|
+
}
|
|
338
346
|
case "testStep":
|
|
339
347
|
case "snippet": {
|
|
340
348
|
const isSnippet = block.type === "snippet";
|
|
@@ -569,9 +577,9 @@ function serializeBlocks(blocks: CustomEditorBlock[], ctx: MarkdownContext): str
|
|
|
569
577
|
export function blocksToMarkdown(blocks: CustomEditorBlock[]): string {
|
|
570
578
|
const lines = serializeBlocks(blocks, { listDepth: 0, insideQuote: false });
|
|
571
579
|
const cleaned = lines
|
|
572
|
-
// Collapse
|
|
580
|
+
// Collapse excessive blank lines but preserve one extra for empty paragraphs.
|
|
573
581
|
.join("\n")
|
|
574
|
-
.replace(/\n{
|
|
582
|
+
.replace(/\n{4,}/g, "\n\n\n")
|
|
575
583
|
.trimEnd();
|
|
576
584
|
|
|
577
585
|
return cleaned;
|
|
@@ -1300,15 +1308,6 @@ export function fixMalformedImageBlocks(blocks: CustomPartialBlock[]): CustomPar
|
|
|
1300
1308
|
const current = blocks[i];
|
|
1301
1309
|
const next = blocks[i + 1];
|
|
1302
1310
|
|
|
1303
|
-
// Skip empty paragraphs
|
|
1304
|
-
if (
|
|
1305
|
-
current.type === "paragraph" &&
|
|
1306
|
-
(!current.content || !Array.isArray(current.content) || current.content.length === 0)
|
|
1307
|
-
) {
|
|
1308
|
-
i += 1;
|
|
1309
|
-
continue;
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
1311
|
// Check if current is a paragraph with just "!" - this is definitely a malformed image
|
|
1313
1312
|
if (
|
|
1314
1313
|
current.type === "paragraph" &&
|
|
@@ -1371,6 +1370,16 @@ export function markdownToBlocks(markdown: string): CustomPartialBlock[] {
|
|
|
1371
1370
|
const line = lines[index];
|
|
1372
1371
|
if (!line.trim()) {
|
|
1373
1372
|
index += 1;
|
|
1373
|
+
// Count consecutive blank lines
|
|
1374
|
+
let blankCount = 1;
|
|
1375
|
+
while (index < lines.length && !lines[index].trim()) {
|
|
1376
|
+
blankCount++;
|
|
1377
|
+
index++;
|
|
1378
|
+
}
|
|
1379
|
+
// Create empty paragraph for each extra blank line beyond the first
|
|
1380
|
+
for (let i = 1; i < blankCount; i++) {
|
|
1381
|
+
blocks.push({ type: "paragraph", content: [], children: [] } as CustomPartialBlock);
|
|
1382
|
+
}
|
|
1374
1383
|
continue;
|
|
1375
1384
|
}
|
|
1376
1385
|
|
|
@@ -1447,12 +1456,36 @@ export function markdownToBlocks(markdown: string): CustomPartialBlock[] {
|
|
|
1447
1456
|
continue;
|
|
1448
1457
|
}
|
|
1449
1458
|
|
|
1459
|
+
const imageMatch = line.trim().match(/^!\[([^\]]*)\]\(([^)]+)\)$/);
|
|
1460
|
+
if (imageMatch) {
|
|
1461
|
+
blocks.push({
|
|
1462
|
+
type: "image",
|
|
1463
|
+
props: {
|
|
1464
|
+
url: imageMatch[2],
|
|
1465
|
+
caption: imageMatch[1] || "",
|
|
1466
|
+
name: "",
|
|
1467
|
+
},
|
|
1468
|
+
children: [],
|
|
1469
|
+
} as CustomPartialBlock);
|
|
1470
|
+
index += 1;
|
|
1471
|
+
continue;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1450
1474
|
const paragraph = parseParagraph(lines, index);
|
|
1451
1475
|
blocks.push(paragraph.block);
|
|
1452
1476
|
index = paragraph.nextIndex;
|
|
1453
1477
|
}
|
|
1454
1478
|
|
|
1455
|
-
|
|
1479
|
+
// Insert empty paragraphs between consecutive headings so users can type between them
|
|
1480
|
+
const result: CustomPartialBlock[] = [];
|
|
1481
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
1482
|
+
result.push(blocks[i]);
|
|
1483
|
+
if (blocks[i].type === "heading" && blocks[i + 1]?.type === "heading") {
|
|
1484
|
+
result.push({ type: "paragraph", content: [], children: [] } as CustomPartialBlock);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
return fixMalformedImageBlocks(result);
|
|
1456
1489
|
}
|
|
1457
1490
|
|
|
1458
1491
|
function splitTableRow(line: string): string[] {
|
package/src/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ export {
|
|
|
4
4
|
type CustomBlock,
|
|
5
5
|
type CustomEditor,
|
|
6
6
|
} from "./editor/customSchema";
|
|
7
|
-
export { stepBlock } from "./editor/blocks/step";
|
|
7
|
+
export { stepBlock, canInsertStepOrSnippet } from "./editor/blocks/step";
|
|
8
8
|
export { snippetBlock } from "./editor/blocks/snippet";
|
|
9
9
|
export { markdownToHtml, htmlToMarkdown } from "./editor/blocks/markdown";
|
|
10
10
|
|