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
78 changes: 64 additions & 14 deletions packages/resolve/src/pipeline/canvasMarkdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,25 +48,70 @@ export function canvasBlockMarkdown(markdown: string): string {
out += body;
blocks += 1;
};
for (const token of lexer.lexer(text.slice(leading))) {
if (token.type === 'space') {
// between blocks: newlines, and lines of spaces, which the editor shows as lines of their own
for (const [i, part] of token.raw.split('\n').entries()) {
if (i > 0) {
gap += 1;
}
if (part !== '') {
block(spacesLine(part));
gap = 0;
const body = text.slice(leading, text.length - trailingNewlines(text));
for (const piece of splitAtEmptyLines(body)) {
if (blocks > 0) {
gap = piece.newlinesBefore;
}
let pieceStart = true;
for (const token of lexer.lexer(piece.text)) {
if (token.type === 'space') {
// between blocks: newlines, and lines of spaces, which the editor shows as lines of their own. Spaces right
// after a block, on its line, are that line's own (Marked can split an empty list item's `- ` that way), unless a piece starts with them
for (const [i, part] of token.raw.split('\n').entries()) {
if (i > 0) {
gap += 1;
}
if (part !== '' && (i > 0 || blocks === 0 || pieceStart)) {
block(spacesLine(part));
gap = 0;
}
}
pieceStart = false;
continue;
}
pieceStart = false;
const raw = token.raw.replace(/\n+$/, '');
block(raw);
gap = token.raw.length - raw.length;
}
}
// newlines at the end: an empty paragraph each, counted in the text, not in the last token, which Marked can end with
// a newline the text does not have (a list whose last item is an empty `- `)
return blocks > 0 ? out + `\n\n${EMPTY_LINE}`.repeat(trailingNewlines(text)) : '';
}

/**
* The text in pieces, cut at each run of three or more newlines outside a code fence, with the count of newlines
* before each piece. Marked keeps a list going across empty lines (one loose list, the empty lines gone), where the
* text has two lists with empty paragraphs between them: each piece is lexed alone, and the run is the gap between.
*/
function splitAtEmptyLines(text: string): Array<{ text: string; newlinesBefore: number }> {
const pieces: Array<{ text: string; newlinesBefore: number }> = [];
let lines: string[] = [];
let newlinesBefore = 0;
let empty = 0;
let fenced = false;
for (const line of text.split('\n')) {
if (line === '' && !fenced) {
empty += 1;
continue;
}
const body = token.raw.replace(/\n+$/, '');
block(body);
gap = token.raw.length - body.length;
if (empty >= 2 && lines.length > 0) {
pieces.push({ text: lines.join('\n'), newlinesBefore });
lines = [];
newlinesBefore = empty + 1;
} else {
lines.push(...Array.from({ length: empty }, () => ''));
}
empty = 0;
if (/^ {0,3}(`{3,}|~{3,})/.test(line)) {
fenced = !fenced;
}
lines.push(line);
}
return blocks > 0 ? out + `\n\n${EMPTY_LINE}`.repeat(gap) : '';
pieces.push({ text: lines.join('\n'), newlinesBefore });
return pieces;
}

/**
Expand Down Expand Up @@ -134,6 +179,11 @@ export function unescapeNewlines(markdown: string): string {
* The newlines before the first character that is not a newline: an empty line each, as the canvas editor reads them
* (its countLeadingLineBreaks). A text of newlines only has none: those are trailing.
*/
/** The newlines after the last character that is not a newline. */
function trailingNewlines(text: string): number {
return /\n*$/.exec(text)?.[0].length ?? 0;
}

function leadingNewlines(text: string): number {
return /^\n+(?=[^\n])/.exec(text)?.[0].length ?? 0;
}
Expand Down
18 changes: 18 additions & 0 deletions packages/resolve/test/sanitize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,24 @@ describe('canvas newline rule', () => {
expect(s.markdown('').html).toBe('');
});

it('block: a list ending in an empty item draws no empty line after it', () => {
expect(s.markdown('- a\n- ').html).toBe('<ul><li>a</li><li></li></ul>');
expect(s.markdown('- a\n-').html).toBe('<ul><li>a</li><li></li></ul>');
expect(s.markdown('- a\n- \n').html).toBe(`<ul><li>a</li><li></li></ul><p>${E}</p>`);
expect(s.markdown('- a\n').html).toBe(`<ul><li>a</li></ul><p>${E}</p>`);
});

it('block: empty lines between two lists are drawn, not merged into one list', () => {
expect(s.markdown('- a\n\n\n- b').html).toBe(
`<ul><li>a</li></ul><p>${E}</p><ul><li>b</li></ul>`,
);
expect(s.markdown('- a\n\n\n\n\n\n- ').html).toBe(
`<ul><li>a</li></ul><p>${E}</p><p>${E}</p><p>${E}</p><p>${E}</p><ul><li></li></ul>`,
);
expect(s.markdown('```\na\n\n\nb\n```').html).toContain('a\n\n\nb');
expect(s.markdown('a\n\n\n ').html).toBe(`<p>a</p><p>${E}</p><p> ${E}</p>`);
});

it('block: a list item breaks its own lines, not before the next item', () => {
const out = s.markdown('- a\nstill a\n- b').html;
expect(out).toContain('a<br />still a');
Expand Down
Loading