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
1820import { longestStreak } from 'longest-streak'
1921import { safe } from 'mdast-util-to-markdown/lib/util/safe.js'
2022import { 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 */
2531export 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 }
0 commit comments