about summary refs log tree commit diff
diff options
context:
space:
mode:
authorNoritada Kobayashi <noritada.kobayashi@gmail.com>2022-12-09 23:45:45 +0900
committerNoritada Kobayashi <noritada.kobayashi@gmail.com>2022-12-09 23:45:45 +0900
commit1efcb6ad9abc0ab0c1af8092ee73dce90e7ab8e3 (patch)
tree02e8faf96f94c52706e15f5e772b022512a17a19
parenta2beeb8dbb5f4596f8c6f28a09c20355ea4c4628 (diff)
downloadrust-1efcb6ad9abc0ab0c1af8092ee73dce90e7ab8e3.tar.gz
rust-1efcb6ad9abc0ab0c1af8092ee73dce90e7ab8e3.zip
Import the initial implementation of AsciiDoc-to-Markdown conversion
-rw-r--r--xtask/src/publish/notes.rs371
1 files changed, 371 insertions, 0 deletions
diff --git a/xtask/src/publish/notes.rs b/xtask/src/publish/notes.rs
new file mode 100644
index 00000000000..257c30a208b
--- /dev/null
+++ b/xtask/src/publish/notes.rs
@@ -0,0 +1,371 @@
+use anyhow::{anyhow, bail};
+use std::{
+    io::{BufRead, Lines},
+    iter::Peekable,
+};
+
+const LISTING_DELIMITER: &'static str = "----";
+
+struct Converter<'a, 'b, R: BufRead> {
+    iter: &'a mut Peekable<Lines<R>>,
+    output: &'b mut String,
+}
+
+impl<'a, 'b, R: BufRead> Converter<'a, 'b, R> {
+    fn new(iter: &'a mut Peekable<Lines<R>>, output: &'b mut String) -> Self {
+        Self { iter, output }
+    }
+
+    fn process(&mut self) -> anyhow::Result<()> {
+        self.process_document_header()?;
+        self.skip_blank_lines()?;
+        self.output.push('\n');
+
+        loop {
+            let line = self.iter.peek().unwrap().as_deref().map_err(|e| anyhow!("{e}"))?;
+            if get_title(line).is_some() {
+                let line = self.iter.next().unwrap().unwrap();
+                let (level, title) = get_title(&line).unwrap();
+                self.write_title(level, title);
+            } else if get_list_item(line).is_some() {
+                self.process_list()?;
+            } else if line.starts_with('[') {
+                self.process_source_code_block(0)?;
+            } else if line.starts_with(LISTING_DELIMITER) {
+                self.process_listing_block(None, 0)?;
+            } else {
+                self.process_paragraph(0)?;
+            }
+
+            self.skip_blank_lines()?;
+            if self.iter.peek().is_none() {
+                break;
+            }
+            self.output.push('\n');
+        }
+        Ok(())
+    }
+
+    fn process_document_header(&mut self) -> anyhow::Result<()> {
+        self.process_document_title()?;
+
+        while let Some(line) = self.iter.next() {
+            let line = line?;
+            if line.is_empty() {
+                break;
+            }
+            if !line.starts_with(':') {
+                self.write_line(&line, 0)
+            }
+        }
+
+        Ok(())
+    }
+
+    fn process_document_title(&mut self) -> anyhow::Result<()> {
+        if let Some(Ok(line)) = self.iter.next() {
+            if let Some((level, title)) = get_title(&line) {
+                if level == 1 {
+                    self.write_title(level, title);
+                    return Ok(());
+                }
+            }
+        }
+        bail!("document title not found")
+    }
+
+    fn process_list(&mut self) -> anyhow::Result<()> {
+        while let Some(line) = self.iter.next() {
+            let line = line?;
+            if line.is_empty() {
+                break;
+            }
+
+            if let Some(item) = get_list_item(&line) {
+                self.write_list_item(item);
+            } else if line == "+" {
+                let line = self
+                    .iter
+                    .peek()
+                    .ok_or_else(|| anyhow!("list continuation unexpectedly terminated"))?;
+                let line = line.as_deref().map_err(|e| anyhow!("{e}"))?;
+                if line.starts_with('[') {
+                    self.write_line("", 0);
+                    self.process_source_code_block(1)?;
+                } else if line.starts_with(LISTING_DELIMITER) {
+                    self.write_line("", 0);
+                    self.process_listing_block(None, 1)?;
+                } else {
+                    self.write_line("", 0);
+                    self.process_paragraph(1)?;
+                }
+            } else {
+                bail!("not a list block")
+            }
+        }
+
+        Ok(())
+    }
+
+    fn process_source_code_block(&mut self, level: usize) -> anyhow::Result<()> {
+        if let Some(Ok(line)) = self.iter.next() {
+            if let Some(styles) = line.strip_prefix("[source").and_then(|s| s.strip_suffix(']')) {
+                let mut styles = styles.split(',');
+                if !styles.next().unwrap().is_empty() {
+                    bail!("not a source code block");
+                }
+                let language = styles.next();
+                return self.process_listing_block(language, level);
+            }
+        }
+        bail!("not a source code block")
+    }
+
+    fn process_listing_block(&mut self, style: Option<&str>, level: usize) -> anyhow::Result<()> {
+        if let Some(Ok(line)) = self.iter.next() {
+            if line == LISTING_DELIMITER {
+                self.write_indent(level);
+                self.output.push_str("```");
+                if let Some(style) = style {
+                    self.output.push_str(style);
+                }
+                self.output.push('\n');
+                while let Some(line) = self.iter.next() {
+                    let line = line?;
+                    if line == LISTING_DELIMITER {
+                        self.write_line("```", level);
+                        return Ok(());
+                    } else {
+                        self.write_line(&line, level);
+                    }
+                }
+                bail!("listing block is not terminated")
+            }
+        }
+        bail!("not a listing block")
+    }
+
+    fn process_paragraph(&mut self, level: usize) -> anyhow::Result<()> {
+        while let Some(line) = self.iter.peek() {
+            let line = line.as_deref().map_err(|e| anyhow!("{e}"))?;
+            if line.is_empty() || (level > 0 && line == "+") {
+                break;
+            }
+
+            self.write_indent(level);
+            let line = self.iter.next().unwrap()?;
+            if line.ends_with('+') {
+                let line = &line[..(line.len() - 1)];
+                self.output.push_str(line);
+                self.output.push('\\');
+            } else {
+                self.output.push_str(&line);
+            }
+            self.output.push('\n');
+        }
+
+        Ok(())
+    }
+
+    fn skip_blank_lines(&mut self) -> anyhow::Result<()> {
+        while let Some(line) = self.iter.peek() {
+            if !line.as_deref().unwrap().is_empty() {
+                break;
+            }
+            self.iter.next().unwrap()?;
+        }
+        Ok(())
+    }
+
+    fn write_title(&mut self, level: usize, title: &str) {
+        for _ in 0..level {
+            self.output.push('#');
+        }
+        self.output.push(' ');
+        self.output.push_str(title);
+        self.output.push('\n');
+    }
+
+    fn write_list_item(&mut self, item: &str) {
+        self.output.push_str("- ");
+        self.output.push_str(item);
+        self.output.push('\n');
+    }
+
+    fn write_indent(&mut self, level: usize) {
+        for _ in 0..level {
+            self.output.push_str("  ");
+        }
+    }
+
+    fn write_line(&mut self, line: &str, level: usize) {
+        self.write_indent(level);
+        self.output.push_str(line);
+        self.output.push('\n');
+    }
+}
+
+pub(crate) fn convert_asciidoc_to_markdown<R>(input: R) -> anyhow::Result<String>
+where
+    R: BufRead,
+{
+    let mut output = String::new();
+    let mut iter = input.lines().peekable();
+
+    let mut converter = Converter::new(&mut iter, &mut output);
+    converter.process()?;
+
+    Ok(output)
+}
+
+fn get_title(line: &str) -> Option<(usize, &str)> {
+    const MARKER: char = '=';
+    let mut iter = line.chars();
+    if iter.next()? != MARKER {
+        return None;
+    }
+    let mut count = 1;
+    loop {
+        match iter.next() {
+            Some(MARKER) => {
+                count += 1;
+            }
+            Some(' ') => {
+                break;
+            }
+            _ => return None,
+        }
+    }
+    Some((count, iter.as_str()))
+}
+
+fn get_list_item(line: &str) -> Option<&str> {
+    const MARKER: &'static str = "* ";
+    if line.starts_with(MARKER) {
+        let item = &line[MARKER.len()..];
+        Some(item)
+    } else {
+        None
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_asciidoc_to_markdown_conversion() {
+        let input = "\
+= Changelog #256
+:sectanchors:
+:page-layout: post
+
+Hello!
+
+Commit: commit:0123456789abcdef0123456789abcdef01234567[] +
+Release: release:2022-01-01[]
+
+== New Features
+
+* pr:1111[] foo bar baz
+* pr:2222[] foo bar baz
++
+image::https://example.com/animation.gif[]
++
+video::https://example.com/movie.mp4[options=\"autoplay,loop\"]
++
+[source,bash]
+----
+rustup update nightly
+----
++
+----
+This is a plain listing.
+----
++
+paragraph
+paragraph
+
+== Fixes
+
+* pr:3333[] foo bar baz
+* pr:4444[] foo bar baz
+
+== Internal Improvements
+
+* pr:5555[] foo bar baz
+* pr:6666[] foo bar baz
+
+The highlight of the month is probably pr:1111[].
+
+[source,bash]
+----
+rustup update nightly
+----
+
+[source]
+----
+rustup update nightly
+----
+
+----
+This is a plain listing.
+----
+";
+        let expected = "\
+# Changelog #256
+
+Hello!
+
+Commit: commit:0123456789abcdef0123456789abcdef01234567[] \\
+Release: release:2022-01-01[]
+
+## New Features
+
+- pr:1111[] foo bar baz
+- pr:2222[] foo bar baz
+
+  image::https://example.com/animation.gif[]
+
+  video::https://example.com/movie.mp4[options=\"autoplay,loop\"]
+
+  ```bash
+  rustup update nightly
+  ```
+
+  ```
+  This is a plain listing.
+  ```
+
+  paragraph
+  paragraph
+
+## Fixes
+
+- pr:3333[] foo bar baz
+- pr:4444[] foo bar baz
+
+## Internal Improvements
+
+- pr:5555[] foo bar baz
+- pr:6666[] foo bar baz
+
+The highlight of the month is probably pr:1111[].
+
+```bash
+rustup update nightly
+```
+
+```
+rustup update nightly
+```
+
+```
+This is a plain listing.
+```
+";
+        let actual = convert_asciidoc_to_markdown(std::io::Cursor::new(input)).unwrap();
+
+        assert_eq!(actual, expected);
+    }
+}