1
1
/**
2
- * @typedef {import('unist').Node & {properties: Object<any, any>} } Node
3
- * @typedef {import('unist').Parent & {properties: Object<any, any>} } Parent
2
+ * @typedef {import('hast').Node & {properties: Object<any, any>} } Node
3
+ * @typedef {import('hast').Parent & {properties: Object<any, any>} } Parent
4
+ * @typedef {import('hast').Root } Root
4
5
* @typedef {import('unist-util-visit').Visitor<Node> } Visitor
6
+ * @typedef Options options
7
+ * Configuration.
8
+ * @property {boolean } [showLineNumbers]
9
+ * Set `showLineNumbers` to `true` to always display line number
10
+ * @property {boolean } [ignoreMissing]
11
+ * Set `ignoreMissing` to `true` to ignore unsupported languages and line highlighting when no language is specified
5
12
*/
6
13
7
14
import { visit } from 'unist-util-visit'
8
15
import { toString } from 'hast-util-to-string'
9
16
import { refractor } from 'refractor/lib/all.js'
17
+ import { toHtml } from 'hast-util-to-html'
18
+ import { filter } from 'unist-util-filter'
19
+ import { unified } from 'unified'
20
+ import parse from 'rehype-parse'
10
21
import rangeParser from 'parse-numeric-range'
11
22
12
23
/**
@@ -73,20 +84,68 @@ const splitLine = (text) => {
73
84
}
74
85
75
86
/**
76
- * Rehype plugin that highlights code blocks with refractor (prismjs)
77
- *
78
- * Set `showLineNumbers` to `true` to always display line number
87
+ * Split line to div node with className `code-line`
79
88
*
80
- * Set `ignoreMissing` to `true` to ignore unsupported languages and line highlighting when no language is specified
89
+ * @param {import('refractor').RefractorRoot } ast
90
+ * @return {Root }
91
+ */
92
+ const getNodePosition = ( ast ) => {
93
+ // @ts -ignore
94
+ let html = toHtml ( ast )
95
+ const hast = unified ( ) . use ( parse , { emitParseErrors : true , fragment : true } ) . parse ( html )
96
+ return hast
97
+ }
98
+
99
+ /**
100
+ * Split multiline text nodes into individual nodes with positioning
81
101
*
82
- * @typedef {{ showLineNumbers?: boolean, ignoreMissing?: boolean } } RehypePrismOptions
83
- * @param {RehypePrismOptions } options
84
- * @return {Visitor }
102
+ * @param {Parent['children'] } ast
103
+ * @return {Parent['children'] }
85
104
*/
86
- const rehypePrism = ( options ) => {
87
- options = options || { }
105
+ const splitTextByLine = ( ast ) => {
106
+ //@ts -ignore
107
+ return ast . reduce ( ( result , node ) => {
108
+ if ( node . type === 'text' ) {
109
+ if ( node . value . indexOf ( '\n' ) === - 1 ) {
110
+ result . push ( node )
111
+ return result
112
+ }
113
+
114
+ const lines = node . value . split ( '\n' )
115
+ for ( const [ i , line ] of lines . entries ( ) ) {
116
+ result . push ( {
117
+ type : 'text' ,
118
+ value : i === lines . length - 1 ? line : line + '\n' ,
119
+ position : {
120
+ start : { line : node . position . start . line + i } ,
121
+ end : { line : node . position . start . line + i } ,
122
+ } ,
123
+ } )
124
+ }
125
+
126
+ return result
127
+ }
128
+
129
+ if ( node . children ) {
130
+ // @ts -ignore
131
+ node . children = splitTextByLine ( node . children )
132
+ result . push ( node )
133
+ return result
134
+ }
88
135
136
+ result . push ( node )
137
+ return result
138
+ } , [ ] )
139
+ }
140
+
141
+ /**
142
+ * Rehype plugin that highlights code blocks with refractor (prismjs)
143
+ *
144
+ * @type {import('unified').Plugin<[Options?], Root> }
145
+ */
146
+ const rehypePrism = ( options = { } ) => {
89
147
return ( tree ) => {
148
+ // @ts -ignore
90
149
visit ( tree , 'element' , visitor )
91
150
}
92
151
@@ -112,6 +171,25 @@ const rehypePrism = (options) => {
112
171
meta = `${ lang } ${ meta } `
113
172
}
114
173
174
+ let refractorRoot
175
+ let langError = false
176
+
177
+ // Syntax highlight
178
+ if ( lang ) {
179
+ try {
180
+ // @ts -ignore
181
+ refractorRoot = refractor . highlight ( toString ( node ) , lang )
182
+ refractorRoot = getNodePosition ( refractorRoot )
183
+ refractorRoot . children = splitTextByLine ( refractorRoot . children )
184
+ } catch ( err ) {
185
+ if ( options . ignoreMissing && / U n k n o w n l a n g u a g e / . test ( err . message ) ) {
186
+ langError = true
187
+ } else {
188
+ throw err
189
+ }
190
+ }
191
+ }
192
+
115
193
const shouldHighlightLine = calculateLinesToHighlight ( meta )
116
194
// @ts -ignore
117
195
const codeLineArray = splitLine ( toString ( node ) )
@@ -129,16 +207,12 @@ const rehypePrism = (options) => {
129
207
}
130
208
131
209
// Syntax highlight
132
- if ( lang && line . children ) {
133
- try {
134
- line . children = refractor . highlight ( line . children [ 0 ] . value , lang ) . children
135
- } catch ( err ) {
136
- // eslint-disable-next-line no-empty
137
- if ( options . ignoreMissing && / U n k n o w n l a n g u a g e / . test ( err . message ) ) {
138
- } else {
139
- throw err
140
- }
141
- }
210
+ if ( lang && line . children && ! langError ) {
211
+ const treeExtract = filter (
212
+ refractorRoot ,
213
+ ( node ) => node . position . start . line <= i + 1 && node . position . end . line >= i + 1
214
+ )
215
+ line . children = treeExtract . children
142
216
}
143
217
}
144
218
0 commit comments