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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Leafdown uses lightweight [Keep a Changelog](https://keepachangelog.com/en/1.1.0

- Keep the form a heading was written in, so `# Level one #` keeps the hashes closing it instead of losing them on the first save, and a heading underlined with `=` or `-` stays underlined instead of being rewritten as `#`. The underline keeps the length it was written at, and the spaces or tabs between an opening `#` run and the heading text are kept too. A heading made in the editor is still written with `#` and nothing closing it, and so is one moved past the levels an underline can carry or written where the underline would be read as part of the paragraph above it.
- Keep that form when a formatting command changes a heading's level, so `Heading 1` through `Heading 6` leave a heading exactly as `Increase heading level` and `Decrease heading level` already did. A heading underlined with `=` stays underlined when `Heading 2` moves it, and `# Level one #` keeps the hash closing it when `Heading 3` moves it, instead of arriving as a rewritten heading a gesture before a save would have written one. A heading turned into a paragraph and made a heading again is a heading the editor made, and is written as one.
- Remove a fenced code block with `Code block` whatever language it names. The command deleted the language instead of removing the block, so a block opened with ` ```ts ` came back opened by a bare fence and the language was gone from the file on the next save, with nowhere to type it back. Running the same command over a selection that holds a paragraph beside a code block no longer clears that block's language either.
- Turn an ordered list into a bullet list when `Unordered list` asks for one. The command reported success and left the list ordered, replacing the `)` it was written with by `.` and adding a blank line between every item on the way. Both list commands now leave the spaces between a marker and its content, and the blank lines between items, exactly as they were, and write the default marker for the list they make, because a bullet list has no ordered delimiter to carry and an ordered list no bullet.
- Keep the bullet a list was written with, so a file authored with `-` no longer comes back with its lists rewritten into a mixture of `*` and `-` that follows the order the lists appear in, and a `+` list stays a `+` list. An ordered list keeps its own delimiter and the numbers its items were written with, so `3.` followed by `8.` is no longer renumbered to `3.` and `4.`, and `4)` no longer becomes `4.`. The spaces between a marker and its content are kept too, along with an item whose content was written on the line after its marker. A list made in the editor is still written with `*`, or `.` when it is ordered, and two lists that meet with the same marker are still written apart, because Markdown reads them back as one list.
- Show a table written with a header row and no body rows as the table it is, instead of adding an empty row beneath it that holds no cells and takes no text.
Expand Down
1 change: 1 addition & 0 deletions docs/specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ For editor input and clipboard ownership, see [Architecture](./architecture.md#e
- `Increase heading level` moves `Heading 1` toward `Heading 6`; `Decrease heading level` moves `Heading 6` toward `Heading 1`.
- A block command keeps the form the block it changes was authored in wherever that block stays the same construct, so `Heading 1` through `Heading 6` leave a heading's form exactly as `Increase heading level` and `Decrease heading level` do. A block that becomes another construct is written in the default form for what it becomes, and one that becomes a paragraph and a heading again is a heading the editor made.
- `Ordered list` and `Unordered list` convert a list of the other kind on that rule. The marker and the numbers belonged to the list that is gone, so the one they make is written in the default form; each item keeps the padding and the opening line it holds itself, and the list keeps its tightness, which the conversion does not ask about.
- A code block's language is content the block carries rather than a format a command asks for, so `Code block` removes a block whatever its info string, and no block command sets or clears one.
- `Clear block formatting` converts the selected blocks, or the current block, to paragraphs when applicable.
- Insert commands add new content after the current block, or after the last selected block when the selection spans multiple blocks.
- The `Image` insert command inserts `![]()` and places the caret inside the parentheses.
Expand Down
27 changes: 27 additions & 0 deletions src/features/editor/commands/formatting/blocks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,31 @@ describe("editor block formatting commands", () => {
expect(decreaseListIndent(mounted.view)).toBe(true);
expect(mounted.view.dom.querySelectorAll("ul > li")).toHaveLength(2);
});

// A code block's own toggle is the command that removes it, and an info string is not a second
// kind of code block. The saved bytes are asserted beside the node because a toggle that only
// deleted the info string left a file the language alone tells from the one a working toggle
// writes.
it.each(["```ts\nconst a = 1;\n```\n", "```\nconst a = 1;\n```\n"])(
"toggles the code block in %j off whatever its info string",
async (source) => {
const mounted = await mountEditor(source);

setTextSelection(mounted.view, 3);

expect(toggleCodeBlock(mounted.view)).toBe(true);
expect(mounted.view.state.doc.child(0).type.name).toBe("paragraph");
expect(mounted.getMarkdown()).toBe("const a = 1;\n");
},
);

// One command reaches every block in the selection, so a paragraph beside a code block gains the
// format while the block that already carries one keeps the language it holds.
it("keeps a code block's info string where the same command reaches a paragraph beside it", async () => {
const mounted = await mountEditor("Paragraph\n\n```ts\nconst a = 1;\n```\n");

expect(selectAll(mounted.view)).toBe(true);
expect(toggleCodeBlock(mounted.view)).toBe(true);
expect(mounted.getMarkdown()).toBe("```\nParagraph\n```\n\n```ts\nconst a = 1;\n```\n");
});
});
7 changes: 5 additions & 2 deletions src/features/editor/commands/formatting/blocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,8 +376,11 @@ export const toggleBlockquote = (view: EditorView) => {
: runProseMirrorCommand(view, wrapIn(blockquoteType));
};

export const toggleCodeBlock = (view: EditorView) =>
toggleTextBlockType(view, "code_block", { language: "" });
// The attributes a toggle names are the ones the command asks for: they say which blocks it reads
// as already carrying the format, and they are written over a block that already has it. A code
// block's language is neither. It is content the block carries, no command asks for a particular
// one, and the schema opens a new block without one anyway.
export const toggleCodeBlock = (view: EditorView) => toggleTextBlockType(view, "code_block");

export const clearBlockFormat = (view: EditorView) => {
const paragraphType = getNodeType(view.state, "paragraph");
Expand Down