Skip to content

Commit 3a79142

Browse files
authored
fix(python): fix indentation for multiline bullets in RST generator (#479)
Fixes #478.
1 parent 4456138 commit 3a79142

File tree

2 files changed

+167
-77
lines changed

2 files changed

+167
-77
lines changed

packages/jsii-pacmak/lib/markdown.ts

Lines changed: 150 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -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
*/
116
export 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+
150199
const headings = ['=', '-', '^', '"'];
151200

152201
type Handler = (node: commonmark.Node, entering: boolean) => void;
153202
type Handlers = {[key in commonmark.NodeType]?: Handler };
154203

204+
/**
205+
* Pump a CommonMark AST tree through a set of handlers
206+
*/
155207
function 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+
*/

packages/jsii-pacmak/test/test.python.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,23 @@ export = {
162162

163163
test.done();
164164
},
165+
166+
'list with multiline text'(test: Test) {
167+
converts(test, [
168+
'This is a bulleted list:',
169+
'* this bullet has multiple lines.',
170+
' I hope these are indendented properly.',
171+
'* two',
172+
], [
173+
'This is a bulleted list:',
174+
'',
175+
'- this bullet has multiple lines.',
176+
' I hope these are indendented properly.',
177+
'- two',
178+
]);
179+
180+
test.done();
181+
},
165182
};
166183

167184
function converts(test: Test, input: string[], output: string[]) {

0 commit comments

Comments
 (0)