@@ -2,156 +2,208 @@ import commonmark = require('commonmark');
22
33/**
44 * 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.
105 */
116export function md2rst ( text : string ) {
127 const parser = new commonmark . Parser ( { smart : false } ) ;
138 const ast = parser . parse ( text ) ;
149
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 ( ) ;
2311
2412 function directive ( name : string , opening : boolean ) {
2513 if ( opening ) {
26- line ( `.. ${ name } ::` ) ;
27- brk ( ) ;
28- indent += 3 ;
14+ doc . appendLine ( `.. ${ name } ::` ) ;
15+ doc . paraBreak ( ) ;
16+ doc . pushPrefix ( ' ' ) ;
2917 } else {
30- indent -= 3 ;
18+ doc . popPrefix ( ) ;
3119 }
3220 }
3321
34- function brk ( ) {
35- if ( ret . length > 0 && ret [ ret . length - 1 ] . trim ( ) !== '' ) { ret . push ( '' ) ; }
36- }
37-
3822 function textOf ( node : commonmark . Node ) {
3923 return node . literal || '' ;
4024 }
4125
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 ::
4527
4628 pump ( ast , {
4729 block_quote ( _node , entering ) {
4830 directive ( 'epigraph' , entering ) ;
4931 } ,
5032
5133 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 ) ) ;
5436 } ,
5537
5638 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 ( ) ;
6758 }
6859 } ,
6960
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 ( '*' ) ; } ,
7768
7869 list ( ) {
79- brk ( ) ;
70+ doc . paraBreak ( ) ;
8071 } ,
8172
8273 link ( node , entering ) {
8374 if ( entering ) {
84- para . add ( '`' ) ;
75+ doc . append ( '`' ) ;
8576 } else {
86- para . add ( ' <' + ( node . destination || '' ) + '>`_' ) ;
77+ doc . append ( ' <' + ( node . destination || '' ) + '>`_' ) ;
8778 }
8879 } ,
8980
90- item ( node , _entering ) {
81+ item ( node , entering ) {
9182 // 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+ }
9489 } else {
95- nextParaPrefix = ` ${ node . listStart } . ` ;
90+ doc . popPrefix ( ) ;
9691 }
97-
9892 } ,
9993
10094 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 ( ) ;
10996
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+ }
111102
112- indent += 3 ;
103+ doc . pushBulletPrefix ( ' ' ) ;
113104
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 ) ;
116107 }
117108
118- indent -= 3 ;
109+ doc . popPrefix ( ) ;
119110 }
120111
121112 } ) ;
122113
123- return ret . join ( '\n' ) . trimRight ( ) ;
114+ return doc . toString ( ) ;
124115}
125116
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 ;
128124
129- constructor ( text ?: string ) {
130- if ( text !== undefined ) { this . parts . push ( text ) ; }
125+ constructor ( ) {
126+ this . lines . push ( [ ] ) ;
131127 }
132128
133- public add ( text : string ) {
134- this . parts . push ( text ) ;
129+ public pushPrefix ( prefix : string ) {
130+ this . prefix . push ( prefix ) ;
135131 }
136132
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 ] ;
139147 }
140148
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 ;
143178 }
144179
145180 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+ }
147189 }
148190}
149191
192+ /**
193+ * Turn a list of string fragments into a string
194+ */
195+ function partsToString ( parts : string [ ] ) {
196+ return parts . join ( '' ) . trimRight ( ) ;
197+ }
198+
150199const headings = [ '=' , '-' , '^' , '"' ] ;
151200
152201type Handler = ( node : commonmark . Node , entering : boolean ) => void ;
153202type Handlers = { [ key in commonmark . NodeType ] ?: Handler } ;
154203
204+ /**
205+ * Pump a CommonMark AST tree through a set of handlers
206+ */
155207function pump ( ast : commonmark . Node , handlers : Handlers ) {
156208 const walker = ast . walker ( ) ;
157209 let event = walker . next ( ) ;
@@ -163,4 +215,25 @@ function pump(ast: commonmark.Node, handlers: Handlers) {
163215
164216 event = walker . next ( ) ;
165217 }
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