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
6 changes: 6 additions & 0 deletions .changeset/list-blank-before.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@meowdown/markdown': minor
'@meowdown/core': minor
---

Keep the blank lines between list items and around nested lists as written.
1 change: 1 addition & 0 deletions packages/core/src/converters/ast-to-pm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export function astToDoc(
marker: node.marker ?? null,
taskMarker: node.taskMarker,
markerGap: node.markerGap,
blankBefore: node.blankBefore ?? null,
},
children,
)
Expand Down
15 changes: 15 additions & 0 deletions packages/core/src/converters/md-to-pm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ describe('markdownToDoc', () => {
marker: '-',
taskMarker: null,
markerGap: 1,
blankBefore: null,
},
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'one' }] }],
},
Expand All @@ -225,6 +226,7 @@ describe('markdownToDoc', () => {
marker: '-',
taskMarker: null,
markerGap: 1,
blankBefore: false,
},
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'two' }] }],
},
Expand Down Expand Up @@ -276,6 +278,7 @@ describe('markdownToDoc', () => {
marker: '.',
taskMarker: null,
markerGap: 1,
blankBefore: null,
},
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'five' }] }],
},
Expand All @@ -289,6 +292,7 @@ describe('markdownToDoc', () => {
marker: '.',
taskMarker: null,
markerGap: 1,
blankBefore: false,
},
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'six' }] }],
},
Expand All @@ -311,6 +315,7 @@ describe('markdownToDoc', () => {
marker: '-',
taskMarker: null,
markerGap: 1,
blankBefore: null,
},
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'todo' }] }],
},
Expand All @@ -333,6 +338,7 @@ describe('markdownToDoc', () => {
marker: '-',
taskMarker: 'x',
markerGap: 1,
blankBefore: null,
},
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'done' }] }],
},
Expand All @@ -356,6 +362,7 @@ describe('markdownToDoc', () => {
marker: '-',
taskMarker: 'x',
markerGap: 1,
blankBefore: null,
},
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Square Task' }] }],
},
Expand All @@ -369,6 +376,7 @@ describe('markdownToDoc', () => {
marker: '+',
taskMarker: null,
markerGap: 1,
blankBefore: true,
},
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'Circle Task' }] }],
},
Expand Down Expand Up @@ -405,6 +413,7 @@ describe('markdownToDoc', () => {
marker: '.',
taskMarker: null,
markerGap: 1,
blankBefore: null,
},
content: [{ type: 'paragraph', content: [{ type: 'text', text: '[x] done' }] }],
},
Expand Down Expand Up @@ -458,6 +467,7 @@ describe('markdownToDoc', () => {
marker: '-',
taskMarker: null,
markerGap: 1,
blankBefore: null,
},
content: [
{ type: 'paragraph', content: [{ type: 'text', text: 'x' }] },
Expand Down Expand Up @@ -685,6 +695,7 @@ describe('markdownToDoc', () => {
marker: '*',
taskMarker: null,
markerGap: 1,
blankBefore: null,
},
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'star' }] }],
},
Expand All @@ -707,6 +718,7 @@ describe('markdownToDoc', () => {
marker: ')',
taskMarker: null,
markerGap: 1,
blankBefore: null,
},
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'paren' }] }],
},
Expand All @@ -729,6 +741,7 @@ describe('markdownToDoc', () => {
marker: '.',
taskMarker: null,
markerGap: 1,
blankBefore: null,
},
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'zero' }] }],
},
Expand All @@ -751,6 +764,7 @@ describe('markdownToDoc', () => {
marker: '-',
taskMarker: 'X',
markerGap: 1,
blankBefore: null,
},
content: [{ type: 'paragraph', content: [{ type: 'text', text: 'upper' }] }],
},
Expand Down Expand Up @@ -989,6 +1003,7 @@ describe('markdownToDoc', () => {
marker: '-',
taskMarker: null,
markerGap: 1,
blankBefore: null,
},
content: [
{ type: 'paragraph', content: [{ type: 'text', text: 'x' }] },
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/converters/pm-to-ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ function readBlock(node: ProseMirrorNode): MarkdownBlock {
marker: attrs.marker ?? undefined,
taskMarker: attrs.taskMarker ?? undefined,
markerGap: attrs.markerGap,
blankBefore: attrs.blankBefore ?? undefined,
children: readBlocks(node),
}
}
Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/converters/pm-to-md.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,15 @@ describe('docToMarkdown', () => {
expect(markdown).toBe('- first\n\n second\n\n- third\n')
})

it('continues the gap of the item above it', () => {
const doc = n.doc(
n.list({ kind: 'bullet' }, n.paragraph('a')),
n.list({ kind: 'bullet', blankBefore: true }, n.paragraph('b')),
n.list({ kind: 'bullet' }, n.paragraph('c')),
)
expect(docToMarkdown(doc)).toBe('- a\n\n- b\n\n- c\n')
})

it('keeps a blank line after a list', () => {
const doc = n.doc(n.list({ kind: 'bullet' }, n.paragraph('item')), n.paragraph('after'))
const markdown = docToMarkdown(doc)
Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/converters/roundtrip.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@ describe('bullet lists', () => {
expect(roundtrip('- a\n - deep')).toBe('- a\n - deep\n')
})

it.fails('keeps a loose list', () => {
it('keeps a loose list', () => {
const md = ['- a', '', '- b'].join('\n')
expect(roundtrip(md)).toBe(md + '\n')
})
Expand All @@ -466,12 +466,12 @@ describe('bullet lists', () => {
expect(roundtrip('- a\n- \n- b')).toBe('- a\n-\n- b\n')
})

it.fails('keeps a tight sibling after a two-block item', () => {
it('keeps a tight sibling after a two-block item', () => {
const md = ['- a', '', ' b', '- c'].join('\n')
expect(roundtrip(md)).toBe(md + '\n')
})

it.fails('keeps a nested list right under its paragraph in a loose list', () => {
it('keeps a nested list right under its paragraph in a loose list', () => {
const md = ['- a', '', ' b', '- c', ' - d'].join('\n')
expect(roundtrip(md)).toBe(md + '\n')
})
Expand Down Expand Up @@ -1071,7 +1071,7 @@ describe('mixed structures', () => {
expect(roundtrip('# h1\n## h2\n### h3')).toBe('# h1\n## h2\n### h3\n')
})

it.fails('keeps a list right under a paragraph', () => {
it('keeps a list right under a paragraph', () => {
expect(roundtrip('para\n- a')).toBe('para\n- a\n')
})

Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/converters/sample-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export const sampleContent: NodeJSON = {
marker: '-',
taskMarker: null,
markerGap: 1,
blankBefore: true,
},
content: [
{
Expand All @@ -89,6 +90,7 @@ export const sampleContent: NodeJSON = {
marker: '-',
taskMarker: null,
markerGap: 1,
blankBefore: false,
},
content: [
{
Expand All @@ -105,6 +107,7 @@ export const sampleContent: NodeJSON = {
marker: '-',
taskMarker: null,
markerGap: 1,
blankBefore: false,
},
content: [
{
Expand All @@ -130,6 +133,7 @@ export const sampleContent: NodeJSON = {
marker: '.',
taskMarker: null,
markerGap: 1,
blankBefore: true,
},
content: [
{
Expand All @@ -148,6 +152,7 @@ export const sampleContent: NodeJSON = {
marker: '.',
taskMarker: null,
markerGap: 1,
blankBefore: false,
},
content: [
{
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/extensions/extension.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ describe('defineEditorExtension', () => {
},
{
"attrs": {
"blankBefore": null,
"checked": false,
"collapsed": false,
"kind": "bullet",
Expand All @@ -94,6 +95,7 @@ describe('defineEditorExtension', () => {
},
{
"attrs": {
"blankBefore": null,
"checked": false,
"collapsed": false,
"kind": "ordered",
Expand All @@ -117,6 +119,7 @@ describe('defineEditorExtension', () => {
},
{
"attrs": {
"blankBefore": null,
"checked": false,
"collapsed": false,
"kind": "task",
Expand Down
18 changes: 18 additions & 0 deletions packages/core/src/extensions/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export interface MeowdownListAttrs extends ListAttrs {
marker?: ListMarker | undefined
taskMarker?: TaskMarker | undefined
markerGap?: number | undefined
blankBefore?: boolean | null | undefined
}

// Each attr persists only its non-canonical values (`.`, `-`, `x`, and a gap
Expand Down Expand Up @@ -165,6 +166,22 @@ function defineListMarkerGapAttr(): ListMarkerGapExtension {
})
}

type ListBlankBeforeExtension = Extension<{
Nodes: { list: { blankBefore?: boolean | null | undefined } }
}>

/**
* Whether one blank line separated the item from the sibling before it in the
* Markdown source. `null` leaves the gap to the serializer.
*/
function defineListBlankBeforeAttr(): ListBlankBeforeExtension {
return defineNodeAttr<'list', 'blankBefore', boolean | null>({
type: 'list' satisfies NodeName,
attr: 'blankBefore',
default: null,
})
}

function getListClipboardAttributes(node: ProseMirrorNode): Record<string, string | undefined> {
const attrs = node.attrs as MeowdownListAttrs
return {
Expand Down Expand Up @@ -529,6 +546,7 @@ export function defineMeowdownList() {
defineListMarkerAttr(),
defineListTaskMarkerAttr(),
defineListMarkerGapAttr(),
defineListBlankBeforeAttr(),
defineMeowdownListCommands(),
)
}
6 changes: 0 additions & 6 deletions packages/core/src/extensions/wililink-fuzz.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,6 @@ describe('caret fuzz over a wikilink outline in focus mode', () => {
- [[bar]]
----- markdown after -------------
top

- [[foo]]
- [[bar]]
----- selection before/after -----
Expand Down Expand Up @@ -204,7 +203,6 @@ describe('caret fuzz over a wikilink outline in focus mode', () => {
- top

[[foo]]

- [[bar]]
----- selection before/after -----
top
Expand Down Expand Up @@ -359,7 +357,6 @@ describe('caret fuzz over a wikilink outline in focus mode', () => {
- [[bar]]
----- markdown after -------------
- top

- [[foo]]

[[bar]]
Expand Down Expand Up @@ -1330,7 +1327,6 @@ describe('caret fuzz over a wikilink outline in hide mode', () => {
- [[bar]]
----- markdown after -------------
top

- [[foo]]
- [[bar]]
----- selection before/after -----
Expand Down Expand Up @@ -1405,7 +1401,6 @@ describe('caret fuzz over a wikilink outline in hide mode', () => {
- top

[[foo]]

- [[bar]]
----- selection before/after -----
top
Expand Down Expand Up @@ -1560,7 +1555,6 @@ describe('caret fuzz over a wikilink outline in hide mode', () => {
- [[bar]]
----- markdown after -------------
- top

- [[foo]]

[[bar]]
Expand Down
17 changes: 10 additions & 7 deletions packages/markdown/src/ast/check-roundtrip.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,20 +121,23 @@ const EXACT_CASES: string[] = [

// a no-break space is heading text, not the gap around a closing `#` run
'# #\u{A0}',
]

const NORMALIZING_CASES: string[] = [
// a loose list serializes tight
// a loose list stays loose, and a tight list after a two-block item tight
'- a\n\n- b',
'1. a\n\n2. b',
'- a\n\n b\n- c',

// a list starts right under a paragraph, in a blockquote too
'para\n- a',
'> text\n> - item',
'> a\n> - x\n> - y',
]

const NORMALIZING_CASES: string[] = [
// the items of an ordered list are numbered in sequence
'1. a\n1. b\n1. c',
'3) a\n7) b',

// a blockquote gains an empty `>` line before a following list
'> text\n> - item',
'> a\n> - x\n> - y',

// a lazy continuation gains the canonical item indent
'- [ ] todo\neen voorlopig idee',
'- item\nlazy line',
Expand Down
3 changes: 2 additions & 1 deletion packages/markdown/src/ast/equal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ function isListItemEqual(a: MarkdownListItem, b: MarkdownListItem): boolean {
a.collapsed === b.collapsed &&
a.marker === b.marker &&
a.taskMarker === b.taskMarker &&
a.markerGap === b.markerGap
a.markerGap === b.markerGap &&
a.blankBefore === b.blankBefore
)
}

Expand Down
Loading
Loading