pax_global_header00006660000000000000000000000064150025471740014517gustar00rootroot0000000000000052 comment=061b6c7cbecd6752049221aa15b7a05160796698 cpuguy83-go-md2man-061b6c7/000077500000000000000000000000001500254717400153175ustar00rootroot00000000000000cpuguy83-go-md2man-061b6c7/.github/000077500000000000000000000000001500254717400166575ustar00rootroot00000000000000cpuguy83-go-md2man-061b6c7/.github/dependabot.yml000066400000000000000000000002631500254717400215100ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: gomod directory: / schedule: interval: weekly - package-ecosystem: github-actions directory: / schedule: interval: weekly cpuguy83-go-md2man-061b6c7/.github/workflows/000077500000000000000000000000001500254717400207145ustar00rootroot00000000000000cpuguy83-go-md2man-061b6c7/.github/workflows/test.yml000066400000000000000000000013361500254717400224210ustar00rootroot00000000000000name: CI on: push: branches: [master] pull_request: branches: [master] jobs: test: name: Build strategy: matrix: go-version: [1.12.x, 1.23.x, 1.24.x] platform: [ubuntu-24.04] runs-on: ${{ matrix.platform }} steps: - name: Install Go uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} - name: Check out code into the Go module directory uses: actions/checkout@v4 - name: Build run: make build - name: Test run: make test lint: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - uses: golangci/golangci-lint-action@v7 with: version: v2.1 cpuguy83-go-md2man-061b6c7/.gitignore000066400000000000000000000000161500254717400173040ustar00rootroot00000000000000go-md2man bin cpuguy83-go-md2man-061b6c7/.golangci.yml000066400000000000000000000005671500254717400177130ustar00rootroot00000000000000# For documentation, see https://golangci-lint.run/usage/configuration/ version: "2" formatters: enable: - gofumpt linters: enable: - errorlint - nolintlint - unconvert - unparam settings: staticcheck: checks: - all - -QF1008 # https://staticcheck.dev/docs/checks/#QF1008 Omit embedded fields from selector expression. cpuguy83-go-md2man-061b6c7/Dockerfile000066400000000000000000000007051500254717400173130ustar00rootroot00000000000000ARG GO_VERSION=1.21 FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION} AS build COPY . /go/src/github.com/cpuguy83/go-md2man WORKDIR /go/src/github.com/cpuguy83/go-md2man ARG TARGETOS TARGETARCH TARGETVARIANT RUN \ --mount=type=cache,target=/go/pkg/mod \ --mount=type=cache,target=/root/.cache/go-build \ make build FROM scratch COPY --from=build /go/src/github.com/cpuguy83/go-md2man/bin/go-md2man /go-md2man ENTRYPOINT ["/go-md2man"] cpuguy83-go-md2man-061b6c7/LICENSE.md000066400000000000000000000020651500254717400167260ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2014 Brian Goff Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. cpuguy83-go-md2man-061b6c7/Makefile000066400000000000000000000012211500254717400167530ustar00rootroot00000000000000GO111MODULE ?= on export GO111MODULE GOOS ?= $(if $(TARGETOS),$(TARGETOS),) GOARCH ?= $(if $(TARGETARCH),$(TARGETARCH),) ifeq ($(TARGETARCH),amd64) GOAMD64 ?= $(TARGETVARIANT) endif ifeq ($(TARGETARCH),arm) GOARM ?= $(TARGETVARIANT:v%=%) endif ifneq ($(GOOS),) export GOOS endif ifneq ($(GOARCH),) export GOARCH endif ifneq ($(GOAMD64),) export GOAMD64 endif ifneq ($(GOARM),) export GOARM endif .PHONY: build: bin/go-md2man .PHONY: clean clean: @rm -rf bin/* .PHONY: test test: @go test $(TEST_FLAGS) ./... bin/go-md2man: go.mod go.sum md2man/* *.go @mkdir -p bin CGO_ENABLED=0 go build $(BUILD_FLAGS) -o $@ .PHONY: mod mod: @go mod tidy cpuguy83-go-md2man-061b6c7/README.md000066400000000000000000000010501500254717400165720ustar00rootroot00000000000000go-md2man ========= Converts markdown into roff (man pages). Uses [blackfriday](https://github.com/russross/blackfriday) to process markdown into man pages. ### Usage ```bash go install github.com/cpuguy83/go-md2man/v2@latest go-md2man -in /path/to/markdownfile.md -out /manfile/output/path ``` For go 1.24 and above, you can run it with `go tool`: ```bash go get -tool github.com/cpuguy83/go-md2man/v2@latest # it will be appended to `tool` directive in go.mod file go tool go-md2man -in /path/to/markdownfile.md -out /manfile/output/path ``` cpuguy83-go-md2man-061b6c7/go-md2man.1.md000066400000000000000000000017031500254717400175620ustar00rootroot00000000000000go-md2man 1 "January 2015" go-md2man "User Manual" ================================================== # NAME go-md2man - Convert markdown files into manpages # SYNOPSIS **go-md2man** [**-in**=*/path/to/md/file*] [**-out**=*/path/to/output*] # DESCRIPTION **go-md2man** converts standard markdown formatted documents into manpages. It is written purely in Go so as to reduce dependencies on 3rd party libs. By default, the input is stdin and the output is stdout. # OPTIONS **-in=**_file_ : Path to markdown file to be processed. Defaults to stdin. **-out=**_file_ : Path to output processed file. Defaults to stdout. # EXAMPLES Convert the markdown file *go-md2man.1.md* into a manpage: ``` go-md2man < go-md2man.1.md > go-md2man.1 ``` Same, but using command line arguments instead of shell redirection: ``` go-md2man -in=go-md2man.1.md -out=go-md2man.1 ``` # HISTORY January 2015, Originally compiled by Brian Goff (cpuguy83@gmail.com). cpuguy83-go-md2man-061b6c7/go.mod000066400000000000000000000001441500254717400164240ustar00rootroot00000000000000module github.com/cpuguy83/go-md2man/v2 go 1.12 require github.com/russross/blackfriday/v2 v2.1.0 cpuguy83-go-md2man-061b6c7/go.sum000066400000000000000000000002731500254717400164540ustar00rootroot00000000000000github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= cpuguy83-go-md2man-061b6c7/md2man.go000066400000000000000000000016061500254717400170270ustar00rootroot00000000000000package main import ( "flag" "fmt" "io/ioutil" "os" "github.com/cpuguy83/go-md2man/v2/md2man" ) var ( inFilePath = flag.String("in", "", "Path to file to be processed (default: stdin)") outFilePath = flag.String("out", "", "Path to output processed file (default: stdout)") ) func main() { var err error flag.Parse() inFile := os.Stdin if *inFilePath != "" { inFile, err = os.Open(*inFilePath) if err != nil { fmt.Println(err) os.Exit(1) } } defer inFile.Close() //nolint:errcheck doc, err := ioutil.ReadAll(inFile) if err != nil { fmt.Println(err) os.Exit(1) } out := md2man.Render(doc) outFile := os.Stdout if *outFilePath != "" { outFile, err = os.Create(*outFilePath) if err != nil { fmt.Println(err) os.Exit(1) } defer outFile.Close() //nolint:errcheck } _, err = outFile.Write(out) if err != nil { fmt.Println(err) os.Exit(1) } } cpuguy83-go-md2man-061b6c7/md2man/000077500000000000000000000000001500254717400164755ustar00rootroot00000000000000cpuguy83-go-md2man-061b6c7/md2man/debug.go000066400000000000000000000026411500254717400201150ustar00rootroot00000000000000package md2man import ( "fmt" "io" "os" "strings" "github.com/russross/blackfriday/v2" ) func fmtListFlags(flags blackfriday.ListType) string { knownFlags := []struct { name string flag blackfriday.ListType }{ {"ListTypeOrdered", blackfriday.ListTypeOrdered}, {"ListTypeDefinition", blackfriday.ListTypeDefinition}, {"ListTypeTerm", blackfriday.ListTypeTerm}, {"ListItemContainsBlock", blackfriday.ListItemContainsBlock}, {"ListItemBeginningOfList", blackfriday.ListItemBeginningOfList}, {"ListItemEndOfList", blackfriday.ListItemEndOfList}, } var f []string for _, kf := range knownFlags { if flags&kf.flag != 0 { f = append(f, kf.name) flags &^= kf.flag } } if flags != 0 { f = append(f, fmt.Sprintf("Unknown(%#x)", flags)) } return strings.Join(f, "|") } type debugDecorator struct { blackfriday.Renderer } func depth(node *blackfriday.Node) int { d := 0 for n := node.Parent; n != nil; n = n.Parent { d++ } return d } func (d *debugDecorator) RenderNode(w io.Writer, node *blackfriday.Node, entering bool) blackfriday.WalkStatus { fmt.Fprintf(os.Stderr, "%s%s %v %v\n", strings.Repeat(" ", depth(node)), map[bool]string{true: "+", false: "-"}[entering], node, fmtListFlags(node.ListFlags)) var b strings.Builder status := d.Renderer.RenderNode(io.MultiWriter(&b, w), node, entering) if b.Len() > 0 { fmt.Fprintf(os.Stderr, ">> %q\n", b.String()) } return status } cpuguy83-go-md2man-061b6c7/md2man/md2man.go000066400000000000000000000011041500254717400201760ustar00rootroot00000000000000// Package md2man aims in converting markdown into roff (man pages). package md2man import ( "os" "strconv" "github.com/russross/blackfriday/v2" ) // Render converts a markdown document into a roff formatted document. func Render(doc []byte) []byte { renderer := NewRoffRenderer() var r blackfriday.Renderer = renderer if v, _ := strconv.ParseBool(os.Getenv("MD2MAN_DEBUG")); v { r = &debugDecorator{Renderer: r} } return blackfriday.Run(doc, []blackfriday.Option{ blackfriday.WithRenderer(r), blackfriday.WithExtensions(renderer.GetExtensions()), }...) } cpuguy83-go-md2man-061b6c7/md2man/roff.go000066400000000000000000000266161500254717400177730ustar00rootroot00000000000000package md2man import ( "bufio" "bytes" "fmt" "io" "os" "strings" "github.com/russross/blackfriday/v2" ) // roffRenderer implements the blackfriday.Renderer interface for creating // roff format (manpages) from markdown text type roffRenderer struct { listCounters []int firstHeader bool listDepth int } const ( titleHeader = ".TH " topLevelHeader = "\n\n.SH " secondLevelHdr = "\n.SH " otherHeader = "\n.SS " crTag = "\n" emphTag = "\\fI" emphCloseTag = "\\fP" strongTag = "\\fB" strongCloseTag = "\\fP" breakTag = "\n.br\n" paraTag = "\n.PP\n" hruleTag = "\n.ti 0\n\\l'\\n(.lu'\n" linkTag = "\n\\[la]" linkCloseTag = "\\[ra]" codespanTag = "\\fB" codespanCloseTag = "\\fR" codeTag = "\n.EX\n" codeCloseTag = ".EE\n" // Do not prepend a newline character since code blocks, by definition, include a newline already (or at least as how blackfriday gives us on). quoteTag = "\n.PP\n.RS\n" quoteCloseTag = "\n.RE\n" listTag = "\n.RS\n" listCloseTag = ".RE\n" dtTag = "\n.TP\n" dd2Tag = "\n" tableStart = "\n.TS\nallbox;\n" tableEnd = ".TE\n" tableCellStart = "T{\n" tableCellEnd = "\nT}" tablePreprocessor = `'\" t` ) // NewRoffRenderer creates a new blackfriday Renderer for generating roff documents // from markdown func NewRoffRenderer() *roffRenderer { return &roffRenderer{} } // GetExtensions returns the list of extensions used by this renderer implementation func (*roffRenderer) GetExtensions() blackfriday.Extensions { return blackfriday.NoIntraEmphasis | blackfriday.Tables | blackfriday.FencedCode | blackfriday.SpaceHeadings | blackfriday.Footnotes | blackfriday.Titleblock | blackfriday.DefinitionLists } // RenderHeader handles outputting the header at document start func (r *roffRenderer) RenderHeader(w io.Writer, ast *blackfriday.Node) { // We need to walk the tree to check if there are any tables. // If there are, we need to enable the roff table preprocessor. ast.Walk(func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus { if node.Type == blackfriday.Table { out(w, tablePreprocessor+"\n") return blackfriday.Terminate } return blackfriday.GoToNext }) // disable hyphenation out(w, ".nh\n") } // RenderFooter handles outputting the footer at the document end; the roff // renderer has no footer information func (r *roffRenderer) RenderFooter(w io.Writer, ast *blackfriday.Node) { } // RenderNode is called for each node in a markdown document; based on the node // type the equivalent roff output is sent to the writer func (r *roffRenderer) RenderNode(w io.Writer, node *blackfriday.Node, entering bool) blackfriday.WalkStatus { walkAction := blackfriday.GoToNext switch node.Type { case blackfriday.Text: // Special case: format the NAME section as required for proper whatis parsing. // Refer to the lexgrog(1) and groff_man(7) manual pages for details. if node.Parent != nil && node.Parent.Type == blackfriday.Paragraph && node.Parent.Prev != nil && node.Parent.Prev.Type == blackfriday.Heading && node.Parent.Prev.FirstChild != nil && bytes.EqualFold(node.Parent.Prev.FirstChild.Literal, []byte("NAME")) { before, after, found := bytesCut(node.Literal, []byte(" - ")) escapeSpecialChars(w, before) if found { out(w, ` \- `) escapeSpecialChars(w, after) } } else { escapeSpecialChars(w, node.Literal) } case blackfriday.Softbreak: out(w, crTag) case blackfriday.Hardbreak: out(w, breakTag) case blackfriday.Emph: if entering { out(w, emphTag) } else { out(w, emphCloseTag) } case blackfriday.Strong: if entering { out(w, strongTag) } else { out(w, strongCloseTag) } case blackfriday.Link: // Don't render the link text for automatic links, because this // will only duplicate the URL in the roff output. // See https://daringfireball.net/projects/markdown/syntax#autolink if !bytes.Equal(node.LinkData.Destination, node.FirstChild.Literal) { out(w, string(node.FirstChild.Literal)) } // Hyphens in a link must be escaped to avoid word-wrap in the rendered man page. escapedLink := strings.ReplaceAll(string(node.LinkData.Destination), "-", "\\-") out(w, linkTag+escapedLink+linkCloseTag) walkAction = blackfriday.SkipChildren case blackfriday.Image: // ignore images walkAction = blackfriday.SkipChildren case blackfriday.Code: out(w, codespanTag) escapeSpecialChars(w, node.Literal) out(w, codespanCloseTag) case blackfriday.Document: break case blackfriday.Paragraph: if entering { if r.listDepth > 0 { // roff .PP markers break lists if node.Prev != nil { // continued paragraph if node.Prev.Type == blackfriday.List && node.Prev.ListFlags&blackfriday.ListTypeDefinition == 0 { out(w, ".IP\n") } else { out(w, crTag) } } } else if node.Prev != nil && node.Prev.Type == blackfriday.Heading { out(w, crTag) } else { out(w, paraTag) } } else { if node.Next == nil || node.Next.Type != blackfriday.List { out(w, crTag) } } case blackfriday.BlockQuote: if entering { out(w, quoteTag) } else { out(w, quoteCloseTag) } case blackfriday.Heading: r.handleHeading(w, node, entering) case blackfriday.HorizontalRule: out(w, hruleTag) case blackfriday.List: r.handleList(w, node, entering) case blackfriday.Item: r.handleItem(w, node, entering) case blackfriday.CodeBlock: out(w, codeTag) escapeSpecialChars(w, node.Literal) out(w, codeCloseTag) case blackfriday.Table: r.handleTable(w, node, entering) case blackfriday.TableHead: case blackfriday.TableBody: case blackfriday.TableRow: // no action as cell entries do all the nroff formatting return blackfriday.GoToNext case blackfriday.TableCell: r.handleTableCell(w, node, entering) case blackfriday.HTMLSpan: // ignore other HTML tags case blackfriday.HTMLBlock: if bytes.HasPrefix(node.Literal, []byte("\n\nSecond paragraph\n", ".nh\n\n.PP\nFirst paragraph\n\n.PP\nSecond paragraph\n", } doTestsParam(t, blockTests, TestParams{}) inlineTests := []string{ "Text with a comment in the middle\n", ".nh\n\n.PP\nText with a comment in the middle\n", } doTestsInlineParam(t, inlineTests, TestParams{}) } func TestHeadings(t *testing.T) { tests := []string{ "# title\n\n# NAME\ncommand - description\n\n# SYNOPSIS\nA short description\n\nWhich spans multiple paragraphs\n", ".nh\n.TH title\n\n.SH NAME\ncommand \\- description\n\n\n.SH SYNOPSIS\nA short description\n\n.PP\nWhich spans multiple paragraphs\n", "# title\n\n# Name\nmy-command, other - description - with - hyphens\n", ".nh\n.TH title\n\n.SH Name\nmy-command, other \\- description - with - hyphens\n", "# title\n\n# Not NAME\nsome - other - text\n", ".nh\n.TH title\n\n.SH Not NAME\nsome - other - text\n", } doTestsInline(t, tests) } func execRecoverableTestSuite(t *testing.T, suite func(candidate *string)) { // Catch and report panics. This is useful when running 'go test -v' on // the integration server. When developing, though, crash dump is often // preferable, so recovery can be easily turned off with doRecover = false. var candidate string const doRecover = true if doRecover { defer func() { if err := recover(); err != nil { t.Errorf("\npanic while processing [%#v]: %s\n", candidate, err) } }() } suite(&candidate) } func runMarkdown(input string, params TestParams) string { renderer := NewRoffRenderer() return string(blackfriday.Run([]byte(input), blackfriday.WithRenderer(renderer), blackfriday.WithExtensions(params.extensions))) } func doTestsParam(t *testing.T, tests []string, params TestParams) { execRecoverableTestSuite(t, func(candidate *string) { for i := 0; i+1 < len(tests); i += 2 { input := tests[i] t.Run(input, func(t *testing.T) { *candidate = input expected := tests[i+1] actual := runMarkdown(*candidate, params) if actual != expected { t.Errorf("\nInput [%#v]\nExpected[%#v]\nActual [%#v]", *candidate, expected, actual) } // now test every substring to stress test bounds checking if !testing.Short() { for start := 0; start < len(input); start++ { for end := start + 1; end <= len(input); end++ { *candidate = input[start:end] runMarkdown(*candidate, params) } } } }) } }) } func doTestsInline(t *testing.T, tests []string) { doTestsInlineParam(t, tests, TestParams{}) } func doTestsInlineParam(t *testing.T, tests []string, params TestParams) { params.extensions |= blackfriday.Strikethrough doTestsParam(t, tests, params) }