Skip to content

Commit 1efcb6a

Browse files
committed
Import the initial implementation of AsciiDoc-to-Markdown conversion
1 parent a2beeb8 commit 1efcb6a

File tree

1 file changed

+371
-0
lines changed

1 file changed

+371
-0
lines changed

xtask/src/publish/notes.rs

Lines changed: 371 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,371 @@
1+
use anyhow::{anyhow, bail};
2+
use std::{
3+
io::{BufRead, Lines},
4+
iter::Peekable,
5+
};
6+
7+
const LISTING_DELIMITER: &'static str = "----";
8+
9+
struct Converter<'a, 'b, R: BufRead> {
10+
iter: &'a mut Peekable<Lines<R>>,
11+
output: &'b mut String,
12+
}
13+
14+
impl<'a, 'b, R: BufRead> Converter<'a, 'b, R> {
15+
fn new(iter: &'a mut Peekable<Lines<R>>, output: &'b mut String) -> Self {
16+
Self { iter, output }
17+
}
18+
19+
fn process(&mut self) -> anyhow::Result<()> {
20+
self.process_document_header()?;
21+
self.skip_blank_lines()?;
22+
self.output.push('\n');
23+
24+
loop {
25+
let line = self.iter.peek().unwrap().as_deref().map_err(|e| anyhow!("{e}"))?;
26+
if get_title(line).is_some() {
27+
let line = self.iter.next().unwrap().unwrap();
28+
let (level, title) = get_title(&line).unwrap();
29+
self.write_title(level, title);
30+
} else if get_list_item(line).is_some() {
31+
self.process_list()?;
32+
} else if line.starts_with('[') {
33+
self.process_source_code_block(0)?;
34+
} else if line.starts_with(LISTING_DELIMITER) {
35+
self.process_listing_block(None, 0)?;
36+
} else {
37+
self.process_paragraph(0)?;
38+
}
39+
40+
self.skip_blank_lines()?;
41+
if self.iter.peek().is_none() {
42+
break;
43+
}
44+
self.output.push('\n');
45+
}
46+
Ok(())
47+
}
48+
49+
fn process_document_header(&mut self) -> anyhow::Result<()> {
50+
self.process_document_title()?;
51+
52+
while let Some(line) = self.iter.next() {
53+
let line = line?;
54+
if line.is_empty() {
55+
break;
56+
}
57+
if !line.starts_with(':') {
58+
self.write_line(&line, 0)
59+
}
60+
}
61+
62+
Ok(())
63+
}
64+
65+
fn process_document_title(&mut self) -> anyhow::Result<()> {
66+
if let Some(Ok(line)) = self.iter.next() {
67+
if let Some((level, title)) = get_title(&line) {
68+
if level == 1 {
69+
self.write_title(level, title);
70+
return Ok(());
71+
}
72+
}
73+
}
74+
bail!("document title not found")
75+
}
76+
77+
fn process_list(&mut self) -> anyhow::Result<()> {
78+
while let Some(line) = self.iter.next() {
79+
let line = line?;
80+
if line.is_empty() {
81+
break;
82+
}
83+
84+
if let Some(item) = get_list_item(&line) {
85+
self.write_list_item(item);
86+
} else if line == "+" {
87+
let line = self
88+
.iter
89+
.peek()
90+
.ok_or_else(|| anyhow!("list continuation unexpectedly terminated"))?;
91+
let line = line.as_deref().map_err(|e| anyhow!("{e}"))?;
92+
if line.starts_with('[') {
93+
self.write_line("", 0);
94+
self.process_source_code_block(1)?;
95+
} else if line.starts_with(LISTING_DELIMITER) {
96+
self.write_line("", 0);
97+
self.process_listing_block(None, 1)?;
98+
} else {
99+
self.write_line("", 0);
100+
self.process_paragraph(1)?;
101+
}
102+
} else {
103+
bail!("not a list block")
104+
}
105+
}
106+
107+
Ok(())
108+
}
109+
110+
fn process_source_code_block(&mut self, level: usize) -> anyhow::Result<()> {
111+
if let Some(Ok(line)) = self.iter.next() {
112+
if let Some(styles) = line.strip_prefix("[source").and_then(|s| s.strip_suffix(']')) {
113+
let mut styles = styles.split(',');
114+
if !styles.next().unwrap().is_empty() {
115+
bail!("not a source code block");
116+
}
117+
let language = styles.next();
118+
return self.process_listing_block(language, level);
119+
}
120+
}
121+
bail!("not a source code block")
122+
}
123+
124+
fn process_listing_block(&mut self, style: Option<&str>, level: usize) -> anyhow::Result<()> {
125+
if let Some(Ok(line)) = self.iter.next() {
126+
if line == LISTING_DELIMITER {
127+
self.write_indent(level);
128+
self.output.push_str("```");
129+
if let Some(style) = style {
130+
self.output.push_str(style);
131+
}
132+
self.output.push('\n');
133+
while let Some(line) = self.iter.next() {
134+
let line = line?;
135+
if line == LISTING_DELIMITER {
136+
self.write_line("```", level);
137+
return Ok(());
138+
} else {
139+
self.write_line(&line, level);
140+
}
141+
}
142+
bail!("listing block is not terminated")
143+
}
144+
}
145+
bail!("not a listing block")
146+
}
147+
148+
fn process_paragraph(&mut self, level: usize) -> anyhow::Result<()> {
149+
while let Some(line) = self.iter.peek() {
150+
let line = line.as_deref().map_err(|e| anyhow!("{e}"))?;
151+
if line.is_empty() || (level > 0 && line == "+") {
152+
break;
153+
}
154+
155+
self.write_indent(level);
156+
let line = self.iter.next().unwrap()?;
157+
if line.ends_with('+') {
158+
let line = &line[..(line.len() - 1)];
159+
self.output.push_str(line);
160+
self.output.push('\\');
161+
} else {
162+
self.output.push_str(&line);
163+
}
164+
self.output.push('\n');
165+
}
166+
167+
Ok(())
168+
}
169+
170+
fn skip_blank_lines(&mut self) -> anyhow::Result<()> {
171+
while let Some(line) = self.iter.peek() {
172+
if !line.as_deref().unwrap().is_empty() {
173+
break;
174+
}
175+
self.iter.next().unwrap()?;
176+
}
177+
Ok(())
178+
}
179+
180+
fn write_title(&mut self, level: usize, title: &str) {
181+
for _ in 0..level {
182+
self.output.push('#');
183+
}
184+
self.output.push(' ');
185+
self.output.push_str(title);
186+
self.output.push('\n');
187+
}
188+
189+
fn write_list_item(&mut self, item: &str) {
190+
self.output.push_str("- ");
191+
self.output.push_str(item);
192+
self.output.push('\n');
193+
}
194+
195+
fn write_indent(&mut self, level: usize) {
196+
for _ in 0..level {
197+
self.output.push_str(" ");
198+
}
199+
}
200+
201+
fn write_line(&mut self, line: &str, level: usize) {
202+
self.write_indent(level);
203+
self.output.push_str(line);
204+
self.output.push('\n');
205+
}
206+
}
207+
208+
pub(crate) fn convert_asciidoc_to_markdown<R>(input: R) -> anyhow::Result<String>
209+
where
210+
R: BufRead,
211+
{
212+
let mut output = String::new();
213+
let mut iter = input.lines().peekable();
214+
215+
let mut converter = Converter::new(&mut iter, &mut output);
216+
converter.process()?;
217+
218+
Ok(output)
219+
}
220+
221+
fn get_title(line: &str) -> Option<(usize, &str)> {
222+
const MARKER: char = '=';
223+
let mut iter = line.chars();
224+
if iter.next()? != MARKER {
225+
return None;
226+
}
227+
let mut count = 1;
228+
loop {
229+
match iter.next() {
230+
Some(MARKER) => {
231+
count += 1;
232+
}
233+
Some(' ') => {
234+
break;
235+
}
236+
_ => return None,
237+
}
238+
}
239+
Some((count, iter.as_str()))
240+
}
241+
242+
fn get_list_item(line: &str) -> Option<&str> {
243+
const MARKER: &'static str = "* ";
244+
if line.starts_with(MARKER) {
245+
let item = &line[MARKER.len()..];
246+
Some(item)
247+
} else {
248+
None
249+
}
250+
}
251+
252+
#[cfg(test)]
253+
mod tests {
254+
use super::*;
255+
256+
#[test]
257+
fn test_asciidoc_to_markdown_conversion() {
258+
let input = "\
259+
= Changelog #256
260+
:sectanchors:
261+
:page-layout: post
262+
263+
Hello!
264+
265+
Commit: commit:0123456789abcdef0123456789abcdef01234567[] +
266+
Release: release:2022-01-01[]
267+
268+
== New Features
269+
270+
* pr:1111[] foo bar baz
271+
* pr:2222[] foo bar baz
272+
+
273+
image::https://example.com/animation.gif[]
274+
+
275+
video::https://example.com/movie.mp4[options=\"autoplay,loop\"]
276+
+
277+
[source,bash]
278+
----
279+
rustup update nightly
280+
----
281+
+
282+
----
283+
This is a plain listing.
284+
----
285+
+
286+
paragraph
287+
paragraph
288+
289+
== Fixes
290+
291+
* pr:3333[] foo bar baz
292+
* pr:4444[] foo bar baz
293+
294+
== Internal Improvements
295+
296+
* pr:5555[] foo bar baz
297+
* pr:6666[] foo bar baz
298+
299+
The highlight of the month is probably pr:1111[].
300+
301+
[source,bash]
302+
----
303+
rustup update nightly
304+
----
305+
306+
[source]
307+
----
308+
rustup update nightly
309+
----
310+
311+
----
312+
This is a plain listing.
313+
----
314+
";
315+
let expected = "\
316+
# Changelog #256
317+
318+
Hello!
319+
320+
Commit: commit:0123456789abcdef0123456789abcdef01234567[] \\
321+
Release: release:2022-01-01[]
322+
323+
## New Features
324+
325+
- pr:1111[] foo bar baz
326+
- pr:2222[] foo bar baz
327+
328+
image::https://example.com/animation.gif[]
329+
330+
video::https://example.com/movie.mp4[options=\"autoplay,loop\"]
331+
332+
```bash
333+
rustup update nightly
334+
```
335+
336+
```
337+
This is a plain listing.
338+
```
339+
340+
paragraph
341+
paragraph
342+
343+
## Fixes
344+
345+
- pr:3333[] foo bar baz
346+
- pr:4444[] foo bar baz
347+
348+
## Internal Improvements
349+
350+
- pr:5555[] foo bar baz
351+
- pr:6666[] foo bar baz
352+
353+
The highlight of the month is probably pr:1111[].
354+
355+
```bash
356+
rustup update nightly
357+
```
358+
359+
```
360+
rustup update nightly
361+
```
362+
363+
```
364+
This is a plain listing.
365+
```
366+
";
367+
let actual = convert_asciidoc_to_markdown(std::io::Cursor::new(input)).unwrap();
368+
369+
assert_eq!(actual, expected);
370+
}
371+
}

0 commit comments

Comments
 (0)