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 @@ -68,6 +68,7 @@ Leafdown uses lightweight [Keep a Changelog](https://keepachangelog.com/en/1.1.0
- Apply that same precise escaping to text held inside bold, italic, strikethrough, or a link label, so literal text such as `**\[a](b)**` keeps the one backslash it needs and an underscore inside bold stays bare, instead of gaining the extra backslashes that ordinary text no longer collects.
- Leave a link or image destination that balances its own parentheses bare on save, so a path such as `garden(section(one)).md` keeps its parentheses as written, instead of saving `garden\(section\(one\)\).md`. A parenthesis that would close the destination early still keeps its backslash.
- Leave a block marker bare where the line it opens cannot form the construct, so text such as `#no separator`, `####### seven hashes`, `1234567890. ten digits`, a list starting at two that follows a paragraph line, or a pipe row that no matching delimiter row follows keeps its markers as written, instead of carrying a backslash for a heading, list, or table the line never spells. Hashes followed by a space, a list starting at one that interrupts a paragraph, and a pipe row a matching delimiter row follows all still keep their backslash.
- Leave a run of `*` or `_` too short for a horizontal rule bare on save, so a line holding `**`, `_`, or `__` keeps its markers as written, instead of carrying a backslash for a rule that needs three of them. A line spending three or more, spaced or not, still keeps its backslash.
- Leave an ampersand in a link or image destination bare where it starts no character reference, so a destination such as `a.md?x=1&y=2` keeps its query as written, instead of saving `a.md?x=1\&y=2`. A destination ampersand that does begin a reference still keeps its backslash, and a link label holding a literal `©` now keeps its own backslash even where the destination drops one.
- 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.
- Escape text the editor keeps literal even when a space follows it, instead of writing it as live Markdown that turns into something else the next time the document is opened.
Expand Down
27 changes: 26 additions & 1 deletion src/features/editor/tests/markdownCompatibility.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,20 @@ describe("Escape precision", () => {
},
// A marker with nothing between it and its content opens no list item at all.
{ saved: "does not interrupt:\n1)item", source: "does not interrupt:\n1\\)item" },
// A thematic break spends three markers, so a shorter run is ordinary text wherever it sits.
{ saved: "**", source: "\\*\\*" },
{ saved: "_", source: "\\_" },
{ saved: "__", source: "\\_\\_" },
{ saved: "_ _", source: "\\_ \\_" },
{ saved: "a paragraph line\n**", source: "a paragraph line\n\\*\\*" },
// The three are counted across the line rather than inside one run of it, and only the run
// the line starts with stands where the break could open.
{ saved: "\\*\\* *", source: "\\*\\* \\*" },
{ saved: "\\* * *", source: "\\* \\* \\*" },
{ saved: "\\_ _ _", source: "\\_ \\_ \\_" },
// An underscore opens no bullet marker, so a run standing off a span of its own character is
// held by nothing once the line it shares spells fewer than three markers.
{ saved: "_ __a__", source: "\\_ __a__" },
// A table needs a delimiter row whose cell count matches the header row above it.
{
saved: "| Header | Cells |\n| --- |\n| mismatch | stays text |",
Expand Down Expand Up @@ -719,6 +733,8 @@ describe("Escape precision", () => {
"1\\. not a list item",
"\\*\\*\\*",
"\\_\\_\\_",
// A thematic break interrupts a paragraph, so a continuation line spending three needs it too.
"a paragraph line\n\\*\\*\\*",
"\\# not a heading",
"\\> not a quote",
"\\- not a list item",
Expand Down Expand Up @@ -760,7 +776,6 @@ describe("Escape precision", () => {
// Whitespace between a run and the span beside it keeps them two runs rather than one, so the
// run is still a bullet marker on its own and the span is not what holds it literal.
"\\* **a**",
"\\_ __a__",
// The enclosing delimiters remain available counterparts for a run of the same character.
"**\\*a**",
"**\\*opening-only asterisk**",
Expand Down Expand Up @@ -823,6 +838,16 @@ describe("Escape precision", () => {
"**strong***trailing",
"x***strong**",
"x___strong__",
// A marker run too short to open a thematic break reopens as its own text whether or not a
// backslash holds it, which puts it in the same blind spot.
"**",
"_",
"__",
"_ _",
"_ __a__",
"\\*\\*\\*",
"\\_\\_\\_",
"\\* * *",
])("writes %j as authored and reopens it as the same document", async (source) => {
const mounted = await mountEditor(`${source}\n`);
const document: unknown = mounted.view.state.doc.toJSON();
Expand Down
7 changes: 6 additions & 1 deletion src/features/editor/utils/markdownText.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,12 @@ const LINE_ENDING_PATTERN = /[\r\n]$/u;
const ESCAPABLE_PATTERN = /[!-/:-@[-`{-~]/u;
const UNICODE_PUNCTUATION_PATTERN = /[\p{P}\p{S}]/u;
const ATTENTION_CHARACTERS = "*_~";
const THEMATIC_BREAK_PATTERNS: Record<string, RegExp> = { "*": /^[*\t ]*$/u, _: /^[_\t ]*$/u };
// A thematic break spends three markers or more, counted across the line rather than inside one
// run of it, and admits nothing else but spaces and tabs.
const THEMATIC_BREAK_PATTERNS: Record<string, RegExp> = {
"*": /^(?:[\t ]*\*){3,}[\t ]*$/u,
_: /^(?:[\t ]*_){3,}[\t ]*$/u,
};
const WHOLE_LINE_PHRASING_PARENTS = new Set(["heading", "paragraph", "tableCell"]);
// A mark holds only a fragment of its line, so its siblings cannot answer what follows the mark.
// Every `[` in the fragment keeps its escape, which is also what makes an unescaped `[` from
Expand Down