@@ -2,156 +2,208 @@ import commonmark = require('commonmark');
2
2
3
3
/**
4
4
* Convert MarkDown to RST
5
- *
6
- * This is hard, and I'm doing it very hackily to get something out quickly.
7
- *
8
- * Preferably, the next person to look at this should a little more OO
9
- * instead of procedural.
10
5
*/
11
6
export function md2rst ( text : string ) {
12
7
const parser = new commonmark . Parser ( { smart : false } ) ;
13
8
const ast = parser . parse ( text ) ;
14
9
15
- const ret = new Array < string > ( ) ;
16
-
17
- let indent = 0 ;
18
- function line ( ...xs : string [ ] ) {
19
- for ( const x of xs ) {
20
- ret . push ( ( ' ' . repeat ( indent ) + x ) . trimRight ( ) ) ;
21
- }
22
- }
10
+ const doc = new DocumentBuilder ( ) ;
23
11
24
12
function directive ( name : string , opening : boolean ) {
25
13
if ( opening ) {
26
- line ( `.. ${ name } ::` ) ;
27
- brk ( ) ;
28
- indent += 3 ;
14
+ doc . appendLine ( `.. ${ name } ::` ) ;
15
+ doc . paraBreak ( ) ;
16
+ doc . pushPrefix ( ' ' ) ;
29
17
} else {
30
- indent -= 3 ;
18
+ doc . popPrefix ( ) ;
31
19
}
32
20
}
33
21
34
- function brk ( ) {
35
- if ( ret . length > 0 && ret [ ret . length - 1 ] . trim ( ) !== '' ) { ret . push ( '' ) ; }
36
- }
37
-
38
22
function textOf ( node : commonmark . Node ) {
39
23
return node . literal || '' ;
40
24
}
41
25
42
- let para = new Paragraph ( ) ; // Where to accumulate text fragments
43
- let lastParaLine : number ; // Where the last paragraph ended, in order to add ::
44
- let nextParaPrefix : string | undefined ;
26
+ // let lastParaLine: number; // Where the last paragraph ended, in order to add ::
45
27
46
28
pump ( ast , {
47
29
block_quote ( _node , entering ) {
48
30
directive ( 'epigraph' , entering ) ;
49
31
} ,
50
32
51
33
heading ( node , _entering ) {
52
- line ( node . literal || '' ) ;
53
- line ( headings [ node . level - 1 ] . repeat ( textOf ( node ) . length ) ) ;
34
+ doc . appendLine ( node . literal || '' ) ;
35
+ doc . appendLine ( headings [ node . level - 1 ] . repeat ( textOf ( node ) . length ) ) ;
54
36
} ,
55
37
56
38
paragraph ( node , entering ) {
57
- if ( entering ) {
58
- para = new Paragraph ( nextParaPrefix ) ;
59
- nextParaPrefix = undefined ;
60
- } else {
61
- // Don't break inside list item
62
- if ( node . parent == null || node . parent . type !== 'item' ) {
63
- brk ( ) ;
64
- }
65
- line ( ...para . lines ( ) ) ;
66
- lastParaLine = ret . length - 1 ;
39
+ // If we're going to a paragraph that's not in a list, open a block.
40
+ if ( entering && node . parent && node . parent . type !== 'item' ) {
41
+ doc . paraBreak ( ) ;
42
+ }
43
+
44
+ // If we're coming out of a paragraph that's being followed by
45
+ // a code block, make sure the current line ends in '::':
46
+ if ( ! entering && node . next && node . next . type === 'code_block' ) {
47
+ doc . transformLastLine ( lastLine => {
48
+ const appended = lastLine . replace ( / [ \W ] $ / , '::' ) ;
49
+ if ( appended !== lastLine ) { return appended ; }
50
+
51
+ return lastLine + ' Example::' ;
52
+ } ) ;
53
+ }
54
+
55
+ // End of paragraph at least implies line break.
56
+ if ( ! entering ) {
57
+ doc . newline ( ) ;
67
58
}
68
59
} ,
69
60
70
- text ( node ) { para . add ( textOf ( node ) ) ; } ,
71
- softbreak ( ) { para . newline ( ) ; } ,
72
- linebreak ( ) { para . newline ( ) ; } ,
73
- thematic_break ( ) { line ( '------' ) ; } ,
74
- code ( node ) { para . add ( '``' + textOf ( node ) + '``' ) ; } ,
75
- strong ( ) { para . add ( '**' ) ; } ,
76
- emph ( ) { para . add ( '*' ) ; } ,
61
+ text ( node ) { doc . append ( textOf ( node ) ) ; } ,
62
+ softbreak ( ) { doc . newline ( ) ; } ,
63
+ linebreak ( ) { doc . newline ( ) ; } ,
64
+ thematic_break ( ) { doc . appendLine ( '------' ) ; } ,
65
+ code ( node ) { doc . append ( '``' + textOf ( node ) + '``' ) ; } ,
66
+ strong ( ) { doc . append ( '**' ) ; } ,
67
+ emph ( ) { doc . append ( '*' ) ; } ,
77
68
78
69
list ( ) {
79
- brk ( ) ;
70
+ doc . paraBreak ( ) ;
80
71
} ,
81
72
82
73
link ( node , entering ) {
83
74
if ( entering ) {
84
- para . add ( '`' ) ;
75
+ doc . append ( '`' ) ;
85
76
} else {
86
- para . add ( ' <' + ( node . destination || '' ) + '>`_' ) ;
77
+ doc . append ( ' <' + ( node . destination || '' ) + '>`_' ) ;
87
78
}
88
79
} ,
89
80
90
- item ( node , _entering ) {
81
+ item ( node , entering ) {
91
82
// AST hierarchy looks like list -> item -> paragraph -> text
92
- if ( node . listType === 'bullet' ) {
93
- nextParaPrefix = '- ' ;
83
+ if ( entering ) {
84
+ if ( node . listType === 'bullet' ) {
85
+ doc . pushBulletPrefix ( '- ' ) ;
86
+ } else {
87
+ doc . pushBulletPrefix ( `${ node . listStart } . ` ) ;
88
+ }
94
89
} else {
95
- nextParaPrefix = ` ${ node . listStart } . ` ;
90
+ doc . popPrefix ( ) ;
96
91
}
97
-
98
92
} ,
99
93
100
94
code_block ( node ) {
101
- // Poke a double :: at the end of the previous line as per ReST "literal block" syntax.
102
- if ( lastParaLine !== undefined ) {
103
- const lastLine = ret [ lastParaLine ] ;
104
- ret [ lastParaLine ] = lastLine . replace ( / [ \W ] $ / , '::' ) ;
105
- if ( ret [ lastParaLine ] === lastLine ) { ret [ lastParaLine ] = lastLine + '::' ; }
106
- } else {
107
- line ( 'Example::' ) ;
108
- }
95
+ doc . paraBreak ( ) ;
109
96
110
- brk ( ) ;
97
+ // If there's no paragraph just before me, add the word "Example::".
98
+ if ( ! node . prev || node . prev . type !== 'paragraph' ) {
99
+ doc . appendLine ( 'Example::' ) ;
100
+ doc . paraBreak ( ) ;
101
+ }
111
102
112
- indent += 3 ;
103
+ doc . pushBulletPrefix ( ' ' ) ;
113
104
114
- for ( const l of textOf ( node ) . split ( '\n' ) ) {
115
- line ( l ) ;
105
+ for ( const l of textOf ( node ) . replace ( / \n + $ / , '' ) . split ( '\n' ) ) {
106
+ doc . appendLine ( l ) ;
116
107
}
117
108
118
- indent -= 3 ;
109
+ doc . popPrefix ( ) ;
119
110
}
120
111
121
112
} ) ;
122
113
123
- return ret . join ( '\n' ) . trimRight ( ) ;
114
+ return doc . toString ( ) ;
124
115
}
125
116
126
- class Paragraph {
127
- private readonly parts = new Array < string > ( ) ;
117
+ /**
118
+ * Build a document incrementally
119
+ */
120
+ class DocumentBuilder {
121
+ private readonly prefix = new Array < string > ( ) ;
122
+ private readonly lines = new Array < string [ ] > ( ) ;
123
+ private queuedNewline = false ;
128
124
129
- constructor ( text ?: string ) {
130
- if ( text !== undefined ) { this . parts . push ( text ) ; }
125
+ constructor ( ) {
126
+ this . lines . push ( [ ] ) ;
131
127
}
132
128
133
- public add ( text : string ) {
134
- this . parts . push ( text ) ;
129
+ public pushPrefix ( prefix : string ) {
130
+ this . prefix . push ( prefix ) ;
135
131
}
136
132
137
- public newline ( ) {
138
- this . parts . push ( '\n' ) ;
133
+ public popPrefix ( ) {
134
+ this . prefix . pop ( ) ;
135
+ }
136
+
137
+ public paraBreak ( ) {
138
+ if ( this . lines . length > 0 && partsToString ( this . lastLine ) !== '' ) { this . newline ( ) ; }
139
+ }
140
+
141
+ public get length ( ) {
142
+ return this . lines . length ;
143
+ }
144
+
145
+ public get lastLine ( ) {
146
+ return this . lines [ this . length - 1 ] ;
139
147
}
140
148
141
- public lines ( ) : string [ ] {
142
- return this . parts . length > 0 ? this . toString ( ) . split ( '\n' ) : [ ] ;
149
+ public append ( text : string ) {
150
+ this . flushQueuedNewline ( ) ;
151
+ this . lastLine . push ( text ) ;
152
+ }
153
+
154
+ public appendLine ( ...lines : string [ ] ) {
155
+ for ( const line of lines ) {
156
+ this . append ( line ) ;
157
+ this . newline ( ) ;
158
+ }
159
+ }
160
+
161
+ public pushBulletPrefix ( prefix : string ) {
162
+ this . append ( prefix ) ;
163
+ this . pushPrefix ( ' ' . repeat ( prefix . length ) ) ;
164
+ }
165
+
166
+ public transformLastLine ( block : ( x : string ) => string ) {
167
+ if ( this . length >= 0 ) {
168
+ this . lines [ this . length - 1 ] . splice ( 0 , this . lastLine . length , block ( partsToString ( this . lastLine ) ) ) ;
169
+ } else {
170
+ this . lines . push ( [ block ( '' ) ] ) ;
171
+ }
172
+ }
173
+
174
+ public newline ( ) {
175
+ this . flushQueuedNewline ( ) ;
176
+ // Don't do the newline here, wait to apply the correct indentation when and if we add more text.
177
+ this . queuedNewline = true ;
143
178
}
144
179
145
180
public toString ( ) {
146
- return this . parts . join ( '' ) . trimRight ( ) ;
181
+ return this . lines . map ( partsToString ) . join ( '\n' ) . replace ( / \n + $ / , '' ) ;
182
+ }
183
+
184
+ private flushQueuedNewline ( ) {
185
+ if ( this . queuedNewline ) {
186
+ this . lines . push ( [ ...this . prefix ] ) ;
187
+ this . queuedNewline = false ;
188
+ }
147
189
}
148
190
}
149
191
192
+ /**
193
+ * Turn a list of string fragments into a string
194
+ */
195
+ function partsToString ( parts : string [ ] ) {
196
+ return parts . join ( '' ) . trimRight ( ) ;
197
+ }
198
+
150
199
const headings = [ '=' , '-' , '^' , '"' ] ;
151
200
152
201
type Handler = ( node : commonmark . Node , entering : boolean ) => void ;
153
202
type Handlers = { [ key in commonmark . NodeType ] ?: Handler } ;
154
203
204
+ /**
205
+ * Pump a CommonMark AST tree through a set of handlers
206
+ */
155
207
function pump ( ast : commonmark . Node , handlers : Handlers ) {
156
208
const walker = ast . walker ( ) ;
157
209
let event = walker . next ( ) ;
@@ -163,4 +215,25 @@ function pump(ast: commonmark.Node, handlers: Handlers) {
163
215
164
216
event = walker . next ( ) ;
165
217
}
166
- }
218
+ }
219
+
220
+ /*
221
+ A typical AST looks like this:
222
+
223
+ document
224
+ ├─┬ paragraph
225
+ │ └── text
226
+ └─┬ list
227
+ ├─┬ item
228
+ │ └─┬ paragraph
229
+ │ ├── text
230
+ │ ├── softbreak
231
+ │ └── text
232
+ └─┬ item
233
+ └─┬ paragraph
234
+ ├── text
235
+ ├─┬ emph
236
+ │ └── text
237
+ └── text
238
+
239
+ */
0 commit comments