From d6f5caf5a403c3ad1093eacf38d83bcc6ec4d9fe Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Tue, 16 Apr 2024 19:16:31 +0200 Subject: Adding upstream version 2.0.4. Signed-off-by: Daniel Baumann --- .github/dependabot.yml | 10 + .github/workflows/test.yml | 38 ++++ .gitignore | 2 + .golangci.yml | 6 + Dockerfile | 14 ++ LICENSE.md | 21 ++ Makefile | 49 +++++ README.md | 15 ++ go-md2man.1.md | 28 +++ go.mod | 5 + go.sum | 2 + md2man.go | 53 +++++ md2man/md2man.go | 16 ++ md2man/roff.go | 382 ++++++++++++++++++++++++++++++++++++ md2man/roff_test.go | 480 +++++++++++++++++++++++++++++++++++++++++++++ 15 files changed, 1121 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 Dockerfile create mode 100644 LICENSE.md create mode 100644 Makefile create mode 100644 README.md create mode 100644 go-md2man.1.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 md2man.go create mode 100644 md2man/md2man.go create mode 100644 md2man/roff.go create mode 100644 md2man/roff_test.go diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..77b7be5 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: +- package-ecosystem: gomod + directory: / + schedule: + interval: weekly +- package-ecosystem: github-actions + directory: / + schedule: + interval: weekly diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..5eddd07 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,38 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + test: + name: Build + strategy: + matrix: + go-version: [1.18.x, 1.19.x, 1.20.x, 1.21.x] + platform: [ubuntu-20.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-20.04 + steps: + - uses: actions/checkout@v4 + - uses: golangci/golangci-lint-action@v4.0.0 + with: + version: v1.55 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..30f97c3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +go-md2man +bin diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..71f073f --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,6 @@ +# For documentation, see https://golangci-lint.run/usage/configuration/ + +linters: + enable: + - gofumpt + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b9fc4df --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +ARG 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"] diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..1cade6c --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +The 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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5f4a423 --- /dev/null +++ b/Makefile @@ -0,0 +1,49 @@ +GO111MODULE ?= 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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..0e30d34 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +go-md2man +========= + +Converts markdown into roff (man pages). + +Uses blackfriday to process markdown into man pages. + +### Usage + +./md2man -in /path/to/markdownfile.md -out /manfile/output/path + +### How to contribute + +We use go modules to manage dependencies. +As such you must be using at lest go1.11. diff --git a/go-md2man.1.md b/go-md2man.1.md new file mode 100644 index 0000000..aa4587e --- /dev/null +++ b/go-md2man.1.md @@ -0,0 +1,28 @@ +go-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. + +# 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). diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0bc888d --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/cpuguy83/go-md2man/v2 + +go 1.11 + +require github.com/russross/blackfriday/v2 v2.1.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..502a072 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= diff --git a/md2man.go b/md2man.go new file mode 100644 index 0000000..4ff873b --- /dev/null +++ b/md2man.go @@ -0,0 +1,53 @@ +package 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) + } +} diff --git a/md2man/md2man.go b/md2man/md2man.go new file mode 100644 index 0000000..42bf32a --- /dev/null +++ b/md2man/md2man.go @@ -0,0 +1,16 @@ +package md2man + +import ( + "github.com/russross/blackfriday/v2" +) + +// Render converts a markdown document into a roff formatted document. +func Render(doc []byte) []byte { + renderer := NewRoffRenderer() + + return blackfriday.Run(doc, + []blackfriday.Option{ + blackfriday.WithRenderer(renderer), + blackfriday.WithExtensions(renderer.GetExtensions()), + }...) +} diff --git a/md2man/roff.go b/md2man/roff.go new file mode 100644 index 0000000..8a290f1 --- /dev/null +++ b/md2man/roff.go @@ -0,0 +1,382 @@ +package 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 { + extensions blackfriday.Extensions + listCounters []int + firstHeader bool + firstDD 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 = "\n.RE\n" + dtTag = "\n.TP\n" + dd2Tag = "\n" + tableStart = "\n.TS\nallbox;\n" + tableEnd = ".TE\n" + tableCellStart = "T{\n" + tableCellEnd = "\nT}\n" + tablePreprocessor = `'\" t` +) + +// NewRoffRenderer creates a new blackfriday Renderer for generating roff documents +// from markdown +func NewRoffRenderer() *roffRenderer { // nolint: golint + var extensions blackfriday.Extensions + + extensions |= blackfriday.NoIntraEmphasis + extensions |= blackfriday.Tables + extensions |= blackfriday.FencedCode + extensions |= blackfriday.SpaceHeadings + extensions |= blackfriday.Footnotes + extensions |= blackfriday.Titleblock + extensions |= blackfriday.DefinitionLists + return &roffRenderer{ + extensions: extensions, + } +} + +// GetExtensions returns the list of extensions used by this renderer implementation +func (r *roffRenderer) GetExtensions() blackfriday.Extensions { + return r.extensions +} + +// 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: + 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: + // roff .PP markers break lists + if r.listDepth > 0 { + return blackfriday.GoToNext + } + if entering { + out(w, paraTag) + } else { + 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 execRecoverableTestSuite(t *testing.T, tests []string, params TestParams, 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, tests, params, 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) +} -- cgit v1.2.3