Skip to content
Open
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
14 changes: 14 additions & 0 deletions e2e/table.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { expect, md, test } from './helpers';

test('typing GFM table markdown creates a table without escaping its delimiter', async ({ page }) => {
await page.goto('/?md=');
const editor = page.locator('#editor');
await editor.click();
await page.keyboard.type('| test | table | row |');
await page.keyboard.press('Shift+Enter');
await page.keyboard.type('| :--- | :--- | :--- |');
await page.keyboard.press('Shift+Enter');
await page.keyboard.type('| test | data | here |');

await expect.poll(() => md(page)).toBe('| test | table | row |\n| :-- | :-- | :-- |\n| test | data | here |\n');
});
20 changes: 19 additions & 1 deletion packages/editor/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,9 @@ import {
keepQuoteStart,
} from './model';
import { emailLiterals, isLiteralBoundary, urlLiteralAt } from './markdown/autolink';
import { tableRowWidth } from './markdown/blocks';
import type { InlineSyntax } from './markdown/options';
import { parseMarkdown } from './markdown/parse';
import { isExcluded } from './decorations';

export type Command = (state: EditorState) => EditorState;
Expand Down Expand Up @@ -1267,7 +1269,7 @@ export function moveCell(state: EditorState, delta: 1 | -1): EditorState {

/**
* Block shortcuts typed at the start of a paragraph, e.g. "## " turns it into
* a level 2 heading. Runs after a space or "-" is typed. Returns null when no
* a level 2 heading. Runs after a space, "-" or "|" is typed. Returns null when no
* rule applies.
*/
const BLOCK_RULES: [RegExp, (m: RegExpExecArray) => BlockTypeSpec][] = [
Expand All @@ -1293,6 +1295,22 @@ export function applyBlockInputRules(state: EditorState, dialect: { callouts?: b
if (!isTextBlock(block) || pos.cell) return null;
const before = inlineText(block.content).slice(0, pos.offset);

if (
block.type === 'paragraph' &&
pos.offset === inlineLength(block.content) &&
before.endsWith('|') &&
before.lastIndexOf('\n') > before.indexOf('\n')
) {
const parsed = parseMarkdown(before).blocks;
const parsedTable = parsed.length === 1 && parsed[0]?.type === 'table' && parsed[0].rows.length > 1 ? parsed[0] : null;
const lastLine = before.slice(before.lastIndexOf('\n') + 1);
if (parsedTable && tableRowWidth(lastLine) === parsedTable.align.length) {
const next = { ...parsedTable, id: block.id };
const cell = { row: next.rows.length - 1, col: next.align.length - 1 };
return replaceBlock(state, next, caret(next.id, inlineLength(next.rows[cell.row]![cell.col]!), cell));
}
}

if ((block.type === 'paragraph' || block.type === 'quote') && before === '---') {
// A divider in a quote is in the quote.
const index = blockIndex(state.doc, block.id);
Expand Down
2 changes: 1 addition & 1 deletion packages/editor/src/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2095,7 +2095,7 @@ export class Editor {
} else {
// Shortcuts are separate undo steps, so undo restores the typed characters.
const typed = this.state;
if (text === ' ' || text === '-') this.run((s) => cmd.applyBlockInputRules(s, this.markdown));
if (text === ' ' || text === '-' || text === '|') this.run((s) => cmd.applyBlockInputRules(s, this.markdown));
if (text.length === 1 && '*_`~'.includes(text)) this.run((s) => cmd.applyInlineInputRules(s));
if (this.markdown.inline.length > 0) this.run((s) => cmd.applyTokenInputRules(s, this.markdown.inline) ?? s);
if (text === ' ' && this.markdown.autolinks) this.run((s) => cmd.applyAutolinkInputRule(s) ?? s);
Expand Down
5 changes: 5 additions & 0 deletions packages/editor/src/markdown/blocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1014,6 +1014,11 @@ function splitRow(text: string, line = 0, offset = 0, pad = 0): Cell[] {
return cells;
}

/** Number of cells written in a table row, before short rows are padded. */
export function tableRowWidth(text: string): number {
return splitRow(text).length;
}

/** A cell from the text between its pipes and the pieces of it written as such: the whitespace around the text is trimmed off, and so are the pieces in it. */
function cell(value: string, pieces: readonly ContentLine[]): Cell {
let lead = 0;
Expand Down
24 changes: 21 additions & 3 deletions packages/editor/test/commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -519,17 +519,19 @@ describe('tables', () => {
});

describe('block shortcuts', () => {
const typed = (text: string) => {
const typedState = (text: string) => {
let s = at(state(paragraph()), 0, 0);
for (const ch of text) {
s = cmd.insertText(s, ch);
if (ch === ' ' || ch === '-') s = cmd.applyBlockInputRules(s) ?? s;
if (ch === ' ' || ch === '-' || ch === '|') s = cmd.applyBlockInputRules(s) ?? s;
}
return show(s);
return s;
};
const typed = (text: string) => show(typedState(text));

it.each([
['## Hi', ['h2:Hi']],
['| h1 | h2 |\n| :--- | ---: |\n| a | b |', ['table:h1|h2/a|b']],
['- item', ['ul0:item']],
['* item', ['ul0:item']],
['1. item', ['ol0:item']],
Expand All @@ -543,6 +545,22 @@ describe('block shortcuts', () => {
expect(typed(input)).toEqual(expected);
});

it('keeps the caret at the end of the typed table', () => {
const s = typedState('| h1 | h2 |\n| --- | --- |\n| a | b |');

expect(show(s)).toEqual(['table:h1|h2/a|b']);
expect(s.selection.focus).toEqual({ blockId: s.doc.blocks[0]!.id, offset: 1, cell: { row: 1, col: 1 } });
});

it.each([
['an unfinished delimiter', '| h1 | h2 |\n| :--- | ---:'],
['a table without a body row', '| h1 | h2 |\n| :--- | ---: |'],
['a standalone delimiter', '| --- | --- |'],
['fewer delimiter cells than header cells', '| h1 | h2 |\n| --- |'],
])('keeps %s as a paragraph', (_name, markdown) => {
expect(typed(markdown)).toEqual([`p:${markdown}`]);
});

it('keeps the start number of an ordered list', () => {
let s = at(state(paragraph()), 0, 0);
s = type(s, '3. ');
Expand Down
Loading