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
15 changes: 10 additions & 5 deletions src/app/plugins/markdown/bidirectional.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,16 @@ describe('bidirectional round-trip', () => {
});

it('round-trips blockquotes', () => {
const markdown = '> Quote text';
const html = markdownToHtml(markdown);
const injected = injectDataMd(html);
const result = htmlToMarkdown(injected);
expect(result).toContain('> Quote text');
const roundtrip = (markdown: string) => {
const html = markdownToHtml(markdown);
const injected = injectDataMd(html);
return htmlToMarkdown(injected);
};
expect(roundtrip('> Quote text')).toBe('> Quote text');
expect(roundtrip('> line one\n> line two')).toBe('> line one\n> line two');
expect(roundtrip('> test\ntest')).toBe('> test\ntest');
expect(roundtrip('> test\n\n> test')).toBe('> test\n\n> test');
expect((markdownToHtml('> test\n\n> test').match(/<blockquote/g) ?? []).length).toBe(2);
});

it('round-trips unordered lists', () => {
Expand Down
25 changes: 25 additions & 0 deletions src/app/plugins/markdown/expandBlockNewlines.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { describe, expect, it } from 'vitest';
import { expandBlockBoundariesAfterSingleNewlines } from './expandBlockNewlines';
import { markdownToHtml } from './markdownToHtml';

describe('expandBlockBoundariesAfterSingleNewlines', () => {
it('does not expand between consecutive blockquote lines', () => {
const md = '> test\n> test\n> test';
expect(expandBlockBoundariesAfterSingleNewlines(md)).toBe(md);
});

it('still expands before the first blockquote line', () => {
expect(expandBlockBoundariesAfterSingleNewlines('intro\n> quote')).toBe('intro\n\n> quote');
});

it('still expands when a blockquote ends', () => {
expect(expandBlockBoundariesAfterSingleNewlines('> quote\nplain')).toBe('> quote\n\nplain');
});
});

describe('consecutive blockquotes', () => {
it('produces a single blockquote element', () => {
const html = markdownToHtml('> test\n> test\n> test');
expect((html.match(/<blockquote/g) ?? []).length).toBe(1);
});
});
4 changes: 4 additions & 0 deletions src/app/plugins/markdown/expandBlockNewlines.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ function nextLineIsBlockStarter(md: string, newlineIdx: number): boolean {
}

function shouldExpandSingleNewline(md: string, newlineIdx: number): boolean {
// Consecutive `>` lines belong to one blockquote, keep the single `\n` between them.
if (prevLineIsBlockquote(md, newlineIdx) && nextLineContinuesBlockquote(md, newlineIdx)) {
return false;
}
if (nextLineIsBlockStarter(md, newlineIdx)) return true;
// CommonMark lazy continuation keeps non-`>` lines inside blockquotes, close on single `\n`.
if (prevLineIsBlockquote(md, newlineIdx) && !nextLineContinuesBlockquote(md, newlineIdx)) {
Expand Down
14 changes: 11 additions & 3 deletions src/app/plugins/markdown/htmlToMarkdown.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,17 @@ describe('htmlToMarkdown', () => {
});

it('converts blockquotes', () => {
const result = htmlToMarkdown('<blockquote>Quote text</blockquote>');
expect(result).toContain('>');
expect(result).toContain('Quote text');
expect(htmlToMarkdown('<blockquote>Quote text</blockquote>')).toBe('> Quote text');
expect(htmlToMarkdown('<blockquote><p>test</p></blockquote><p>test</p>')).toBe('> test\ntest');
expect(htmlToMarkdown('<blockquote><p>line one</p><p>line two</p></blockquote>')).toBe(
'> line one\n> line two'
);
expect(htmlToMarkdown('<blockquote><p>line one<br>line two</p></blockquote>')).toBe(
'> line one\n> line two'
);
expect(
htmlToMarkdown('<blockquote><p>first</p></blockquote><blockquote><p>second</p></blockquote>')
).toBe('> first\n\n> second');
});

it('converts unordered lists', () => {
Expand Down
72 changes: 55 additions & 17 deletions src/app/plugins/markdown/htmlToMarkdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,15 @@ function processNodes(nodes: ChildNode[]): string {
const prev = filtered[i - 1];
// Adjacent <p> blocks must become \n\n in markdown so the editor gets separate Slate
// paragraphs and marked emits <p> per block again on send (single \n would collapse).
if (
i > 0 &&
prev &&
isTag(prev) &&
isTag(cur) &&
prev.name.toLowerCase() === 'p' &&
cur.name.toLowerCase() === 'p'
) {
parts.push('\n');
if (i > 0 && prev && isTag(prev) && isTag(cur)) {
const prevTag = prev.name.toLowerCase();
const curTag = cur.name.toLowerCase();
if (
(prevTag === 'p' && curTag === 'p') ||
(prevTag === 'blockquote' && curTag === 'blockquote')
) {
parts.push('\n');
}
}
parts.push(processNode(cur));
}
Expand Down Expand Up @@ -333,19 +333,57 @@ function processParagraph(
return `${content}\n`;
}

function collectBlockquoteBodyLines(
node: Element,
listDepth: number,
insideCode: boolean
): string[] {
const lines: string[] = [];
const pushLine = (line: string) => {
lines.push(line);
};
const pushMultiline = (text: string) => {
for (const part of text.split('\n')) {
pushLine(part);
}
};

for (const child of node.children) {
if (isText(child)) {
if (/^\s*$/.test(child.data)) continue;
const text = insideCode ? child.data : escapeMarkdownInlineSequences(child.data);
pushMultiline(text);
continue;
}

if (!isTag(child)) continue;

const tag = child.name.toLowerCase();
if (tag === 'p') {
pushMultiline(processChildren(child.children, listDepth, insideCode));
} else if (tag === 'br') {
pushLine('');
} else if (tag === 'blockquote') {
lines.push(...collectBlockquoteBodyLines(child, listDepth, insideCode));
} else {
pushMultiline(processNode(child, listDepth, insideCode).trimEnd());
}
}

return lines;
}

function processBlockquote(
node: Element,
listDepth: number = 0,
insideCode: boolean = false
): string {
const content = node.children
.map((child) => {
if (isTag(child) && child.name === 'br') return '\n';
const text = processNode(child, listDepth, insideCode);
return text.replace(/\n/g, '\n> ');
})
.join('');
return `> ${content}\n`;
const marker = node.attribs['data-md'] ? `${node.attribs['data-md']} ` : '> ';
const lines = collectBlockquoteBodyLines(node, listDepth, insideCode);
const body = lines
.map((line) => (line.length === 0 ? marker.trimEnd() : `${marker}${line}`))
.join('\n');
return `${body}\n`;
}

/**
Expand Down
6 changes: 6 additions & 0 deletions src/app/plugins/markdown/markdownToHtml.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,12 @@ describe('markdownToHtml', () => {
expect(html).toContain('<blockquote>');
expect(html).toContain('line one');
expect(html).toContain('line two');
expect((html.match(/<blockquote/g) ?? []).length).toBe(1);
});

it('keeps three or more consecutive blockquote lines in one blockquote', () => {
const html = markdownToHtml('> test\n> test\n> test');
expect((html.match(/<blockquote/g) ?? []).length).toBe(1);
});

it('does not promote -# inside fenced code when the fence follows a single newline', () => {
Expand Down
Loading