1
1
/* eslint-disable @typescript-eslint/no-explicit-any */
2
2
import type MarkdownIt from "markdown-it/lib"
3
- // import type StateBlock from "markdown-it/lib/rules_block/state_block"
3
+ import type StateBlock from "markdown-it/lib/rules_block/state_block"
4
4
import type StateInline from "markdown-it/lib/rules_inline/state_inline"
5
5
6
6
export interface IOptions {
7
- // Capture math blocks with label suffix, e.g. `$$a=1$$ (eq1)`
8
- allow_labels ?: boolean
9
7
// Parse inline math when there is space after/before the opening/closing `$`, e.g. `$ a $`
10
8
allow_space ?: boolean
11
9
// Parse inline math when there is a digit before/after the opening/closing `$`, e.g. `1$` or `$2`
12
10
allow_digits ?: boolean
13
11
// Search for double-dollar math within inline contexts
14
12
double_inline ?: boolean
13
+ // Capture math blocks with label suffix, e.g. `$$a=1$$ (eq1)`
14
+ allow_labels ?: boolean
15
+ // function to normalize the label, by default replaces whitespace with `-`
16
+ labelNormalizer ?: ( label : string ) => string
15
17
// The render function for math content
16
18
renderer ?: ( content : string , options ?: { [ key : string ] : any } ) => string
17
19
// options to parse to the render function, for inline math
@@ -21,10 +23,13 @@ export interface IOptions {
21
23
}
22
24
23
25
const OptionDefaults : IOptions = {
24
- allow_labels : true ,
25
26
allow_space : true ,
26
27
allow_digits : true ,
27
- double_inline : true
28
+ double_inline : true ,
29
+ allow_labels : true ,
30
+ labelNormalizer : ( label : string ) => {
31
+ return label . replace ( / [ \s ] + / g, "-" )
32
+ }
28
33
}
29
34
30
35
/**
@@ -34,31 +39,70 @@ const OptionDefaults: IOptions = {
34
39
export default function dollarmath_plugin ( md : MarkdownIt , options ?: IOptions ) : void {
35
40
const fullOptions = { ...OptionDefaults , ...options }
36
41
md . inline . ruler . before ( "escape" , "math_inline" , math_inline_dollar ( fullOptions ) )
37
- // md.block.ruler.before("fence", "math_block", math_block_dollar(fullOptions))
42
+ md . block . ruler . before ( "fence" , "math_block" , math_block_dollar ( fullOptions ) )
38
43
39
44
const renderer = fullOptions ?. renderer
40
45
41
46
if ( renderer ) {
42
47
md . renderer . rules [ "math_inline" ] = ( tokens , idx ) => {
43
48
const content = tokens [ idx ] . content
44
- const renderOptions =
45
- tokens [ idx ] . markup === "$$"
46
- ? fullOptions ?. optionsBlock
47
- : fullOptions ?. optionsInline
48
49
let res : string
49
50
try {
50
- res = renderer ( content , renderOptions )
51
+ res = renderer ( content , fullOptions ?. optionsInline )
52
+ } catch ( err ) {
53
+ res = md . utils . escapeHtml ( `${ content } :${ err . message } ` )
54
+ }
55
+ return res
56
+ }
57
+ md . renderer . rules [ "math_inline_double" ] = ( tokens , idx ) => {
58
+ const content = tokens [ idx ] . content
59
+ let res : string
60
+ try {
61
+ res = renderer ( content , fullOptions ?. optionsBlock )
51
62
} catch ( err ) {
52
63
res = md . utils . escapeHtml ( `${ content } :${ err . message } ` )
53
64
}
54
65
return res
55
66
}
67
+ md . renderer . rules [ "math_block" ] = ( tokens , idx ) => {
68
+ const content = tokens [ idx ] . content
69
+ let res : string
70
+ try {
71
+ res = renderer ( content , fullOptions ?. optionsBlock )
72
+ } catch ( err ) {
73
+ res = md . utils . escapeHtml ( `${ content } :${ err . message } ` )
74
+ }
75
+ return res
76
+ }
77
+ md . renderer . rules [ "math_block_label" ] = ( tokens , idx ) => {
78
+ const content = tokens [ idx ] . content
79
+ const label = tokens [ idx ] . info
80
+ let res : string
81
+ try {
82
+ res = renderer ( content , fullOptions ?. optionsBlock )
83
+ } catch ( err ) {
84
+ res = md . utils . escapeHtml ( `${ content } :${ err . message } ` )
85
+ }
86
+ return (
87
+ res +
88
+ `\n<a href="#${ label } " class="mathlabel" title="Permalink to this equation">¶</a>\n`
89
+ )
90
+ }
56
91
} else {
57
- // basic renderer for testing
92
+ // basic renderers for testing
58
93
md . renderer . rules [ "math_inline" ] = ( tokens , idx ) => {
94
+ return `<eq>${ tokens [ idx ] . content } </eq>`
95
+ }
96
+ md . renderer . rules [ "math_inline_double" ] = ( tokens , idx ) => {
97
+ return `<eqn>${ tokens [ idx ] . content } </eqn>`
98
+ }
99
+ md . renderer . rules [ "math_block" ] = ( tokens , idx ) => {
100
+ return `<section>\n<eqn>${ tokens [ idx ] . content } </eqn>\n</section>\n`
101
+ }
102
+ md . renderer . rules [ "math_block_label" ] = ( tokens , idx ) => {
59
103
const content = tokens [ idx ] . content
60
- const tag = tokens [ idx ] . markup === "$$" ? "eqn" : "eq"
61
- return `<${ tag } > ${ content } </${ tag } > `
104
+ const label = tokens [ idx ] . info
105
+ return `<section>\n<eqn> ${ content } </eqn>\n<span class="eqno">( ${ label } )</span>\n</section>\n `
62
106
}
63
107
}
64
108
}
@@ -181,7 +225,11 @@ function math_inline_dollar(
181
225
return false
182
226
}
183
227
if ( ! silent ) {
184
- const token = state . push ( "math_inline" , "math" , 0 )
228
+ const token = state . push (
229
+ is_double ? "math_inline_double" : "math_inline" ,
230
+ "math" ,
231
+ 0
232
+ )
185
233
token . content = text
186
234
token . markup = is_double ? "$$" : "$"
187
235
}
@@ -190,3 +238,113 @@ function math_inline_dollar(
190
238
}
191
239
return math_inline_dollar_rule
192
240
}
241
+
242
+ /** Match a trailing label for a math block */
243
+ function matchLabel ( lineText : string , end : number ) : { label ?: string ; end : number } {
244
+ // reverse the line and match
245
+ const eqnoMatch = lineText
246
+ . split ( "" )
247
+ . reverse ( )
248
+ . join ( "" )
249
+ . match ( / ^ \s * \) (?< label > [ ^ ) $ \r \n ] + ?) \( \s * \$ { 2 } / )
250
+ if ( eqnoMatch && eqnoMatch . groups ) {
251
+ const label = eqnoMatch . groups [ "label" ] . split ( "" ) . reverse ( ) . join ( "" )
252
+ end = end - ( ( eqnoMatch . index || 0 ) + eqnoMatch [ 0 ] . length )
253
+ return { label, end }
254
+ }
255
+ return { end }
256
+ }
257
+
258
+ /** Generate inline dollar rule */
259
+ function math_block_dollar (
260
+ options : IOptions
261
+ ) : ( state : StateBlock , startLine : number , endLine : number , silent : boolean ) => boolean {
262
+ /** Block dollar rule */
263
+ function math_block_dollar_rule (
264
+ state : StateBlock ,
265
+ startLine : number ,
266
+ endLine : number ,
267
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
268
+ silent : boolean
269
+ ) : boolean {
270
+ let haveEndMarker = false
271
+ const startPos = state . bMarks [ startLine ] + state . tShift [ startLine ]
272
+ let end = state . eMarks [ startLine ]
273
+
274
+ // if it's indented more than 3 spaces, it should be a code block
275
+ if ( state . sCount [ startLine ] - state . blkIndent >= 4 ) {
276
+ return false
277
+ }
278
+ if ( startPos + 2 > end ) {
279
+ return false
280
+ }
281
+ if (
282
+ state . src . charCodeAt ( startPos ) != 0x24 ||
283
+ state . src . charCodeAt ( startPos + 1 ) != 0x24
284
+ ) {
285
+ return false
286
+ }
287
+ // search for end of block
288
+ let nextLine = startLine
289
+ let label : undefined | string = undefined
290
+ // search for end of block on same line
291
+ let lineText = state . src . slice ( startPos , end )
292
+ if ( lineText . trim ( ) . length > 3 ) {
293
+ if ( lineText . trim ( ) . endsWith ( "$$" ) ) {
294
+ haveEndMarker = true
295
+ end = end - 2 - ( lineText . length - lineText . trim ( ) . length )
296
+ } else if ( options . allow_labels ) {
297
+ const output = matchLabel ( lineText , end )
298
+ if ( output . label !== undefined ) {
299
+ haveEndMarker = true
300
+ label = output . label
301
+ end = output . end
302
+ }
303
+ }
304
+ }
305
+
306
+ // search for end of block on subsequent line
307
+ let start : number
308
+ if ( ! haveEndMarker ) {
309
+ while ( nextLine + 1 < endLine ) {
310
+ nextLine += 1
311
+ start = state . bMarks [ nextLine ] + state . tShift [ nextLine ]
312
+ end = state . eMarks [ nextLine ]
313
+ if ( end - start < 2 ) {
314
+ continue
315
+ }
316
+ lineText = state . src . slice ( start , end )
317
+ if ( lineText . trim ( ) . endsWith ( "$$" ) ) {
318
+ haveEndMarker = true
319
+ end = end - 2 - ( lineText . length - lineText . trim ( ) . length )
320
+ break
321
+ }
322
+ if ( options . allow_labels ) {
323
+ const output = matchLabel ( lineText , end )
324
+ if ( output . label !== undefined ) {
325
+ haveEndMarker = true
326
+ label = output . label
327
+ end = output . end
328
+ break
329
+ }
330
+ }
331
+ }
332
+ }
333
+ if ( ! haveEndMarker ) {
334
+ return false
335
+ }
336
+
337
+ state . line = nextLine + ( haveEndMarker ? 1 : 0 )
338
+
339
+ const token = state . push ( label ? "math_block_label" : "math_block" , "math" , 0 )
340
+ token . block = true
341
+ token . content = state . src . slice ( startPos + 2 , end )
342
+ token . markup = "$$"
343
+ token . map = [ startLine , state . line ]
344
+ if ( label ) {
345
+ token . info = options . labelNormalizer ? options . labelNormalizer ( label ) : label
346
+ }
347
+ return true
348
+ }
349
+ return math_block_dollar_rule
350
+ }
0 commit comments