Skip to content
Closed
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
43 changes: 39 additions & 4 deletions packages/remark-stringify/lib/index.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
/**
* @import {Root} from 'mdast'
* @import {Options as ToMarkdownOptions} from 'mdast-util-to-markdown'
* @import {Break, Parents, Root} from 'mdast'
* @import {Info, Options as ToMarkdownOptions, State} from 'mdast-util-to-markdown'
* @import {Processor} from 'unified'
*/

/**
* @typedef {Omit<ToMarkdownOptions, 'extensions'>} Options
*/

import {toMarkdown} from 'mdast-util-to-markdown'
import {defaultHandlers, toMarkdown} from 'mdast-util-to-markdown'

/**
* Add support for serializing to markdown.
Expand All @@ -34,7 +34,42 @@ export default function remarkStringify(options) {
// Note: this option is not in the readme.
// The goal is for it to be set by plugins on `data` instead of being
// passed by users.
extensions: self.data('toMarkdownExtensions') || []
extensions: [
{handlers: {break: hardBreak}},
...(self.data('toMarkdownExtensions') || [])
]
})
}
}

/**
* Serialize a `break`.
*
* CommonMark cannot represent a break at the end of phrasing, so trailing
* breaks are omitted instead of emitting a `\` that would parse as text.
*
* @param {Break} node
* Break to serialize.
* @param {Parents | undefined} parent
* Parent of `node`.
* @param {State} state
* Info passed around.
* @param {Info} info
* Info on the surrounding of the node.
* @returns {string}
* Serialized markdown.
*/
function hardBreak(node, parent, state, info) {
const siblings = /** @type {Array<{type: string}>} */ (
/** @type {Parents} */ (parent).children
)
let index = siblings.indexOf(node)

while (++index < siblings.length) {
if (siblings[index].type !== 'break') {
return defaultHandlers.break(node, parent, state, info)
}
}

return ''
}
77 changes: 77 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,83 @@ test('remark-stringify', async function (t) {
)
})

await t.test(
'should not serialize a trailing break as a backslash (#1477)',
async function () {
const md = unified()
.use(remarkStringify)
.stringify({
type: 'root',
children: [
{
type: 'paragraph',
children: [{type: 'text', value: 'test'}, {type: 'break'}]
}
]
})

assert.equal(md, 'test\n')

const tree = unified().use(remarkParse).parse(md)
removePosition(tree, {force: true})

assert.deepEqual(tree, {
type: 'root',
children: [
{type: 'paragraph', children: [{type: 'text', value: 'test'}]}
]
})
}
)

await t.test(
'should still serialize a break between phrasing',
async function () {
assert.equal(
unified()
.use(remarkStringify)
.stringify({
type: 'root',
children: [
{
type: 'paragraph',
children: [
{type: 'text', value: 'alpha'},
{type: 'break'},
{type: 'text', value: 'bravo'}
]
}
]
}),
'alpha\\\nbravo\n'
)
}
)

await t.test(
'should not serialize multiple trailing breaks as backslashes',
async function () {
assert.equal(
unified()
.use(remarkStringify)
.stringify({
type: 'root',
children: [
{
type: 'paragraph',
children: [
{type: 'text', value: 'test'},
{type: 'break'},
{type: 'break'}
]
}
]
}),
'test\n'
)
}
)

await t.test('should support extensions', async function () {
const result = unified()
.data('toMarkdownExtensions', [gfmToMarkdown()])
Expand Down
Loading