Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 15 additions & 10 deletions src/editor/blocks/snippet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,16 +200,21 @@ export const snippetBlock = createReactBlockSpec(
onSelect={handleSnippetSelect}
/>
</div>
{isSnippetSelected && snippetData && (
<div
className="bn-snippet__content"
dangerouslySetInnerHTML={{
__html: snippetData
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;"),
}}
/>
{isSnippetSelected && (
<div className="bn-snippet__content">
{snippetData ? (
<span
dangerouslySetInnerHTML={{
__html: snippetData
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;"),
}}
/>
) : (
<span className="bn-snippet__empty">No content here. Please update the snippet.</span>
)}
</div>
)}
</div>
);
Expand Down
114 changes: 26 additions & 88 deletions src/editor/customMarkdownConverter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1173,36 +1173,17 @@ describe("markdownToBlocks", () => {

const blocks = markdownToBlocks(markdown);

// Find the paragraph blocks that contain the images (links)
const imageBlocks = blocks.filter(block =>
block.type === "paragraph" &&
block.content &&
Array.isArray(block.content) &&
block.content.some((item: any) =>
(item.type === "text" && item.text === "!") ||
(item.type === "link" && item.href && item.href.includes("/attachments/"))
)
);
// Find the image blocks
const imageBlocks = blocks.filter(block => block.type === "image");

// Should have two paragraph blocks with images
// Should have two image blocks
expect(imageBlocks.length).toBe(2);

// Check that both image links are properly parsed
const imageLinks: any[] = [];
imageBlocks.forEach(block => {
if (block.content && Array.isArray(block.content)) {
const link = (block.content as any[]).find(item => item.type === "link");
if (link) {
imageLinks.push(link);
}
}
});

expect(imageLinks).toHaveLength(2);
expect(imageLinks[0].href).toBe("/attachments/se2n8jaGon.png");
expect(imageLinks[0].content).toEqual([{ type: "text", text: "logs", styles: {} }]);
expect(imageLinks[1].href).toBe("/attachments/p5DgklVeMg.png");
expect(imageLinks[1].content).toEqual([{ type: "text", text: "", styles: {} }]);
// Check image block props
expect((imageBlocks[0].props as any).url).toBe("/attachments/se2n8jaGon.png");
expect((imageBlocks[0].props as any).caption).toBe("logs");
expect((imageBlocks[1].props as any).url).toBe("/attachments/p5DgklVeMg.png");
expect((imageBlocks[1].props as any).caption).toBe("");

// Test round-trip conversion
const roundTripMarkdown = blocksToMarkdown(blocks as CustomEditorBlock[]);
Expand Down Expand Up @@ -1311,47 +1292,19 @@ describe("markdownToBlocks", () => {

const blocks = markdownToBlocks(markdown);

// Find image paragraphs
const imageParagraphs = blocks.filter(block =>
block.type === "paragraph" &&
block.content &&
Array.isArray(block.content) &&
block.content.some((item: any) => item.type === "link")
);
// Find image blocks
const imageBlocks = blocks.filter(block => block.type === "image");

// Should have exactly 2 image paragraphs
expect(imageParagraphs).toHaveLength(2);
// Should have exactly 2 image blocks
expect(imageBlocks).toHaveLength(2);

// First image with alt text
expect(imageParagraphs[0].content).toContainEqual({
type: "text",
text: "!",
styles: {}
});
expect(imageParagraphs[0].content).toContainEqual({
type: "link",
href: "/attachments/se2n8jaGon.png",
content: [{ type: "text", text: "logs", styles: {} }]
});
expect((imageBlocks[0].props as any).url).toBe("/attachments/se2n8jaGon.png");
expect((imageBlocks[0].props as any).caption).toBe("logs");

// Second image without alt text
expect(imageParagraphs[1].content).toContainEqual({
type: "text",
text: "!",
styles: {}
});
expect(imageParagraphs[1].content).toContainEqual({
type: "link",
href: "/attachments/p5DgklVeMg.png",
content: [{ type: "text", text: "", styles: {} }]
});

// No extra empty paragraphs
const emptyParagraphs = blocks.filter(block =>
block.type === "paragraph" &&
(!block.content || block.content.length === 0)
);
expect(emptyParagraphs).toHaveLength(0);
expect((imageBlocks[1].props as any).url).toBe("/attachments/p5DgklVeMg.png");
expect((imageBlocks[1].props as any).caption).toBe("");

// Test round-trip conversion
const roundTripMarkdown = blocksToMarkdown(blocks as CustomEditorBlock[]);
Expand All @@ -1374,18 +1327,9 @@ describe("markdownToBlocks", () => {

const blocks = markdownToBlocks(markdown);

// Should have exactly 2 image paragraphs, no empty paragraphs
const imageParagraphs = blocks.filter(block =>
block.type === "paragraph" &&
block.content &&
Array.isArray(block.content) &&
block.content.some((item: any) => item.type === "link")
);

const emptyParagraphs = blocks.filter(block =>
block.type === "paragraph" &&
(!block.content || block.content.length === 0)
);
// Should have exactly 2 image blocks
const imageBlocks = blocks.filter(block => block.type === "image");
expect(imageBlocks).toHaveLength(2);

// Check for malformed image blocks (paragraphs with just "!" but no link)
const malformedBlocks = blocks.filter(block =>
Expand All @@ -1395,9 +1339,6 @@ describe("markdownToBlocks", () => {
block.content.some((item: any) => item.type === "text" && item.text === "!") &&
!block.content.some((item: any) => item.type === "link")
);

expect(imageParagraphs).toHaveLength(2);
expect(emptyParagraphs).toHaveLength(0);
expect(malformedBlocks).toHaveLength(0);

// Test round-trip conversion
Expand Down Expand Up @@ -1489,8 +1430,8 @@ describe("markdownToBlocks", () => {
// Apply the fixMalformedImageBlocks function
const fixedBlocks = fixMalformedImageBlocks(malformedBlocks);

// Should have removed the malformed image blocks (both the "!" only block and the empty block)
expect(fixedBlocks.length).toBe(2);
// Should have removed the malformed "!" only block but kept the empty paragraph and image block
expect(fixedBlocks.length).toBe(3);
expect(fixedBlocks[0].type).toBe("heading");
expect(fixedBlocks[1].type).toBe("paragraph");
expect(fixedBlocks[1].content).toContainEqual(
Expand All @@ -1499,6 +1440,8 @@ describe("markdownToBlocks", () => {
expect(fixedBlocks[1].content).toContainEqual(
{ type: "link", href: "/attachments/se2n8jaGon.png", content: [{ type: "text", text: "logs", styles: {} }] }
);
expect(fixedBlocks[2].type).toBe("paragraph");
expect(fixedBlocks[2].content).toHaveLength(0);
});

it("reproduces the exact Unsplash URL issue", () => {
Expand All @@ -1525,14 +1468,9 @@ describe("markdownToBlocks", () => {
// Should have at least 3 blocks
expect(blocks.length).toBeGreaterThanOrEqual(3);

// Should have at least one paragraph with content (images)
const imageBlocks = blocks.filter(b =>
b.type === "paragraph" &&
b.content &&
Array.isArray(b.content) &&
b.content.some((item: any) => item.type === "link")
);
expect(imageBlocks.length).toBeGreaterThan(0);
// Should have image blocks
const imageBlocks = blocks.filter(b => b.type === "image");
expect(imageBlocks.length).toBe(2);

// Test round-trip conversion - check that we get the images back
const roundTripMarkdown = blocksToMarkdown(blocks as CustomEditorBlock[]);
Expand Down
57 changes: 45 additions & 12 deletions src/editor/customMarkdownConverter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,14 @@ function serializeBlock(
lines.push(...serializeChildren(block, ctx));
return lines;
}
case "image": {
const url = (block.props as any).url || "";
const caption = (block.props as any).caption || "";
if (url) {
lines.push(`![${caption}](${url})`);
}
return flattenWithBlankLine(lines, true);
}
case "testStep":
case "snippet": {
const isSnippet = block.type === "snippet";
Expand Down Expand Up @@ -569,9 +577,9 @@ function serializeBlocks(blocks: CustomEditorBlock[], ctx: MarkdownContext): str
export function blocksToMarkdown(blocks: CustomEditorBlock[]): string {
const lines = serializeBlocks(blocks, { listDepth: 0, insideQuote: false });
const cleaned = lines
// Collapse more than two blank lines into just two for readability.
// Collapse excessive blank lines but preserve one extra for empty paragraphs.
.join("\n")
.replace(/\n{3,}/g, "\n\n")
.replace(/\n{4,}/g, "\n\n\n")
.trimEnd();

return cleaned;
Expand Down Expand Up @@ -1300,15 +1308,6 @@ export function fixMalformedImageBlocks(blocks: CustomPartialBlock[]): CustomPar
const current = blocks[i];
const next = blocks[i + 1];

// Skip empty paragraphs
if (
current.type === "paragraph" &&
(!current.content || !Array.isArray(current.content) || current.content.length === 0)
) {
i += 1;
continue;
}

// Check if current is a paragraph with just "!" - this is definitely a malformed image
if (
current.type === "paragraph" &&
Expand Down Expand Up @@ -1371,6 +1370,16 @@ export function markdownToBlocks(markdown: string): CustomPartialBlock[] {
const line = lines[index];
if (!line.trim()) {
index += 1;
// Count consecutive blank lines
let blankCount = 1;
while (index < lines.length && !lines[index].trim()) {
blankCount++;
index++;
}
// Create empty paragraph for each extra blank line beyond the first
for (let i = 1; i < blankCount; i++) {
blocks.push({ type: "paragraph", content: [], children: [] } as CustomPartialBlock);
}
continue;
}

Expand Down Expand Up @@ -1447,12 +1456,36 @@ export function markdownToBlocks(markdown: string): CustomPartialBlock[] {
continue;
}

const imageMatch = line.trim().match(/^!\[([^\]]*)\]\(([^)]+)\)$/);
if (imageMatch) {
blocks.push({
type: "image",
props: {
url: imageMatch[2],
caption: imageMatch[1] || "",
name: "",
},
children: [],
} as CustomPartialBlock);
index += 1;
continue;
}

const paragraph = parseParagraph(lines, index);
blocks.push(paragraph.block);
index = paragraph.nextIndex;
}

return fixMalformedImageBlocks(blocks);
// Insert empty paragraphs between consecutive headings so users can type between them
const result: CustomPartialBlock[] = [];
for (let i = 0; i < blocks.length; i++) {
result.push(blocks[i]);
if (blocks[i].type === "heading" && blocks[i + 1]?.type === "heading") {
result.push({ type: "paragraph", content: [], children: [] } as CustomPartialBlock);
}
}

return fixMalformedImageBlocks(result);
}

function splitTableRow(line: string): string[] {
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export {
type CustomBlock,
type CustomEditor,
} from "./editor/customSchema";
export { stepBlock } from "./editor/blocks/step";
export { stepBlock, canInsertStepOrSnippet } from "./editor/blocks/step";
export { snippetBlock } from "./editor/blocks/snippet";
export { markdownToHtml, htmlToMarkdown } from "./editor/blocks/markdown";

Expand Down
Loading