Skip to content

Commit 108450c

Browse files
committed
Fix handling of spaces, tabs around line endings
Closes GH-41.
1 parent d3c2c46 commit 108450c

File tree

4 files changed

+120
-10
lines changed

4 files changed

+120
-10
lines changed

lib/handle/heading.js

+8
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ export function heading(node, _, context) {
3939
const subexit = context.enter('phrasing')
4040
let value = containerPhrasing(node, context, {before: '# ', after: '\n'})
4141

42+
if (/^[\t ]/.test(value)) {
43+
value =
44+
'&#x' +
45+
value.charCodeAt(0).toString(16).toUpperCase() +
46+
';' +
47+
value.slice(1)
48+
}
49+
4250
value = value ? sequence + ' ' + value : sequence
4351

4452
if (context.options.closeAtx) {

lib/unsafe.js

+5-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
/** @type {Array.<Unsafe>} */
66
export const unsafe = [
7+
{character: '\t', after: '[\\r\\n]', inConstruct: 'phrasing'},
8+
{character: '\t', before: '[\\r\\n]', inConstruct: 'phrasing'},
79
{
810
character: '\t',
911
inConstruct: ['codeFencedLangGraveAccent', 'codeFencedLangTilde']
@@ -30,6 +32,8 @@ export const unsafe = [
3032
'headingAtx'
3133
]
3234
},
35+
{character: ' ', after: '[\\r\\n]', inConstruct: 'phrasing'},
36+
{character: ' ', before: '[\\r\\n]', inConstruct: 'phrasing'},
3337
{
3438
character: ' ',
3539
inConstruct: ['codeFencedLangGraveAccent', 'codeFencedLangTilde']
@@ -88,10 +92,7 @@ export const unsafe = [
8892
// Note: typical escapes are handled in `safe`!
8993
{character: '\\', after: '[\\r\\n]', inConstruct: 'phrasing'},
9094
// A right bracket can exit labels.
91-
{
92-
character: ']',
93-
inConstruct: ['label', 'reference']
94-
},
95+
{character: ']', inConstruct: ['label', 'reference']},
9596
// Caret is not used in markdown for constructs.
9697
// An underscore can start emphasis, strong, or a thematic break.
9798
{atBreak: true, character: '_'},

lib/util/safe.js

+9-5
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,15 @@ export function safe(context, input, config) {
7171
// the next character, and the next character is definitly being escaped,
7272
// then skip this escape.
7373
if (
74-
position + 1 < end &&
75-
positions[index + 1] === position + 1 &&
76-
infos[position].after &&
77-
!infos[position + 1].before &&
78-
!infos[position + 1].after
74+
(position + 1 < end &&
75+
positions[index + 1] === position + 1 &&
76+
infos[position].after &&
77+
!infos[position + 1].before &&
78+
!infos[position + 1].after) ||
79+
(positions[index - 1] === position - 1 &&
80+
infos[position].before &&
81+
!infos[position - 1].before &&
82+
!infos[position - 1].after)
7983
) {
8084
continue
8185
}

test/index.js

+98-1
Original file line numberDiff line numberDiff line change
@@ -1210,6 +1210,50 @@ test('heading', (t) => {
12101210
'should not escape a `#` in a heading (2)'
12111211
)
12121212

1213+
t.equal(
1214+
to({type: 'heading', depth: 1, children: [{type: 'text', value: ' a'}]}),
1215+
'# &#x20; a\n',
1216+
'should encode a space at the start of an atx heading'
1217+
)
1218+
1219+
t.equal(
1220+
to({type: 'heading', depth: 1, children: [{type: 'text', value: '\t\ta'}]}),
1221+
'# &#x9;\ta\n',
1222+
'should encode a tab at the start of an atx heading'
1223+
)
1224+
1225+
t.equal(
1226+
to({type: 'heading', depth: 1, children: [{type: 'text', value: 'a '}]}),
1227+
'# a &#x20;\n',
1228+
'should encode a space at the end of an atx heading'
1229+
)
1230+
1231+
t.equal(
1232+
to({type: 'heading', depth: 1, children: [{type: 'text', value: 'a\t\t'}]}),
1233+
'# a\t&#x9;\n',
1234+
'should encode a tab at the end of an atx heading'
1235+
)
1236+
1237+
t.equal(
1238+
to({
1239+
type: 'heading',
1240+
depth: 1,
1241+
children: [{type: 'text', value: 'a \n b'}]
1242+
}),
1243+
'a&#x20;\n&#x20;b\n=======\n',
1244+
'should encode spaces around a line ending in a setext heading'
1245+
)
1246+
1247+
t.equal(
1248+
to({
1249+
type: 'heading',
1250+
depth: 3,
1251+
children: [{type: 'text', value: 'a \n b'}]
1252+
}),
1253+
'### a &#xA; b\n',
1254+
'should not need to encode spaces around a line ending in an atx heading (because the line ending is encoded)'
1255+
)
1256+
12131257
t.end()
12141258
})
12151259

@@ -1507,7 +1551,7 @@ test('imageReference', (t) => {
15071551
t.end()
15081552
})
15091553

1510-
test('Code text', (t) => {
1554+
test('code (text)', (t) => {
15111555
// @ts-expect-error: `value` missing.
15121556
t.equal(to({type: 'inlineCode'}), '``\n', 'should support an empty code text')
15131557

@@ -2733,6 +2777,42 @@ test('paragraph', (t) => {
27332777
'should support a paragraph'
27342778
)
27352779

2780+
t.equal(
2781+
to({type: 'paragraph', children: [{type: 'text', value: ' a'}]}),
2782+
'&#x20; a\n',
2783+
'should encode spaces at the start of paragraphs'
2784+
)
2785+
2786+
t.equal(
2787+
to({type: 'paragraph', children: [{type: 'text', value: 'a '}]}),
2788+
'a &#x20;\n',
2789+
'should encode spaces at the end of paragraphs'
2790+
)
2791+
2792+
t.equal(
2793+
to({type: 'paragraph', children: [{type: 'text', value: '\t\ta'}]}),
2794+
'&#x9;\ta\n',
2795+
'should encode tabs at the start of paragraphs'
2796+
)
2797+
2798+
t.equal(
2799+
to({type: 'paragraph', children: [{type: 'text', value: 'a\t\t'}]}),
2800+
'a\t&#x9;\n',
2801+
'should encode tabs at the end of paragraphs'
2802+
)
2803+
2804+
t.equal(
2805+
to({type: 'paragraph', children: [{type: 'text', value: 'a \n b'}]}),
2806+
'a &#x20;\n&#x20; b\n',
2807+
'should encode spaces around line endings in paragraphs'
2808+
)
2809+
2810+
t.equal(
2811+
to({type: 'paragraph', children: [{type: 'text', value: 'a\t\t\n\t\tb'}]}),
2812+
'a\t&#x9;\n&#x9;\tb\n',
2813+
'should encode spaces around line endings in paragraphs'
2814+
)
2815+
27362816
t.end()
27372817
})
27382818

@@ -2769,6 +2849,7 @@ test('text', (t) => {
27692849
t.equal(to({type: 'text'}), '', 'should support a void text')
27702850
t.equal(to({type: 'text', value: ''}), '', 'should support an empty text')
27712851
t.equal(to({type: 'text', value: 'a\nb'}), 'a\nb\n', 'should support text')
2852+
27722853
t.end()
27732854
})
27742855

@@ -3488,5 +3569,21 @@ test('roundtrip', (t) => {
34883569
'should roundtrip different lists w/ `bulletOrderedOther` and lists that could turn into thematic breaks (6)'
34893570
)
34903571

3572+
doc = '&#x20;\n'
3573+
3574+
t.equal(to(from(doc)), doc, 'should roundtrip a single encoded space')
3575+
3576+
doc = '&#x9;\n'
3577+
3578+
t.equal(to(from(doc)), doc, 'should roundtrip a single encoded tab')
3579+
3580+
doc = '&#x20; a &#x20;\n&#x9;\tb\t&#x9;\n'
3581+
3582+
t.equal(
3583+
to(from(doc)),
3584+
doc,
3585+
'should roundtrip encoded spaces and tabs where needed'
3586+
)
3587+
34913588
t.end()
34923589
})

0 commit comments

Comments
 (0)