Repository: pacak/bpaf
Pull request #234 (2023-07-08 16:47:10)

A whole lot of markdown improvements
@ematipico - can you check if this works for you or do you see any problems in generated markdown?


Patch:
diff --git a/Changelog.md b/Changelog.md
index 10a7d5c..c6fb869 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -1,6 +1,9 @@
 # Change Log
 
-## bpaf [0.9.1] - 2023-07-05, bpaf_derive [0.5.1]
+## bpaf [0.9.2], bpaf_derive [0.5.2] - Unreleased
+- with `docgen` feature you can render documentation as markdown
+
+## bpaf [0.9.1], bpaf_derive [0.5.1] - 2023-07-05
 - add a way to print usage when called with no argument_os
 - since 0.9.0 bpaf splits help messages into "full" and "partial", displaying
   full only when `--help` flag is passed twice or when rendering the documentation,
diff --git a/bpaf_derive/src/td.rs b/bpaf_derive/src/td.rs
index a97febc..8306797 100644
--- a/bpaf_derive/src/td.rs
+++ b/bpaf_derive/src/td.rs
@@ -194,6 +194,10 @@ impl Parse for TopInfo {
             } else if kw == "long" {
                 command(&kw, mode)?;
                 attrs.push(TopAttr::CommandLong(parse_arg(input)?));
+            } else if kw == "header" {
+                attrs.push(TopAttr::Header(Help::Custom(parse_arg(input)?)));
+            } else if kw == "footer" {
+                attrs.push(TopAttr::Footer(Help::Custom(parse_arg(input)?)));
             } else if kw == "usage" {
                 options(&kw, mode).or_else(|_| command(&kw, mode))?;
                 attrs.push(TopAttr::Usage(parse_arg(input)?));
@@ -279,6 +283,10 @@ impl Parse for Ed {
                 attrs.push(EAttr::Adjacent);
             } else if kw == "usage" {
                 attrs.push(EAttr::Usage(parse_arg(input)?));
+            } else if kw == "header" {
+                attrs.push(EAttr::Header(Help::Custom(parse_arg(input)?)));
+            } else if kw == "footer" {
+                attrs.push(EAttr::Footer(Help::Custom(parse_arg(input)?)));
             } else if kw == "env" {
                 attrs.push(EAttr::Env(parse_arg(input)?));
             } else {
@@ -313,6 +321,8 @@ pub(crate) enum EAttr {
     UnitShort(Option<LitChar>),
     UnitLong(Option<LitStr>),
     Descr(Help),
+    Header(Help),
+    Footer(Help),
     Usage(Box<Expr>),
     Env(Box<Expr>),
     ToOptions,
@@ -327,6 +337,8 @@ impl ToTokens for EAttr {
             Self::CommandLong(n) => quote!(long(#n)),
             Self::Adjacent => quote!(adjacent()),
             Self::Descr(d) => quote!(descr(#d)),
+            Self::Header(d) => quote!(header(#d)),
+            Self::Footer(d) => quote!(footer(#d)),
             Self::Usage(u) => quote!(usage(#u)),
             Self::Env(e) => quote!(env(#e)),
             Self::Hide => quote!(hide()),
diff --git a/bpaf_derive/src/top.rs b/bpaf_derive/src/top.rs
index 9435a37..af30760 100644
--- a/bpaf_derive/src/top.rs
+++ b/bpaf_derive/src/top.rs
@@ -78,23 +78,7 @@ impl Parse for Top {
                     attrs.push(attr);
 
                     if let Some(h) = std::mem::take(&mut help) {
-                        match &h {
-                            Help::Custom(_) => attrs.push(TopAttr::Descr(h)),
-                            Help::Doc(c) => {
-                                let mut chunks = LineIter::from(c.as_str());
-                                if let Some(s) = chunks.next() {
-                                    attrs.push(TopAttr::Descr(Help::Doc(s)));
-                                }
-                                if let Some(s) = chunks.next() {
-                                    if !s.is_empty() {
-                                        attrs.push(TopAttr::Header(Help::Doc(s)));
-                                    }
-                                }
-                                if let Some(s) = chunks.rest() {
-                                    attrs.push(TopAttr::Footer(Help::Doc(s)));
-                                }
-                            }
-                        }
+                        split_help_into(h, &mut attrs);
                     }
                 }
                 TopAttr::Usage(_)
@@ -111,7 +95,7 @@ impl Parse for Top {
                     attrs.push(TopAttr::ToOptions);
 
                     if let Some(h) = std::mem::take(&mut help) {
-                        attrs.push(TopAttr::Descr(h));
+                        split_help_into(h, &mut attrs);
                     }
 
                     attrs.push(attr);
@@ -161,6 +145,46 @@ impl Parse for Top {
     }
 }
 
+fn split_help_into(h: Help, attrs: &mut Vec<TopAttr>) {
+    match &h {
+        Help::Custom(_) => attrs.push(TopAttr::Descr(h)),
+        Help::Doc(c) => {
+            let mut chunks = LineIter::from(c.as_str());
+            if let Some(s) = chunks.next() {
+                attrs.push(TopAttr::Descr(Help::Doc(s)));
+            }
+            if let Some(s) = chunks.next() {
+                if !s.is_empty() {
+                    attrs.push(TopAttr::Header(Help::Doc(s)));
+                }
+            }
+            if let Some(s) = chunks.rest() {
+                attrs.push(TopAttr::Footer(Help::Doc(s)));
+            }
+        }
+    }
+}
+
+fn split_ehelp_into(h: Help, opts_at: usize, attrs: &mut Vec<EAttr>) {
+    match &h {
+        Help::Custom(_) => attrs.push(EAttr::Descr(h)),
+        Help::Doc(c) => {
+            let mut chunks = LineIter::from(c.as_str());
+            if let Some(s) = chunks.next() {
+                attrs.insert(opts_at, EAttr::Descr(Help::Doc(s)));
+            }
+            if let Some(s) = chunks.next() {
+                if !s.is_empty() {
+                    attrs.insert(opts_at, EAttr::Header(Help::Doc(s)));
+                }
+            }
+            if let Some(s) = chunks.rest() {
+                attrs.insert(opts_at, EAttr::Footer(Help::Doc(s)));
+            }
+        }
+    }
+}
+
 impl ToTokens for Top {
     fn to_tokens(&self, tokens: &mut TokenStream) {
         let Top {
@@ -339,25 +363,20 @@ impl ParsedEnumBranch {
         }
 
         let mut attrs = Vec::with_capacity(ea.len());
+        let mut has_options = None;
 
         for attr in ea {
             match attr {
                 EAttr::NamedCommand(_) => {
                     branch.set_command();
                     attrs.push(EAttr::ToOptions);
-                    if let Some(h) = std::mem::take(&mut help) {
-                        attrs.push(EAttr::Descr(h));
-                    }
+                    has_options = Some(attrs.len());
                     attrs.push(attr);
                 }
                 EAttr::UnnamedCommand => {
                     branch.set_command();
                     attrs.push(EAttr::ToOptions);
-
-                    if let Some(h) = std::mem::take(&mut help) {
-                        attrs.push(EAttr::Descr(h));
-                    }
-
+                    has_options = Some(attrs.len());
                     attrs.push(EAttr::NamedCommand(ident_to_long(&branch.ident)));
                 }
 
@@ -379,7 +398,18 @@ impl ParsedEnumBranch {
                     }
                 }
                 EAttr::Adjacent | EAttr::Hide => attrs.push(attr),
-                EAttr::ToOptions | EAttr::Descr(_) => unreachable!(),
+                EAttr::Header(_) | EAttr::Footer(_) | EAttr::Descr(_) => {
+                    if let Some(o) = attrs.iter().position(|i| matches!(i, EAttr::ToOptions)) {
+                        attrs.insert(o + 1, attr);
+                    }
+                }
+                EAttr::ToOptions => unreachable!(),
+            }
+        }
+
+        if let Some(opts_at) = has_options {
+            if let Some(h) = std::mem::take(&mut help) {
+                split_ehelp_into(h, opts_at, &mut attrs);
             }
         }
         branch.set_inplicit_name();
@@ -546,24 +576,3 @@ pub(crate) enum FieldSet {
     Unit(Vec<StrictName>, Option<Help>),
     Pure(Box<Expr>),
 }
-
-/*
-impl ToTokens for FieldSet {
-    fn to_tokens(&self, tokens: &mut TokenStream) {
-        match self {
-            FieldSet::Named(n) => {
-                let names = n.iter().enumerate().map(|(ix, field)| field.var_name(ix));
-                let items = n.iter();
-                quote! {{
-                    #( let #names = #items; )*
-                }}
-            }
-            FieldSet::Unnamed(_) => todo!(),
-            FieldSet::Unit => quote!(),
-            FieldSet::Pure(_) => todo!(),
-        }
-        .to_tokens(tokens)
-    }
-}
-*/
-// }}}
diff --git a/bpaf_derive/src/utils.rs b/bpaf_derive/src/utils.rs
index 5dcc4c4..d20ed09 100644
--- a/bpaf_derive/src/utils.rs
+++ b/bpaf_derive/src/utils.rs
@@ -167,8 +167,12 @@ impl Iterator for LineIter<'_> {
                     }
                     self.prev_empty = true;
                 } else {
+                    if self.prev_empty {
+                        self.current.push('\n');
+                    }
                     self.current.push_str(line);
                     self.current.push('\n');
+                    self.prev_empty = false;
                 }
             } else {
                 if self.current.is_empty() {
@@ -179,3 +183,34 @@ impl Iterator for LineIter<'_> {
         }
     }
 }
+
+#[cfg(test)]
+fn split(input: &str) -> LineIter {
+    LineIter::from(input)
+}
+
+#[test]
+fn splitter_preserves_line_breaks() {
+    let x = split("a\nb").collect::<Vec<_>>();
+    assert_eq!(x, ["a\nb"]);
+
+    let x = split("a\n\nb").collect::<Vec<_>>();
+    assert_eq!(x, ["a\n\nb"]);
+
+    let x = split("a\n\n\nb").collect::<Vec<_>>();
+    assert_eq!(x, ["a", "b"]);
+}
+
+#[test]
+fn splitter_with_code_blocks() {
+    let input = "Make a tree\n\n\n\n\nExamples:\n\n```sh\ncargo 1\ncargo 2\n```";
+    let out = split(input).collect::<Vec<_>>();
+    assert_eq!(
+        out,
+        [
+            "Make a tree",
+            "",
+            "Examples:\n\n```sh\ncargo 1\ncargo 2\n```"
+        ]
+    );
+}
diff --git a/src/buffer.rs b/src/buffer.rs
index 7c0f342..916b582 100644
--- a/src/buffer.rs
+++ b/src/buffer.rs
@@ -1,16 +1,21 @@
+#[cfg(feature = "docgen")]
 use crate::{
     info::Info,
-    item::{Item, ShortLong},
     meta_help::{HelpItem, HelpItems},
+};
+use crate::{
+    item::{Item, ShortLong},
     Meta,
 };
 
 mod console;
 mod html;
+#[cfg(feature = "docgen")]
 mod manpage;
 mod splitter;
 
 pub(crate) use self::console::Color;
+#[cfg(feature = "docgen")]
 pub use manpage::Section;
 
 impl From<&[(&str, Style)]> for Doc {
@@ -230,12 +235,15 @@ impl Doc {
         }
 
         let meta = meta.normalized(for_usage);
+        self.token(Token::BlockStart(Block::Mono));
         go(&meta, self);
+        self.token(Token::BlockEnd(Block::Mono));
     }
 }
 
 /// Style of a text fragment inside of [`Doc`]
 #[derive(Debug, Copy, Clone, Eq, PartialEq)]
+#[non_exhaustive]
 pub enum Style {
     /// Plain text, no decorations
     Text,
@@ -254,6 +262,7 @@ pub enum Style {
 }
 
 #[derive(Debug, Copy, Clone, Eq, PartialEq)]
+
 pub(crate) enum Block {
     /// level 1 section header, block for separate command inside manpage, not used in --help
     Header,
@@ -289,7 +298,13 @@ pub(crate) enum Block {
     /// In markdown this becomes a link to a term if one is defined
     TermRef,
 
+    /// Surrounds metavars block in manpage
+    ///
+    /// used only inside render_manpage at the moment
     Meta,
+
+    /// Monospaced font that goes around [`Meta`]
+    Mono,
 }
 
 #[derive(Debug, Copy, Clone)]
@@ -440,6 +455,7 @@ impl Doc {
     }
 }
 
+#[cfg(feature = "docgen")]
 #[derive(Debug, Clone)]
 struct DocSection<'a> {
     /// Path name to the command name starting from the application
@@ -448,6 +464,7 @@ struct DocSection<'a> {
     meta: &'a Meta,
 }
 
+#[cfg(feature = "docgen")]
 fn extract_sections<'a>(
     meta: &'a Meta,
     info: &'a Info,
@@ -467,7 +484,6 @@ fn extract_sections<'a>(
             short: _,
             help: _,
             meta,
-            #[cfg(feature = "manpage")]
             info,
         } = item
         {
diff --git a/src/buffer/console.rs b/src/buffer/console.rs
index 6c67f5e..a625d1f 100644
--- a/src/buffer/console.rs
+++ b/src/buffer/console.rs
@@ -218,13 +218,16 @@ impl Doc {
                                     }
                                 }
 
+                                let mut pushed = 0;
                                 if let Some(missing) = margin.checked_sub(char_pos) {
                                     res.push_str(&PADDING[..missing]);
                                     char_pos = margin;
+                                    pushed = missing;
                                 }
-                                if pending_margin && char_pos >= MAX_TAB + 4 {
-                                    res.push_str("  ");
-                                    char_pos += 2;
+                                if pending_margin && char_pos >= MAX_TAB + 4 && pushed < 2 {
+                                    let missing = 2 - pushed;
+                                    res.push_str(&PADDING[..missing]);
+                                    char_pos += missing;
                                 }
 
                                 pending_newline = false;
@@ -286,7 +289,7 @@ impl Doc {
                         Block::Block => {
                             margins.push(margin);
                         }
-                        Block::DefinitionList | Block::Meta => {}
+                        Block::DefinitionList | Block::Meta | Block::Mono => {}
                         Block::TermRef => {
                             if color == Color::Monochrome {
                                 res.push('`');
@@ -309,7 +312,8 @@ impl Doc {
                         | Block::Section3
                         | Block::ItemTerm
                         | Block::DefinitionList
-                        | Block::Meta => {}
+                        | Block::Meta
+                        | Block::Mono => {}
                         Block::InlineBlock => {
                             skip.pop();
                         }
diff --git a/src/buffer/html.rs b/src/buffer/html.rs
index 47791ff..12c597e 100644
--- a/src/buffer/html.rs
+++ b/src/buffer/html.rs
@@ -1,14 +1,20 @@
 use crate::{
     buffer::{
-        extract_sections,
         splitter::{split, Chunk},
-        Block, Info, Meta, Skip, Style, Token,
+        Block, Skip, Style, Token,
     },
+    Doc, OptionParser,
+};
+
+#[cfg(feature = "docgen")]
+use crate::{
+    buffer::{extract_sections, Info, Meta},
     meta_help::render_help,
-    Doc, OptionParser, Parser,
+    Parser,
 };
 
 #[inline(never)]
+#[cfg(feature = "docgen")]
 fn collect_html(app: String, meta: &Meta, info: &Info) -> Doc {
     let mut sections = Vec::new();
     let root = meta;
@@ -56,11 +62,13 @@ fn collect_html(app: String, meta: &Meta, info: &Info) -> Doc {
 
 impl<T> OptionParser<T> {
     /// Render command line documentation for the app into html/markdown mix
+    #[cfg(feature = "docgen")]
     pub fn render_html(&self, app: impl Into<String>) -> String {
         collect_html(app.into(), &self.inner.meta(), &self.info).render_html(true, false)
     }
 
     /// Render command line documentation for the app into Markdown
+    #[cfg(feature = "docgen")]
     pub fn render_markdown(&self, app: impl Into<String>) -> String {
         collect_html(app.into(), &self.inner.meta(), &self.info).render_markdown(true)
     }
@@ -138,7 +146,6 @@ fn change_to_markdown_style(res: &mut String, cur: &mut Styles, new: Styles) {
     if new.bold {
         res.push_str("**");
     }
-
     if new.mono {
         res.push('`');
     }
@@ -253,6 +260,7 @@ impl Doc {
                         }
                         Block::Meta => todo!(),
                         Block::Section3 => res.push_str("<div style='padding-left: 0.5em'>"),
+                        Block::Mono => {}
                         Block::TermRef => {}
                         Block::InlineBlock => {
                             skip.push();
@@ -286,6 +294,7 @@ impl Doc {
                         Block::Block => {
                             res.push_str("</p>");
                         }
+                        Block::Mono => {}
                         Block::Meta => todo!(),
                         Block::TermRef => {}
                         Block::Section3 => res.push_str("</div>"),
@@ -310,6 +319,9 @@ impl Doc {
         let mut skip = Skip::default();
         let mut stack = Vec::new();
         let mut empty_term = false;
+        let mut mono = 0;
+        let mut def_list = false;
+        let mut code_block = false;
         for (ix, token) in self.tokens.iter().copied().enumerate() {
             match token {
                 Token::Text { bytes, style } => {
@@ -323,10 +335,39 @@ impl Doc {
 
                     for chunk in split(input) {
                         match chunk {
-                            Chunk::Raw(input, _) => res.push_str(input),
+                            Chunk::Raw(input, w) => {
+                                if w == Chunk::TICKED_CODE {
+                                    new_markdown_line(&mut res);
+                                    res.push_str("  ");
+                                    res.push_str(input);
+                                    res.push('\n');
+                                } else if w == Chunk::CODE {
+                                    if !code_block {
+                                        res.push_str("\n\n  ```text\n");
+                                    }
+                                    code_block = true;
+                                    res.push_str("  ");
+                                    res.push_str(input);
+                                    res.push('\n');
+                                } else {
+                                    if code_block {
+                                        res.push_str("\n  ```\n");
+                                        code_block = false;
+                                    }
+                                    if mono > 0 {
+                                        let input = input.replace('[', "\\[").replace(']', "\\]");
+                                        res.push_str(&input);
+                                    } else {
+                                        res.push_str(input);
+                                    }
+                                }
+                            }
                             Chunk::Paragraph => {
                                 if full {
-                                    res.push('\n');
+                                    res.push_str("\n\n");
+                                    if def_list {
+                                        res.push_str("  ");
+                                    }
                                 } else {
                                     skip.enable();
                                     break;
@@ -335,6 +376,11 @@ impl Doc {
                             Chunk::LineBreak => res.push('\n'),
                         }
                     }
+
+                    if code_block {
+                        res.push_str("  ```\n");
+                        code_block = false;
+                    }
                 }
                 Token::BlockStart(b) => {
                     change_to_markdown_style(&mut res, &mut cur_style, Styles::default());
@@ -347,6 +393,7 @@ impl Doc {
                             res.push_str("");
                         }
                         Block::ItemTerm => {
+                            new_markdown_line(&mut res);
                             empty_term = matches!(
                                 self.tokens.get(ix + 1),
                                 Some(Token::BlockEnd(Block::ItemTerm))
@@ -354,15 +401,23 @@ impl Doc {
                             res.push_str(if empty_term { "  " } else { "- " });
                         }
                         Block::ItemBody => {
+                            if def_list {
+                                res.push_str(if empty_term { " " } else { " &mdash; " });
+                            }
                             new_markdown_line(&mut res);
+                            res.push_str("  ");
                         }
                         Block::DefinitionList => {
+                            def_list = true;
                             res.push_str("");
                         }
                         Block::Block => {
                             res.push('\n');
                         }
                         Block::Meta => todo!(),
+                        Block::Mono => {
+                            mono += 1;
+                        }
                         Block::Section3 => res.push_str("### "),
                         Block::TermRef => {}
                         Block::InlineBlock => {
@@ -383,16 +438,22 @@ impl Doc {
                         Block::InlineBlock => {
                             skip.pop();
                         }
-                        Block::ItemTerm => res.push_str(if empty_term { " " } else { " &mdash; " }),
+                        Block::ItemTerm => {}
                         Block::ItemBody => {
-                            if stack.last().copied() == Some(Block::DefinitionList) {
+                            if def_list {
                                 res.push('\n');
                             }
                         }
-                        Block::DefinitionList => res.push('\n'),
+                        Block::DefinitionList => {
+                            def_list = false;
+                            res.push('\n');
+                        }
                         Block::Block => {
                             res.push('\n');
                         }
+                        Block::Mono => {
+                            mono -= 1;
+                        }
                         Block::Meta => todo!(),
                         Block::TermRef => {}
                         Block::Section3 => res.push('\n'),
diff --git a/src/buffer/manpage.rs b/src/buffer/manpage.rs
index 5d2237e..eea250d 100644
--- a/src/buffer/manpage.rs
+++ b/src/buffer/manpage.rs
@@ -196,7 +196,10 @@ impl Doc {
                         Block::ItemTerm => {
                             roff.control0("TP").strip_newlines(true);
                         }
-                        Block::ItemBody | Block::DefinitionList | Block::InlineBlock => {}
+                        Block::Mono
+                        | Block::ItemBody
+                        | Block::DefinitionList
+                        | Block::InlineBlock => {}
                         Block::Block => {
                             roff.control0("PP");
                         }
@@ -226,7 +229,8 @@ impl Doc {
                         Block::ItemBody => {
                             roff.control0("PP").strip_newlines(false);
                         }
-                        Block::DefinitionList | Block::Block | Block::InlineBlock => {}
+                        Block::Mono | Block::DefinitionList | Block::Block | Block::InlineBlock => {
+                        }
                         Block::Meta => {
                             roff.control0("fi");
                         }
diff --git a/src/buffer/splitter.rs b/src/buffer/splitter.rs
index 784879a..9b325d4 100644
--- a/src/buffer/splitter.rs
+++ b/src/buffer/splitter.rs
@@ -1,11 +1,24 @@
 pub(super) struct Splitter<'a> {
     input: &'a str,
+
+    #[cfg(feature = "docgen")]
+    code: Code,
+}
+
+enum Code {
+    No,
+    First,
+    Rest,
 }
 
 /// Split payload into chunks annotated with character width and containing no newlines according
 /// to text formatting rules
 pub(super) fn split(input: &str) -> Splitter {
-    Splitter { input }
+    Splitter {
+        input,
+        #[cfg(feature = "docgen")]
+        code: Code::No,
+    }
 }
 
 #[cfg_attr(test, derive(Debug, Clone, Copy, Eq, PartialEq))]
@@ -15,21 +28,87 @@ pub(super) enum Chunk<'a> {
     LineBreak,
 }
 
+impl Chunk<'_> {
+    pub(crate) const CODE: usize = 1_000_000;
+    pub(crate) const TICKED_CODE: usize = 1_000_001;
+}
+
 impl<'a> Iterator for Splitter<'a> {
     type Item = Chunk<'a>;
 
+    // 1. paragraphs are separated by a blank line.
+    // 2. code blocks are aligned by 4 spaces and are kept intact
+    // 3. linebreaks followed by space are preserved
+    // 4. leftovers are fed word by word
+    // 5. everything between "^```" is passed as is
+
+    // 1. "\n\n" = Paragraph
+    // 2. "\n " = LineBreak
+    // 3. "\n" = " "
+    // 4. "\n    " = code block
+    // 5. take next word
+    //
+
     fn next(&mut self) -> Option<Self::Item> {
         if self.input.is_empty() {
-            None
-        } else if let Some(tail) = self.input.strip_prefix("\n\n") {
-            self.input = tail;
-            Some(Chunk::Paragraph)
-        } else if let Some(tail) = self.input.strip_prefix("\n ") {
-            self.input = tail;
-            Some(Chunk::LineBreak)
-        } else if let Some(tail) = self.input.strip_prefix('\n') {
-            self.input = tail;
-            Some(Chunk::Raw(" ", 1))
+            return None;
+        }
+
+        #[cfg(feature = "docgen")]
+        if matches!(self.code, Code::First | Code::Rest) {
+            if matches!(self.code, Code::Rest) && self.input.starts_with("```") {
+                self.code = Code::No;
+            }
+            if matches!(self.code, Code::First) {
+                self.code = Code::Rest;
+            }
+
+            let tail = self.input;
+            let code = if let Some((code, rest)) = self.input.split_once('\n') {
+                let tail = &tail[code.len()..];
+                if tail.starts_with("\n\n") && matches!(self.code, Code::No) {
+                    self.input = tail;
+                } else {
+                    self.input = rest;
+                }
+                code
+            } else {
+                self.input = "";
+                tail
+            };
+            return Some(Chunk::Raw(code, Chunk::TICKED_CODE));
+        }
+
+        if let Some(tail) = self.input.strip_prefix('\n') {
+            if let Some(tail) = tail.strip_prefix("    ") {
+                let code = if let Some((code, _rest)) = tail.split_once('\n') {
+                    self.input = &tail[code.len()..];
+                    code
+                } else {
+                    self.input = "";
+                    tail
+                };
+                Some(Chunk::Raw(code, Chunk::CODE))
+            } else if tail.starts_with("\n```") {
+                #[cfg(feature = "docgen")]
+                {
+                    self.code = Code::First;
+                }
+                self.input = &tail[1..];
+                Some(Chunk::Paragraph)
+            } else if tail.starts_with("\n    ") {
+                self.input = tail;
+                Some(Chunk::Paragraph)
+            } else if let Some(tail) = tail.strip_prefix('\n') {
+                self.input = tail;
+                Some(Chunk::Paragraph)
+            } else if let Some(tail) = tail.strip_prefix(' ') {
+                self.input = tail;
+                Some(Chunk::LineBreak)
+            } else {
+                self.input = tail;
+                Some(Chunk::Raw(" ", 1))
+            }
         } else if let Some(tail) = self.input.strip_prefix(' ') {
             self.input = tail;
             Some(Chunk::Raw(" ", 1))
@@ -52,3 +131,49 @@ impl<'a> Iterator for Splitter<'a> {
         }
     }
 }
+
+#[test]
+fn space_code_block() {
+    use Chunk::*;
+    let xs = split("a\n\n    a\n    b\n\ndf\n\n    c\n    d\n").collect::<Vec<_>>();
+    assert_eq!(
+        xs,
+        [
+            Raw("a", 1),
+            Paragraph,
+            Raw("a", 1000000),
+            Raw("b", 1000000),
+            Paragraph,
+            Raw("df", 2),
+            Paragraph,
+            Raw("c", 1000000),
+            Raw("d", 1000000),
+            Raw(" ", 1),
+        ]
+    );
+}
+
+#[test]
+fn ticks_code_block() {
+    use Chunk::*;
+    let a = "a\n\n```text\na\nb\n```\n\ndf\n\n```\nc\nd\n```\n";
+    let xs = split(a).collect::<Vec<_>>();
+    assert_eq!(
+        xs,
+        [
+            Raw("a", 1),
+            Paragraph,
+            Raw("```text", Chunk::TICKED_CODE),
+            Raw("a", Chunk::TICKED_CODE),
+            Raw("b", Chunk::TICKED_CODE),
+            Raw("```", Chunk::TICKED_CODE),
+            Paragraph,
+            Raw("df", 2),
+            Paragraph,
+            Raw("```", Chunk::TICKED_CODE),
+            Raw("c", Chunk::TICKED_CODE),
+            Raw("d", Chunk::TICKED_CODE),
+            Raw("```", Chunk::TICKED_CODE),
+        ],
+    );
+}
diff --git a/src/doc.rs b/src/doc.rs
index ff55233..81c1543 100644
--- a/src/doc.rs
+++ b/src/doc.rs
@@ -46,7 +46,11 @@
 //! ```
 
 #[doc(inline)]
-pub use crate::buffer::{Doc, MetaInfo, Section, Style};
+pub use crate::buffer::{Doc, MetaInfo, Style};
+
+#[doc(inline)]
+#[cfg(feature = "docgen")]
+pub use crate::buffer::Section;
 
 #[cfg(doc)]
 use crate::*;
diff --git a/src/meta_help.rs b/src/meta_help.rs
index c40b9bf..f1e407a 100644
--- a/src/meta_help.rs
+++ b/src/meta_help.rs
@@ -38,7 +38,7 @@ pub(crate) enum HelpItem<'a> {
         short: Option<char>,
         help: Option<&'a Doc>,
         meta: &'a Meta,
-        #[cfg(feature = "manpage")]
+        #[cfg(feature = "docgen")]
         info: &'a Info,
     },
     Flag {
@@ -289,16 +289,16 @@ impl<'a> From<&'a Item> for HelpItem<'a> {
                 short,
                 help,
                 meta,
-                #[cfg(feature = "manpage")]
+                #[cfg(feature = "docgen")]
                 info,
-                #[cfg(not(feature = "manpage"))]
+                #[cfg(not(feature = "docgen"))]
                     info: _,
             } => Self::Command {
                 name,
                 short: *short,
                 help: help.as_ref(),
                 meta,
-                #[cfg(feature = "manpage")]
+                #[cfg(feature = "docgen")]
                 info,
             },
             Item::Flag {
@@ -403,7 +403,7 @@ fn write_help_item(buf: &mut Doc, item: &HelpItem, include_env: bool) {
             short,
             help,
             meta: _,
-            #[cfg(feature = "manpage")]
+            #[cfg(feature = "docgen")]
                 info: _,
         } => {
             buf.token(Token::BlockStart(Block::ItemTerm));
@@ -543,8 +543,10 @@ pub(crate) fn render_help(
     } else {
         buf.write_str("Usage", Style::Emphasis);
         buf.write_str(": ", Style::Text);
+        buf.token(Token::BlockStart(Block::Mono));
         buf.write_path(path);
         buf.write_meta(parser_meta, true);
+        buf.token(Token::BlockEnd(Block::Mono));
     }
     buf.token(Token::BlockEnd(Block::Block));
 
@@ -657,7 +659,6 @@ impl Doc {
         }
     }
 
-    // TODO - use this
     pub(crate) fn write_path(&mut self, path: &[String]) {
         for item in path {
             self.write_str(item, Style::Literal);


Test patch:
diff --git a/bpaf_derive/src/top_tests.rs b/bpaf_derive/src/top_tests.rs
index a7ec460..fac9553 100644
--- a/bpaf_derive/src/top_tests.rs
+++ b/bpaf_derive/src/top_tests.rs
@@ -70,7 +70,13 @@ fn top_enum_construct() {
 fn top_struct_options1() {
     let top: Top = parse_quote! {
         /// those are options
-        #[bpaf(options)]
+        ///
+        ///
+        /// header
+        ///
+        ///
+        /// footer
+        #[bpaf(options, header(h), footer(f))]
         struct Opt {}
     };
 
@@ -83,6 +89,10 @@ fn top_struct_options1() {
                 }
                 .to_options()
                 .descr("those are options")
+                .header("header")
+                .footer("footer")
+                .header(h)
+                .footer(f)
         }
     };
 
@@ -193,13 +203,59 @@ fn empty_enum() {
 }
 */
 
+#[test]
+fn enum_markdownish() {
+    let input: Top = parse_quote! {
+        enum Opt {
+            /// Make a tree
+            ///
+            ///
+            ///
+            ///
+            /// Examples:
+            ///
+            /// ```sh
+            /// cargo 1
+            /// cargo 2
+            /// ```
+            #[bpaf(command, header("x"))]
+            Opt { field: bool },
+        }
+    };
+
+    let expected = quote! {
+        fn opt() -> impl ::bpaf::Parser<Opt> {
+            #[allow(unused_imports)]
+            use ::bpaf::Parser;
+            {
+                let field = ::bpaf::long("field").switch();
+                ::bpaf::construct!(Opt::Opt { field ,})
+            }
+            .to_options()
+            .footer("Examples:\n\n```sh\ncargo 1\ncargo 2\n```")
+            .descr("Make a tree")
+            .header("x")
+            .command("opt")
+        }
+    };
+
+    assert_eq!(input.to_token_stream().to_string(), expected.to_string());
+}
+
 #[test]
 fn enum_command() {
     let input: Top = parse_quote! {
         // those are options
+        #[bpaf(options, header(h), footer(f))]
         enum Opt {
             #[bpaf(command("foo"))]
             /// foo doc
+            ///
+            ///
+            /// header
+            ///
+            ///
+            /// footer
             Foo { field: usize },
             /// bar doc
             #[bpaf(command, adjacent)]
@@ -208,7 +264,7 @@ fn enum_command() {
     };
 
     let expected = quote! {
-        fn opt() -> impl ::bpaf::Parser<Opt> {
+        fn opt() -> ::bpaf::OptionParser<Opt> {
             #[allow(unused_imports)]
             use ::bpaf::Parser;
             {
@@ -217,6 +273,8 @@ fn enum_command() {
                     ::bpaf::construct!(Opt::Foo { field, })
                 }
                 .to_options()
+                .footer("footer")
+                .header("header")
                 .descr("foo doc")
                 .command("foo");
 
@@ -230,6 +288,9 @@ fn enum_command() {
                 .adjacent();
                 ::bpaf::construct!([alt0, alt1, ])
             }
+            .to_options()
+            .header(h)
+            .footer(f)
         }
     };
     assert_eq!(input.to_token_stream().to_string(), expected.to_string());
diff --git a/tests/help_format.rs b/tests/help_format.rs
index 69df664..20ee16f 100644
--- a/tests/help_format.rs
+++ b/tests/help_format.rs
@@ -508,3 +508,79 @@ fn custom_help_and_version() {
     let expected = "Usage: [-a]\n\nAvailable options:\n    -a\n    -H, --halp  halps you\n";
     assert_eq!(r, expected);
 }
+
+#[test]
+fn various_name_lengths_under() {
+    let parser = short('a')
+        .long("123456789012345")
+        .help("A")
+        .switch()
+        .to_options();
+    let r = parser.run_inner(&["--help"]).unwrap_err().unwrap_stdout();
+
+    let expected = "\
+Usage: [-a]
+
+Available options:
+    -a, --123456789012345  A
+    -h, --help             Prints help information
+";
+    assert_eq!(r, expected);
+}
+
+#[test]
+fn various_name_lengths_at() {
+    let parser = short('a')
+        .long("1234567890123456")
+        .help("A")
+        .switch()
+        .to_options();
+    let r = parser.run_inner(&["--help"]).unwrap_err().unwrap_stdout();
+
+    let expected = "\
+Usage: [-a]
+
+Available options:
+    -a, --1234567890123456  A
+    -h, --help              Prints help information
+";
+    assert_eq!(r, expected);
+}
+
+#[test]
+fn various_name_lengths_over1() {
+    let parser = short('a')
+        .long("12345678901234567")
+        .help("A")
+        .switch()
+        .to_options();
+    let r = parser.run_inner(&["--help"]).unwrap_err().unwrap_stdout();
+
+    let expected = "\
+Usage: [-a]
+
+Available options:
+    -a, --12345678901234567  A
+    -h, --help               Prints help information
+";
+    assert_eq!(r, expected);
+}
+
+#[test]
+fn various_name_lengths_over2() {
+    let parser = short('a')
+        .long("1234567890123456789")
+        .help("A")
+        .switch()
+        .to_options();
+    let r = parser.run_inner(&["--help"]).unwrap_err().unwrap_stdout();
+
+    let expected = "\
+Usage: [-a]
+
+Available options:
+    -a, --1234567890123456789  A
+    -h, --help  Prints help information
+";
+    assert_eq!(r, expected);
+}
diff --git a/tests/markdown.md b/tests/markdown.md
index 645ea1a..e31d8ad 100644
--- a/tests/markdown.md
+++ b/tests/markdown.md
@@ -2,19 +2,19 @@
 
 I am a program and I do things
 
-**Usage**: **`simple`** [**`-d`**] **`--user`**=_`USER`_
+**Usage**: **`simple`** \[**`-d`**\] **`--user`**=_`USER`_
 
 Sometimes they even work.
 
 **Available options:**
 - **`-d`**, **`--kraken`** &mdash; 
-Unleash the kraken
+  Unleash the kraken
 - **`    --user`**=_`USER`_ &mdash; 
-Log in as this user
+  Log in as this user
    
-Uses environment variable **`USER`**
+  Uses environment variable **`USER`**
 - **`-h`**, **`--help`** &mdash; 
-Prints help information
+  Prints help information
 
 
 
diff --git a/tests/markdown.rs b/tests/markdown.rs
index a4d6a68..ec5dcf1 100644
--- a/tests/markdown.rs
+++ b/tests/markdown.rs
@@ -1,3 +1,6 @@
+#![cfg(feature = "docgen")]
+#![allow(dead_code)]
+
 use bpaf::*;
 
 fn write_updated(new_val: &str, path: impl AsRef<std::path::Path>) -> std::io::Result<bool> {
@@ -42,3 +45,212 @@ fn simple() {
     #[cfg(unix)]
     assert!(write_updated(&roff, "tests/markdown.md").unwrap());
 }
+
+#[test]
+fn nested() {
+    #[derive(Debug, Clone, Bpaf)]
+    /// Options
+    #[bpaf(options)]
+    enum Options {
+        #[bpaf(command)]
+        /// Alpha
+        Alpha,
+
+        #[bpaf(command)]
+        /// Beta
+        ///
+        /// More Beta
+        ///
+        /// Even More Beta
+        Beta,
+    }
+
+    let r = options().render_markdown("options");
+    let expected = "
+
+
+# Command summary
+
+  * [`options`↴](#options)
+  * [`options alpha`↴](#options-alpha)
+  * [`options beta`↴](#options-beta)
+
+# options
+
+Options
+
+**Usage**: **`options`** _`COMMAND ...`_
+
+**Available options:**
+- **`-h`**, **`--help`** &mdash; \n  Prints help information
+
+
+
+**Available commands:**
+- **`alpha`** &mdash; \n  Alpha
+- **`beta`** &mdash; \n  Beta
+
+
+# options alpha
+
+Alpha
+
+**Usage**: **`options`** **`alpha`** \n
+**Available options:**
+- **`-h`**, **`--help`** &mdash; \n  Prints help information
+
+
+# options beta
+
+Beta
+
+More Beta
+
+Even More Beta
+
+**Usage**: **`options`** **`beta`** \n
+**Available options:**\n- **`-h`**, **`--help`** &mdash; \n  Prints help information
+
+
+";
+    assert_eq!(r, expected);
+}
+
+#[test]
+fn multi_line_help() {
+    let opts = short('a').help("help\n\nmore help").switch().to_options();
+    let r = opts.render_markdown("ml");
+
+    let expected = "\
+# ml
+
+**Usage**: **`ml`** \\[**`-a`**\\]
+
+**Available options:**
+- **`-a`** &mdash; \n  help
+
+  more help
+- **`-h`**, **`--help`** &mdash; \n  Prints help information
+
+
+";
+    assert_eq!(r, expected);
+}
+
+#[test]
+fn no_help() {
+    let a = short('a').help("help").switch();
+    let b = short('b').switch();
+    let opts = construct!(a, b).to_options();
+
+    let r = opts.render_markdown("ml");
+
+    let expected = "\
+# ml
+
+**Usage**: **`ml`** \\[**`-a`**\\] \\[**`-b`**\\]
+
+**Available options:**
+- **`-a`** &mdash; \n  help
+- **`-b`**\n- **`-h`**, **`--help`** &mdash; \n  Prints help information\n\n\n";
+
+    assert_eq!(r, expected);
+}
+
+#[test]
+fn codeblock_space_help() {
+    #[derive(Bpaf, Clone, Debug)]
+    #[bpaf(options)]
+    struct Options {
+        /// Verbose help
+        ///
+        ///     block
+        ///     of
+        ///     code
+        verbose: bool,
+    }
+
+    let r = options().run_inner(&["-hh"]).unwrap_err().unwrap_stdout();
+    let expected = "\
+Usage: [--verbose]
+
+Available options:
+        --verbose  Verbose help
+                   block
+                   of
+                   code
+    -h, --help     Prints help information
+";
+
+    assert_eq!(r, expected);
+
+    let r = options().render_markdown("ml");
+    let expected = "\
+# ml
+
+**Usage**: **`ml`** \\[**`--verbose`**\\]
+
+**Available options:**
+- **`    --verbose`** &mdash; \n  Verbose help
+
+  \n
+  ```text
+  block
+  of
+  code
+  ```
+
+- **`-h`**, **`--help`** &mdash; \n  Prints help information\n\n\n";
+
+    assert_eq!(r, expected);
+}
+
+#[test]
+fn codeblock_ticks_help() {
+    #[derive(Bpaf, Clone, Debug)]
+    #[bpaf(options)]
+    struct Options {
+        /// Verbose help
+        ///
+        /// ```text
+        /// block
+        ///     of
+        ///     code
+        /// ```
+        verbose: bool,
+    }
+
+    let r = options().run_inner(&["-hh"]).unwrap_err().unwrap_stdout();
+    let expected = "\
+Usage: [--verbose]
+
+Available options:
+        --verbose  Verbose help
+                   ```text
+                   block
+                       of
+                       code
+                   ```
+    -h, --help     Prints help information
+";
+    assert_eq!(r, expected);
+
+    let r = options().render_markdown("ml");
+    let expected = "\
+# ml
+
+**Usage**: **`ml`** \\[**`--verbose`**\\]
+
+**Available options:**
+- **`    --verbose`** &mdash; \n  Verbose help
+
+  \n  ```text
+  block
+      of
+      code
+  ```
+
+- **`-h`**, **`--help`** &mdash; \n  Prints help information\n\n\n";
+
+    assert_eq!(r, expected);
+}
diff --git a/tests/nested.1 b/tests/nested.1
index 2e78e80..26965a2 100644
--- a/tests/nested.1
+++ b/tests/nested.1
@@ -46,11 +46,11 @@
 \fRBeware `\-d`, dragons be here 3\fP
 .SH SIMPLE\ CMD\ 
 .SH NAME
-\fRsimple \- \fP\fRI am a program and I do things 3\fP
+\fRsimple \- \fP\fRI am a program and I do things\fP
 .SH SYNOPSIS
 \fBsimple\fP\fR \fP\fBcmd\fP\fR \fP\fB\-d\fP\fR=\fP\fI<y>\fP
 .PP
-\fRSometimes they even work. 3\fP
+\fRSometimes they even work. 1\fP
 .PP
 .SS AVAILABLE\ OPTIONS:
 .TP
@@ -62,14 +62,14 @@
 \fRPrints help information\fP
 .PP
 .PP
-\fRBeware `\-d`, dragons be here 3\fP
+\fRBeware `\-d`, dragons be here 1\fP
 .SH SIMPLE\ DMC\ 
 .SH NAME
-\fRsimple \- \fP\fRI am a program and I do things 3\fP
+\fRsimple \- \fP\fRI am a program and I do things 2\fP
 .SH SYNOPSIS
 \fBsimple\fP\fR \fP\fBdmc\fP\fR \fP\fB\-k\fP\fR=\fP\fI<x>\fP
 .PP
-\fRSometimes they even work. 3\fP
+\fRSometimes they even work. 2\fP
 .PP
 .SS AVAILABLE\ OPTIONS:
 .TP
@@ -81,4 +81,4 @@
 \fRPrints help information\fP
 .PP
 .PP
-\fRBeware `\-d`, dragons be here 3\fP
\ No newline at end of file
+\fRBeware `\-d`, dragons be here 2\fP
\ No newline at end of file
diff --git a/tests/simple.1 b/tests/simple.1
index afc6a71..f92efb0 100644
--- a/tests/simple.1
+++ b/tests/simple.1
@@ -4,7 +4,7 @@
 .SH NAME
 \fRsimple \- \fP\fRI am a program and I do things\fP
 .SH SYNOPSIS
-\fBsimple\fP\fR [\fP\fB\-d\fP\fR] \fP\fB\-\-user\fP\fR=\fP\fIUSER\fP
+\fBsimple\fP\fR \fP\fR[\fP\fB\-d\fP\fR] \fP\fB\-\-user\fP\fR=\fP\fIUSER\fP
 .PP
 \fRSometimes they even work.\fP
 .PP
diff --git a/tests/very_nested.1 b/tests/very_nested.1
index af079cf..2df9ae0 100644
--- a/tests/very_nested.1
+++ b/tests/very_nested.1
@@ -27,6 +27,8 @@
 \fRlvl 1 description\fP
 .PP
 .SH SIMPLE\ LVL1\ 
+.SH NAME
+\fRsimple \- \fP\fRlvl 1 description\fP
 .SH SYNOPSIS
 \fBsimple\fP\fR \fP\fBlvl1\fP\fR \fP\fICOMMAND ...\fP
 .PP
@@ -42,6 +44,8 @@
 \fRlvl 2 description\fP
 .PP
 .SH SIMPLE\ LVL1\ LVL2\ 
+.SH NAME
+\fRsimple \- \fP\fRlvl 2 description\fP
 .SH SYNOPSIS
 \fBsimple\fP\fR \fP\fBlvl1\fP\fR \fP\fBlvl2\fP\fR \fP\fICOMMAND ...\fP
 .PP
@@ -57,6 +61,8 @@
 \fRlvl 3 description\fP
 .PP
 .SH SIMPLE\ LVL1\ LVL2\ LVL3\ 
+.SH NAME
+\fRsimple \- \fP\fRlvl 3 description\fP
 .SH SYNOPSIS
 \fBsimple\fP\fR \fP\fBlvl1\fP\fR \fP\fBlvl2\fP\fR \fP\fBlvl3\fP\fR \fP\fICOMMAND ...\fP
 .PP
@@ -72,6 +78,8 @@
 \fRlvl 4 description\fP
 .PP
 .SH SIMPLE\ LVL1\ LVL2\ LVL3\ LVL4\ 
+.SH NAME
+\fRsimple \- \fP\fRlvl 4 description\fP
 .SH SYNOPSIS
 \fBsimple\fP\fR \fP\fBlvl1\fP\fR \fP\fBlvl2\fP\fR \fP\fBlvl3\fP\fR \fP\fBlvl4\fP\fR \fP\fB\-k\fP\fR=\fP\fINAME\fP
 .PP