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 @@ -24,6 +24,7 @@ Leafdown uses lightweight [Keep a Changelog](https://keepachangelog.com/en/1.1.0

### Fixed

- Write a backslash on save only where the character it precedes would otherwise be read as Markdown, so text such as `garden_sensor_name` keeps its underscores bare, instead of escaping every character that could be syntax somewhere else.
- Keep a list item that starts with a code block, table, quote, nested list, heading, or thematic break nested in the saved file, instead of writing an empty item and leaving the block outside the list the next time the document is opened.
- Keep typed link and autolink source literal in the saved file when a space follows it, instead of writing it as live Markdown that turns into a link the next time the document is opened.
- Open the Markdown source of a link whose label holds a footnote reference, instead of leaving it closed everywhere in the label except on the reference itself.
Expand Down
10 changes: 6 additions & 4 deletions src/features/editor/plugins/sourceProjection.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -492,14 +492,16 @@ describe("source projection", () => {

it("maps a selection through escaped text in a mixed-format link label", async () => {
const mounted = await mountProjectionEditor(
"[literal \\* and **bold**](https://example.com)",
"[\\*literal\\* and **bold**](https://example.com)",
);
const selectionFrom = getEditorTextPosition(mounted, "bold");

setTextSelection(mounted.view, selectionFrom, selectionFrom + "bold".length);

expect(hasActiveSourceProjection(mounted.view.state)).toBe(true);
expect(getEditorTextContent(mounted)).toBe("[literal \\* and **bold**](https://example.com)");
expect(getEditorTextContent(mounted)).toBe(
"[\\*literal\\* and **bold**](https://example.com)",
);
expect(getSelectedEditorText(mounted)).toBe("bold");
});

Expand Down Expand Up @@ -1030,7 +1032,7 @@ describe("source projection", () => {
setSelectionAtDocumentEnd(mounted.view);

expect(getEditorTextContent(mounted)).toContain("Text[^note");
expect(mounted.getMarkdown()).toContain("Text\\[^note");
expect(mounted.getMarkdown()).toBe("Text[^note\n\n[^note]: Detail\n");
});

it.each([
Expand Down Expand Up @@ -1171,7 +1173,7 @@ describe("source projection", () => {

const literal = "[^note";

expect(mounted.getMarkdown()).toContain("Text\\[^note");
expect(mounted.getMarkdown()).toBe("Text[^note\n\n[^note]: Detail\n");
expect(hasActiveSourceProjection(mounted.view.state)).toBe(false);
expect(mounted.view.state.selection.from).toBe(
getEditorTextPosition(mounted, literal) + literal.length,
Expand Down
64 changes: 61 additions & 3 deletions src/features/editor/tests/markdownCompatibility.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ describe("Markdown compatibility", () => {
"[**bold** plain](https://example.com)",
"[plain *soft* and ~~strike~~](https://example.com)",
"[plain `code` and **bold**](https://example.com)",
"[plain \\* literal and **bold**](https://example.com)",
"[plain \\*literal\\* and **bold**](https://example.com)",
'**[plain *soft*](https://example.com "Title")**',
"[plain **bold**]()",
"[a](https://example.com) [b](https://example.com)",
Expand Down Expand Up @@ -347,10 +347,68 @@ describe("Markdown compatibility", () => {
});
});

// Each fixture is the source that produces the document, so a case that must lose an escape is
// written with the one it loses.
describe("Escape precision", () => {
it.each([
{ saved: "garden_sensor_name", source: "garden\\_sensor\\_name" },
{ saved: "sensor.reading_value", source: "sensor.reading\\_value" },
{ saved: "foo__bar__baz", source: "foo\\_\\_bar\\_\\_baz" },
{ saved: "snake_case_ trailing", source: "snake\\_case\\_ trailing" },
{ saved: "*opening-only asterisk emphasis", source: "\\*opening-only asterisk emphasis" },
{ saved: "closing-only asterisk emphasis*", source: "closing-only asterisk emphasis\\*" },
{ saved: "a * b * c", source: "a \\* b \\* c" },
{ saved: "text with [ bracket", source: "text with \\[ bracket" },
{ saved: "text with ] bracket", source: "text with ] bracket" },
{
saved: "\\[intentionally literal](garden.md)",
source: "\\[intentionally literal]\\(garden.md)",
},
{
saved: "!\"#$%&'()*+,-./:;<=>?@\\[\\\\]^_\\`{|}\\~",
source:
"\\!\\\"\\#\\$\\%\\&\\'\\(\\)\\*\\+\\,\\-\\.\\/\\:\\;\\<\\=\\>\\?\\@\\[\\\\\\]\\^\\_\\`\\{\\|\\}\\~",
},
])("writes $saved without an escape it does not need", async ({ saved, source }) => {
const mounted = await mountEditor(`${source}\n`);

expect(mounted.getMarkdown()).toBe(`${saved}\n`);
});

it.each([
"\\*not emphasis\\*",
"\\*\\*not strong emphasis\\*\\*",
"\\_not emphasis\\_",
"\\_\\_not strong emphasis\\_\\_",
"\\* not a list item",
"\\*\\*\\*",
"\\_\\_\\_",
"\\# not a heading",
"\\> not a quote",
"\\- not a list item",
"\\[reference]\\[label]",
"\\[intentionally literal](garden.md)",
"!\\[intentionally literal](garden.png)",
"\\![literal bang before a live link](garden.png)",
"\\`not code\\`",
"\\~\\~not strikethrough\\~\\~",
"\\<span>not html\\</span>",
"| bed |\n| ----------- |\n| alpha\\|beta |",
"a \\ b",
"C:\\Users\\me",
"\\\\#",
"\\\\[",
])("keeps the escape the document needs in %j", async (source) => {
const mounted = await mountEditor(`${source}\n`);

expect(mounted.getMarkdown()).toBe(`${source}\n`);
});
});

describe("Typed link source", () => {
const typedLinkSourceFixtures = [
{
expected: "\\[test link]\\(./test.html)",
expected: "\\[test link](./test.html)",
name: "inline link",
typed: "[test link](./test.html)",
},
Expand Down Expand Up @@ -382,7 +440,7 @@ describe("Typed link source", () => {
},
);

it.each(["\\[test link]\\(./test.html)", "\\[test link]\\(./test.html) "])(
it.each(["\\[test link](./test.html)", "\\[test link](./test.html) "])(
"reloads a typed inline link as the text the editor presented in %j",
async (source) => {
const mounted = await mountEditor(source);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ describe("source projection clipboard slices", () => {

it.each([
{ expected: "*Paste*", html: "<p><em>Paste</em></p>", label: "emphasis" },
{ expected: "a\\*b", html: "<p>a*b</p>", label: "characters that mean something in source" },
{ expected: "\\*a\\*", html: "<p>*a*</p>", label: "characters that mean something in source" },
{ expected: "", html: "<ul><li>one</li><li>two</li></ul>", label: "a list" },
{ expected: "", html: "<p>one</p><p>two</p>", label: "two paragraphs" },
])(
Expand Down
Loading