8
8
* @typedef {import('../index.js').InlineMath } InlineMath
9
9
*
10
10
* @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
+ *
14
15
* Single dollars work in Pandoc and many other places, but often interfere
15
16
* with “normal” dollars in text.
17
+ * If you turn this off, you can still use two or more dollars for text math.
16
18
*/
17
19
18
20
import { longestStreak } from 'longest-streak'
19
21
import { safe } from 'mdast-util-to-markdown/lib/util/safe.js'
20
22
import { track } from 'mdast-util-to-markdown/lib/util/track.js'
23
+ import { patternCompile } from 'mdast-util-to-markdown/lib/util/pattern-compile.js'
21
24
22
25
/**
26
+ * Create an extension for `mdast-util-from-markdown`.
27
+ *
23
28
* @returns {FromMarkdownExtension }
29
+ * Extension for `mdast-util-from-markdown`.
24
30
*/
25
31
export function mathFromMarkdown ( ) {
26
32
return {
@@ -144,11 +150,15 @@ export function mathFromMarkdown() {
144
150
}
145
151
146
152
/**
147
- * @param {ToOptions } [options]
153
+ * Create an extension for `mdast-util-to-markdown`.
154
+ *
155
+ * @param {ToOptions | null | undefined } [options]
156
+ * Configuration.
148
157
* @returns {ToMarkdownExtension }
158
+ * Extension for `mdast-util-to-markdown`.
149
159
*/
150
- export function mathToMarkdown ( options = { } ) {
151
- let single = options . singleDollarTextMath
160
+ export function mathToMarkdown ( options ) {
161
+ let single = ( options || { } ) . singleDollarTextMath
152
162
153
163
if ( single === null || single === undefined ) {
154
164
single = true
@@ -158,15 +168,14 @@ export function mathToMarkdown(options = {}) {
158
168
159
169
return {
160
170
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' } ,
170
179
{ atBreak : true , character : '$' , after : '\\$' }
171
180
] ,
172
181
handlers : { math, inlineMath}
@@ -176,21 +185,24 @@ export function mathToMarkdown(options = {}) {
176
185
* @type {ToMarkdownHandle }
177
186
* @param {Math } node
178
187
*/
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>
179
191
function math ( node , _ , context , safeOptions ) {
180
192
const raw = node . value || ''
193
+ const tracker = track ( safeOptions )
181
194
const sequence = '$' . repeat ( Math . max ( longestStreak ( raw , '$' ) + 1 , 2 ) )
182
195
const exit = context . enter ( 'mathFlow' )
183
- const tracker = track ( safeOptions )
184
196
let value = tracker . move ( sequence )
185
197
186
198
if ( node . meta ) {
187
199
const subexit = context . enter ( 'mathFlowMeta' )
188
200
value += tracker . move (
189
201
safe ( context , node . meta , {
190
- ...tracker . current ( ) ,
191
202
before : value ,
192
- after : ' ' ,
193
- encode : [ '$' ]
203
+ after : '\n' ,
204
+ encode : [ '$' ] ,
205
+ ...tracker . current ( )
194
206
} )
195
207
)
196
208
subexit ( )
@@ -211,10 +223,14 @@ export function mathToMarkdown(options = {}) {
211
223
* @type {ToMarkdownHandle }
212
224
* @param {InlineMath } node
213
225
*/
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 || ''
216
233
let size = 1
217
- let pad = ''
218
234
219
235
if ( ! single ) size ++
220
236
@@ -227,21 +243,63 @@ export function mathToMarkdown(options = {}) {
227
243
size ++
228
244
}
229
245
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.
232
251
if (
252
+ // Contains non-space.
233
253
/ [ ^ \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 ) )
236
258
) {
237
- pad = ' '
259
+ value = ' ' + value + ' '
238
260
}
239
261
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
242
298
}
243
299
244
- /** @type {ToMarkdownHandle } */
300
+ /**
301
+ * @returns {string }
302
+ */
245
303
function inlineMathPeek ( ) {
246
304
return '$'
247
305
}
0 commit comments