Skip to content

Commit 32368f5

Browse files
committed
Fix some bugs, refactor some code
* Add more docs to JSDoc * Add support for `null` in input of API types * Backport syntax-tree/mdast-util-to-markdown@cf7cf8f * Backport syntax-tree/mdast-util-to-markdown@eed5115 * Fix typo, don’t allow `\n` in math meta
1 parent 5982b9e commit 32368f5

File tree

3 files changed

+162
-31
lines changed

3 files changed

+162
-31
lines changed

Diff for: lib/index.js

+88-30
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,25 @@
88
* @typedef {import('../index.js').InlineMath} InlineMath
99
*
1010
* @typedef ToOptions
11-
* @property {boolean} [singleDollarTextMath=true]
12-
* Whether to support math (text) with a single dollar (`boolean`, default:
13-
* `true`).
11+
* Configuration.
12+
* @property {boolean | null | undefined} [singleDollarTextMath=true]
13+
* Whether to support math (text) with a single dollar.
14+
*
1415
* Single dollars work in Pandoc and many other places, but often interfere
1516
* with “normal” dollars in text.
17+
* If you turn this off, you can still use two or more dollars for text math.
1618
*/
1719

1820
import {longestStreak} from 'longest-streak'
1921
import {safe} from 'mdast-util-to-markdown/lib/util/safe.js'
2022
import {track} from 'mdast-util-to-markdown/lib/util/track.js'
23+
import {patternCompile} from 'mdast-util-to-markdown/lib/util/pattern-compile.js'
2124

2225
/**
26+
* Create an extension for `mdast-util-from-markdown`.
27+
*
2328
* @returns {FromMarkdownExtension}
29+
* Extension for `mdast-util-from-markdown`.
2430
*/
2531
export function mathFromMarkdown() {
2632
return {
@@ -144,11 +150,15 @@ export function mathFromMarkdown() {
144150
}
145151

146152
/**
147-
* @param {ToOptions} [options]
153+
* Create an extension for `mdast-util-to-markdown`.
154+
*
155+
* @param {ToOptions | null | undefined} [options]
156+
* Configuration.
148157
* @returns {ToMarkdownExtension}
158+
* Extension for `mdast-util-to-markdown`.
149159
*/
150-
export function mathToMarkdown(options = {}) {
151-
let single = options.singleDollarTextMath
160+
export function mathToMarkdown(options) {
161+
let single = (options || {}).singleDollarTextMath
152162

153163
if (single === null || single === undefined) {
154164
single = true
@@ -158,15 +168,14 @@ export function mathToMarkdown(options = {}) {
158168

159169
return {
160170
unsafe: [
161-
{character: '\r', inConstruct: ['mathFlowMeta']},
162-
{character: '\r', inConstruct: ['mathFlowMeta']},
163-
single
164-
? {character: '$', inConstruct: ['mathFlowMeta', 'phrasing']}
165-
: {
166-
character: '$',
167-
after: '\\$',
168-
inConstruct: ['mathFlowMeta', 'phrasing']
169-
},
171+
{character: '\r', inConstruct: 'mathFlowMeta'},
172+
{character: '\n', inConstruct: 'mathFlowMeta'},
173+
{
174+
character: '$',
175+
after: single ? undefined : '\\$',
176+
inConstruct: 'phrasing'
177+
},
178+
{character: '$', inConstruct: 'mathFlowMeta'},
170179
{atBreak: true, character: '$', after: '\\$'}
171180
],
172181
handlers: {math, inlineMath}
@@ -176,21 +185,24 @@ export function mathToMarkdown(options = {}) {
176185
* @type {ToMarkdownHandle}
177186
* @param {Math} node
178187
*/
188+
// To do: next major: rename `context` to state, `safeOptions` to info.
189+
// Note: fixing this code? Please also fix the similar code for code:
190+
// <https://github.com/syntax-tree/mdast-util-to-markdown/blob/main/lib/handle/code.js>
179191
function math(node, _, context, safeOptions) {
180192
const raw = node.value || ''
193+
const tracker = track(safeOptions)
181194
const sequence = '$'.repeat(Math.max(longestStreak(raw, '$') + 1, 2))
182195
const exit = context.enter('mathFlow')
183-
const tracker = track(safeOptions)
184196
let value = tracker.move(sequence)
185197

186198
if (node.meta) {
187199
const subexit = context.enter('mathFlowMeta')
188200
value += tracker.move(
189201
safe(context, node.meta, {
190-
...tracker.current(),
191202
before: value,
192-
after: ' ',
193-
encode: ['$']
203+
after: '\n',
204+
encode: ['$'],
205+
...tracker.current()
194206
})
195207
)
196208
subexit()
@@ -211,10 +223,14 @@ export function mathToMarkdown(options = {}) {
211223
* @type {ToMarkdownHandle}
212224
* @param {InlineMath} node
213225
*/
214-
function inlineMath(node) {
215-
const value = node.value || ''
226+
// Note: fixing this code? Please also fix the similar code for inline code:
227+
// <https://github.com/syntax-tree/mdast-util-to-markdown/blob/main/lib/handle/inline-code.js>
228+
//
229+
// To do: next major: rename `context` to state.
230+
// To do: next major: use `state` (`safe`, `track`, `patternCompile`).
231+
function inlineMath(node, _, context) {
232+
let value = node.value || ''
216233
let size = 1
217-
let pad = ''
218234

219235
if (!single) size++
220236

@@ -227,21 +243,63 @@ export function mathToMarkdown(options = {}) {
227243
size++
228244
}
229245

230-
// If this is not just spaces or eols (tabs don’t count), and either the first
231-
// or last character are a space, eol, or dollar sign, then pad with spaces.
246+
const sequence = '$'.repeat(size)
247+
248+
// If this is not just spaces or eols (tabs don’t count), and either the
249+
// first and last character are a space or eol, or the first or last
250+
// character are dollar signs, then pad with spaces.
232251
if (
252+
// Contains non-space.
233253
/[^ \r\n]/.test(value) &&
234-
(/[ \r\n$]/.test(value.charAt(0)) ||
235-
/[ \r\n$]/.test(value.charAt(value.length - 1)))
254+
// Starts with space and ends with space.
255+
((/^[ \r\n]/.test(value) && /[ \r\n]$/.test(value)) ||
256+
// Starts or ends with dollar.
257+
/^\$|\$$/.test(value))
236258
) {
237-
pad = ' '
259+
value = ' ' + value + ' '
238260
}
239261

240-
const sequence = '$'.repeat(size)
241-
return sequence + pad + value + pad + sequence
262+
let index = -1
263+
264+
// We have a potential problem: certain characters after eols could result in
265+
// blocks being seen.
266+
// For example, if someone injected the string `'\n# b'`, then that would
267+
// result in an ATX heading.
268+
// We can’t escape characters in `inlineMath`, but because eols are
269+
// transformed to spaces when going from markdown to HTML anyway, we can swap
270+
// them out.
271+
while (++index < context.unsafe.length) {
272+
const pattern = context.unsafe[index]
273+
const expression = patternCompile(pattern)
274+
/** @type {RegExpExecArray | null} */
275+
let match
276+
277+
// Only look for `atBreak`s.
278+
// Btw: note that `atBreak` patterns will always start the regex at LF or
279+
// CR.
280+
if (!pattern.atBreak) continue
281+
282+
while ((match = expression.exec(value))) {
283+
let position = match.index
284+
285+
// Support CRLF (patterns only look for one of the characters).
286+
if (
287+
value.codePointAt(position) === 10 /* `\n` */ &&
288+
value.codePointAt(position - 1) === 13 /* `\r` */
289+
) {
290+
position--
291+
}
292+
293+
value = value.slice(0, position) + ' ' + value.slice(match.index + 1)
294+
}
295+
}
296+
297+
return sequence + value + sequence
242298
}
243299

244-
/** @type {ToMarkdownHandle} */
300+
/**
301+
* @returns {string}
302+
*/
245303
function inlineMathPeek() {
246304
return '$'
247305
}

Diff for: readme.md

+1
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ Configuration (optional).
167167

168168
Whether to support text math (inline) with a single dollar (`boolean`, default:
169169
`true`).
170+
170171
Single dollars work in Pandoc and many other places, but often interfere with
171172
“normal” dollars in text.
172173

Diff for: test.js

+73-1
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,33 @@ test('mdast -> markdown', (t) => {
190190
'should serialize math (text) w/ padding when starting in a dollar sign'
191191
)
192192

193+
t.equal(
194+
toMarkdown(
195+
{type: 'inlineMath', value: ' a '},
196+
{extensions: [mathToMarkdown()]}
197+
),
198+
'$ a $\n',
199+
'should pad w/ a space if the value starts and ends w/ a space'
200+
)
201+
202+
t.equal(
203+
toMarkdown(
204+
{type: 'inlineMath', value: ' a'},
205+
{extensions: [mathToMarkdown()]}
206+
),
207+
'$ a$\n',
208+
'should not pad w/ spaces if the value ends w/ a non-space'
209+
)
210+
211+
t.equal(
212+
toMarkdown(
213+
{type: 'inlineMath', value: 'a '},
214+
{extensions: [mathToMarkdown()]}
215+
),
216+
'$a $\n',
217+
'should not pad w/ spaces if the value starts w/ a non-space'
218+
)
219+
193220
t.deepEqual(
194221
toMarkdown({type: 'math', value: 'a'}, {extensions: [mathToMarkdown()]}),
195222
'$$\na\n$$\n',
@@ -286,7 +313,7 @@ test('mdast -> markdown', (t) => {
286313
{type: 'math', meta: 'a\rb\nc', value: ''},
287314
{extensions: [mathToMarkdown()]}
288315
),
289-
'$$a&#xD;b\nc\n$$\n',
316+
'$$a&#xD;b&#xA;c\n$$\n',
290317
'should escape `\\r`, `\\n` when in `meta` of math (flow)'
291318
)
292319

@@ -299,5 +326,50 @@ test('mdast -> markdown', (t) => {
299326
'should escape `$` when in `meta` of math (flow)'
300327
)
301328

329+
t.equal(
330+
toMarkdown(
331+
{type: 'inlineMath', value: 'a\n- b'},
332+
{extensions: [mathToMarkdown()]}
333+
),
334+
'$a - b$\n',
335+
'should prevent breaking out of code (-)'
336+
)
337+
338+
t.equal(
339+
toMarkdown(
340+
{type: 'inlineMath', value: 'a\n#'},
341+
{extensions: [mathToMarkdown()]}
342+
),
343+
'$a #$\n',
344+
'should prevent breaking out of code (#)'
345+
)
346+
347+
t.equal(
348+
toMarkdown(
349+
{type: 'inlineMath', value: 'a\n1. '},
350+
{extensions: [mathToMarkdown()]}
351+
),
352+
'$a 1. $\n',
353+
'should prevent breaking out of code (\\d\\.)'
354+
)
355+
356+
t.equal(
357+
toMarkdown(
358+
{type: 'inlineMath', value: 'a\r- b'},
359+
{extensions: [mathToMarkdown()]}
360+
),
361+
'$a - b$\n',
362+
'should prevent breaking out of code (cr)'
363+
)
364+
365+
t.equal(
366+
toMarkdown(
367+
{type: 'inlineMath', value: 'a\r\n- b'},
368+
{extensions: [mathToMarkdown()]}
369+
),
370+
'$a - b$\n',
371+
'should prevent breaking out of code (crlf)'
372+
)
373+
302374
t.end()
303375
})

0 commit comments

Comments
 (0)