├── .gitignore ├── install.go ├── LICENSE ├── README.md ├── roff.go ├── parse.go ├── main.go └── mandown.1.md /.gitignore: -------------------------------------------------------------------------------- 1 | mandown -------------------------------------------------------------------------------- /install.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "compress/gzip" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | func (mp ManPage) InstallTo(dir string) error { 10 | f, err := os.Create(fmt.Sprintf("%s/%s.%d.gz", dir, mp.Name, mp.Section)) 11 | if err != nil { 12 | return err 13 | } 14 | defer f.Close() 15 | w := gzip.NewWriter(f) 16 | defer w.Close() 17 | fmt.Fprintf(w, "%s", mp.TroffString()) 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Dave MacFarlane 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mandown - a tool for writing man pages in markdown. 2 | 3 | mandown converts markdown formatted manuals into the troff(1) based format that 4 | is used by man(1) on most systems. 5 | 6 | Man pages are useful, but often neglected by modern software developers. This 7 | may be because man pages need to be written in a typesetting language seldom 8 | used by most developers (outside of man), or it may be that the software 9 | being written is written in a language that can be used on non-UNIX systems 10 | and the effort of writing extra documentation for a subset of users isn't 11 | perceived as worth the effort. 12 | 13 | Writing the documentation in markdown addresses both of these problems. Modern 14 | software developers are already likely familiar with it, and it's consumable 15 | without any special software on non-UNIX systems. 16 | 17 | By default, mandown will take any markdown file passed to it, try to parse it, 18 | normalize the sections/whitespace, and print it to the screen in a format 19 | similar to running `man `. With the -t option, it will print it as troff 20 | macros instead of human-readable format. 21 | 22 | See mandown.1.md for an example. 23 | 24 | ## Installation 25 | 26 | mandown is written in Go, and should be go-gettable 27 | 28 | ``` 29 | go get github.com/driusan/mandown 30 | ``` 31 | 32 | -------------------------------------------------------------------------------- /roff.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | var boldRe *regexp.Regexp = regexp.MustCompile(`\*(.+?)\*([ \t]*)`) 10 | var culRe *regexp.Regexp = regexp.MustCompile(`__(.+?)__([ \t]*)`) 11 | var ulRe *regexp.Regexp = regexp.MustCompile(`_(.+?)_([ \t]*)`) 12 | 13 | func (mp ManPage) TroffString() string { 14 | r := fmt.Sprintf(".TH %s %d\n", strings.ToUpper(mp.Name), mp.Section) 15 | r += fmt.Sprintf(".SH NAME\n%s \\- %s\n", mp.Name, mp.Title) 16 | 17 | for _, s := range mp.Sections { 18 | // This is probably not the most efficient way to check if a 19 | // string contains a space, but it avoids importing another 20 | // package 21 | if len(strings.Fields(s.Name)) > 1 { 22 | r += fmt.Sprintf(".SH \"%s\"\n", strings.ToUpper(s.Name)) 23 | } else { 24 | r += fmt.Sprintf(".SH %s\n", strings.ToUpper(s.Name)) 25 | } 26 | 27 | // Do a couple transformations 28 | // Paragraphs 29 | content := strings.Replace(s.Content, "\n\n", "\n.PP\n", -1) + "\n" 30 | // *Bold* 31 | content = boldRe.ReplaceAllString(content, "\n.B $1\n") 32 | // __continuous underline__ 33 | content = culRe.ReplaceAllString(content, "\n.cu\n$1\n") 34 | // _underline_ 35 | content = ulRe.ReplaceAllString(content, "\n.ul\n$1\n") 36 | r += fmt.Sprintf(content) 37 | 38 | } 39 | return r 40 | } 41 | -------------------------------------------------------------------------------- /parse.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | var titleRe *regexp.Regexp = regexp.MustCompile("^# (.+) - (.+)") 11 | var subsectionRe *regexp.Regexp = regexp.MustCompile("^## (.+)") 12 | 13 | func ReadFile(r io.Reader) (ManPage, error) { 14 | scanner := bufio.NewReader(r) 15 | 16 | var mp ManPage 17 | var line string 18 | var err error 19 | var curSection Subsection 20 | for { 21 | line, err = scanner.ReadString('\n') 22 | switch err { 23 | case io.EOF: 24 | if curSection.Name != "" { 25 | curSection.Content = strings.TrimSpace(curSection.Content) 26 | mp.Sections = append(mp.Sections, curSection) 27 | } 28 | return mp, nil 29 | case nil: 30 | // Nothing special 31 | default: 32 | if curSection.Name != "" { 33 | curSection.Content = strings.TrimSpace(curSection.Content) 34 | 35 | mp.Sections = append(mp.Sections, curSection) 36 | } 37 | return mp, err 38 | } 39 | line = strings.TrimSuffix(line, "\n") 40 | if strings.TrimSpace(line) == "" { 41 | // If there's multiple blank lines, collapse them into 42 | // one. 43 | // Blank lines at the beginning or end are collapsed 44 | // before appending the subsection with strings.TrimSpace 45 | c := curSection.Content 46 | if len(c) > 2 && c[len(c)-2:] == "\n\n" { 47 | continue 48 | } 49 | } 50 | if matches := titleRe.FindStringSubmatch(line); matches != nil { 51 | mp.Name = matches[1] 52 | mp.Title = matches[2] 53 | curSection = Subsection{} 54 | } else if matches := subsectionRe.FindStringSubmatch(line); matches != nil { 55 | if curSection.Name != "" { 56 | curSection.Content = strings.TrimSpace(curSection.Content) 57 | mp.Sections = append(mp.Sections, curSection) 58 | } 59 | curSection = Subsection{Name: matches[1]} 60 | } else { 61 | if curSection.Name != "" { 62 | curSection.Content += line + "\n" 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "regexp" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | type ManPage struct { 14 | Name, Title string 15 | Section uint8 16 | Sections []Subsection 17 | } 18 | 19 | type Subsection struct { 20 | Name string 21 | Content string 22 | } 23 | 24 | var fnameRe *regexp.Regexp = regexp.MustCompile(`(.+)\.([1-9])\.md`) 25 | 26 | func guessSection(f *os.File) uint8 { 27 | if matches := fnameRe.FindStringSubmatch(f.Name()); matches != nil { 28 | n, err := strconv.Atoi(matches[2]) 29 | if err == nil { 30 | return uint8(n) 31 | } 32 | 33 | } 34 | return 1 35 | } 36 | func (mp ManPage) String() string { 37 | r := "NAME\n\t" + mp.Name 38 | 39 | if mp.Title != "" { 40 | r += " - " + mp.Title 41 | } 42 | for _, s := range mp.Sections { 43 | indented := strings.Replace(s.Content, "\n", "\n\t", -1) 44 | 45 | // This isn't really necessary, but just for my sanity. 46 | // 47 | // Get rid of any blank lines we just created which only contain 48 | // a tab. 49 | indented = strings.Replace(indented, "\t\n", "\n", -1) 50 | r += "\n\n" + strings.ToUpper(s.Name) + "\n\t" + indented 51 | } 52 | return r 53 | } 54 | 55 | func main() { 56 | troff := flag.Bool("t", false, "Output in troff format") 57 | section := flag.Int("section", 0, "Specify section of manual") 58 | install := flag.String("install", "", "Gzip troff output and copy to dir") 59 | 60 | flag.Parse() 61 | files := flag.Args() 62 | if len(files) < 1 || (*section > 9) { 63 | flag.Usage() 64 | os.Exit(2) 65 | } 66 | 67 | for _, file := range files { 68 | f, err := os.Open(file) 69 | if err != nil { 70 | log.Println(err) 71 | continue 72 | } 73 | md, err := ReadFile(f) 74 | if err != nil { 75 | log.Println(err) 76 | } 77 | f.Close() 78 | 79 | if *section > 0 { 80 | md.Section = uint8(*section) 81 | } else { 82 | md.Section = guessSection(f) 83 | } 84 | 85 | if *install != "" { 86 | err := md.InstallTo(*install) 87 | if err != nil { 88 | log.Println(err) 89 | } 90 | } else if *troff { 91 | fmt.Println(md.TroffString()) 92 | } else { 93 | fmt.Println(md.String()) 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /mandown.1.md: -------------------------------------------------------------------------------- 1 | # mandown - a tool for writing man pages in markdown. 2 | 3 | ## Synopsis 4 | 5 | mandown [options] filename [filenames..] 6 | 7 | ## Description 8 | 9 | mandown converts markdown formatted manuals into the troff(1) based format that 10 | is used by man(1) on most systems. 11 | 12 | Man pages are useful, but often neglected by modern software developers. This 13 | may be because man pages need to be written in a typesetting language seldom 14 | used by most developers (outside of man), or it may be that the software 15 | being written is written in a language that can be used on non-UNIX systems 16 | and the effort of writing extra documentation for a subset of users isn't 17 | perceived as worth the effort. 18 | 19 | Writing the documentation in markdown addresses both of these problems. Modern 20 | software developers are already likely familiar with it, and it's consumable 21 | without any special software on non-UNIX systems. 22 | 23 | By default, mandown will take any markdown file passed to it, try to parse it, 24 | normalize the sections/whitespace, and print it to the screen in a format 25 | similar to running `man `. With the -t option, it will print it as troff 26 | macros instead of human-readable format. 27 | 28 | mandown will attempt to guess the section of the manual page as best as it can 29 | from context. Files named foo.n.md are assumed to be intended to be 30 | documentation for foo(n). Otherwise, mandown will assume you're documenting a 31 | command and assume section 1. 32 | 33 | You need to take a little care when writing your markdown. Your file should 34 | start with a header of the form "# command - title" and each section of the 35 | manpage should start with "## sectionname". Other than that, mandown doesn't 36 | pay much attention to the content of the file and passes most things along 37 | literally. 38 | 39 | ## Options 40 | 41 | -t 42 | Output the file in troff format, instead of plaintext 43 | 44 | --section=n 45 | specify the section that the manual being parsed belongs to. If unspecified, 46 | mandown will try and guess based on the filename as described above. 47 | 48 | --install=dir 49 | Copy the gzipped output to `dir` instead of printing it to the screen (implies -t) 50 | 51 | ## Bugs 52 | 53 | This is mostly written as a hack and proof of concept. It probably has many. 54 | 55 | Please file any specific issues that you encounter at 56 | https://github.com/driusan/mandown 57 | 58 | ## Author 59 | Dave MacFarlane 60 | 61 | ## See Also 62 | 63 | man(1), groff(1), nroff(1), troff(1) 64 | --------------------------------------------------------------------------------