Skip to content

Commit 15c77f6

Browse files
authored
Switch to goldmark and glamour for Markdown-to-package-description rendering (#170)
* Switch to goldmark/Glamour for README.md-to-description rendering * ci-test: Remove Go 1.15.x from test matrix as the use of //go:embed requires Go 1.16 and newer * Remove backtick quotes from Markdown code for long description * Refactor description.go Move the call to Markdown renderer into its own markdownToLongDescription() function, remove unneeded string() conversions, and add comments that explain the adding of hanging indents to lists in reformatForControl(). * Add unit tests for markdownToLongDescription and reformatForControl Thanks to @creekorful for the idea and encouragement.
1 parent 5627500 commit 15c77f6

File tree

8 files changed

+333
-169
lines changed

8 files changed

+333
-169
lines changed

.github/workflows/ci-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88
if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')"
99
strategy:
1010
matrix:
11-
go-version: ['1.15.x', '1.16.x', '1.17.x']
11+
go-version: ['1.16.x', '1.17.x']
1212
env:
1313
GO111MODULE: on # Needed for github.com/google/go-github/v38
1414

description.go

Lines changed: 55 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,70 @@
11
package main
22

33
import (
4-
"bytes"
54
"context"
5+
_ "embed"
66
"fmt"
7-
"os/exec"
7+
"regexp"
88
"strings"
99

10-
"github.com/russross/blackfriday"
10+
"github.com/charmbracelet/glamour"
1111
)
1212

13+
//go:embed description.json
14+
var descriptionJSONBytes []byte
15+
16+
// reformatForControl reformats the wrapped description
17+
// to conform to Debian’s control format.
1318
func reformatForControl(raw string) string {
14-
// Reformat the wrapped description to conform to Debian’s control format.
15-
for strings.Contains(raw, "\n\n") {
16-
raw = strings.Replace(raw, "\n\n", "\n.\n", -1)
19+
output := ""
20+
next_prefix := ""
21+
re := regexp.MustCompile(`^ \d+\. `)
22+
23+
for _, line := range strings.Split(strings.TrimSpace(raw), "\n") {
24+
// Remove paddings that Glamour currently add to the end of each line
25+
line = strings.TrimRight(line, " ")
26+
27+
// Try to add hanging indent for list items that span over one line
28+
prefix := next_prefix
29+
if strings.HasPrefix(line, " * ") {
30+
// unordered list
31+
prefix = ""
32+
next_prefix = " "
33+
}
34+
if re.MatchString(line) {
35+
// ordered list
36+
prefix = ""
37+
next_prefix = " "
38+
}
39+
if line == "" {
40+
// blank line, implying end of list
41+
line = "."
42+
prefix = ""
43+
next_prefix = ""
44+
}
45+
output += " " + prefix + line + "\n"
46+
}
47+
return output
48+
}
49+
50+
// markdownToLongDescription converts Markdown to plain text
51+
// and reformat it for expanded description in debian/control.
52+
func markdownToLongDescription(markdown string) (string, error) {
53+
r, _ := glamour.NewTermRenderer(
54+
glamour.WithStylesFromJSONBytes(descriptionJSONBytes),
55+
glamour.WithWordWrap(72),
56+
)
57+
out, err := r.Render(markdown)
58+
if err != nil {
59+
return "", fmt.Errorf("fail to render Markdown: %w", err)
1760
}
18-
return strings.Replace(raw, "\n", "\n ", -1)
61+
//fmt.Println(out)
62+
//fmt.Println(reformatForControl(out))
63+
return reformatForControl(out), nil
1964
}
2065

2166
// getDescriptionForGopkg reads from README.md (or equivalent) from GitHub,
22-
// intended for the extended description in debian/control.
67+
// intended for extended description in debian/control.
2368
func getLongDescriptionForGopkg(gopkg string) (string, error) {
2469
owner, repo, err := findGitHubRepo(gopkg)
2570
if err != nil {
@@ -47,151 +92,8 @@ func getLongDescriptionForGopkg(gopkg string) (string, error) {
4792
!strings.HasSuffix(rr.GetName(), "markdown") &&
4893
!strings.HasSuffix(rr.GetName(), "mdown") &&
4994
!strings.HasSuffix(rr.GetName(), "mkdn") {
50-
return reformatForControl(strings.TrimSpace(string(content))), nil
95+
return reformatForControl(content), nil
5196
}
5297

53-
output := blackfriday.Markdown([]byte(content), &TextRenderer{}, 0)
54-
// Shell out to fmt(1) to line-wrap the output.
55-
cmd := exec.Command("fmt")
56-
cmd.Stdin = bytes.NewBuffer(output)
57-
out, err := cmd.Output()
58-
if err != nil {
59-
return "", fmt.Errorf("fmt: %w", err)
60-
}
61-
return reformatForControl(strings.TrimSpace(string(out))), nil
62-
}
63-
64-
type TextRenderer struct {
65-
}
66-
67-
func (options *TextRenderer) BlockCode(out *bytes.Buffer, text []byte, lang string) {
68-
out.Write(text)
69-
}
70-
71-
func (options *TextRenderer) BlockQuote(out *bytes.Buffer, text []byte) {
72-
out.Write(text)
73-
}
74-
75-
func (options *TextRenderer) BlockHtml(out *bytes.Buffer, text []byte) {
76-
out.Write(text)
77-
}
78-
79-
func (options *TextRenderer) Header(out *bytes.Buffer, text func() bool, level int, id string) {
80-
text()
81-
}
82-
83-
func (options *TextRenderer) HRule(out *bytes.Buffer) {
84-
out.WriteString("--------------------------------------------------------------------------------\n")
85-
}
86-
87-
func (options *TextRenderer) List(out *bytes.Buffer, text func() bool, flags int) {
88-
text()
89-
}
90-
91-
func (options *TextRenderer) ListItem(out *bytes.Buffer, text []byte, flags int) {
92-
out.WriteString("• ")
93-
out.Write(text)
94-
}
95-
96-
func (options *TextRenderer) Paragraph(out *bytes.Buffer, text func() bool) {
97-
out.WriteString("\n")
98-
text()
99-
out.WriteString("\n")
100-
}
101-
102-
func (options *TextRenderer) Table(out *bytes.Buffer, header []byte, body []byte, columnData []int) {
103-
out.Write(header)
104-
out.Write(body)
105-
}
106-
107-
func (options *TextRenderer) TableRow(out *bytes.Buffer, text []byte) {
108-
out.Write(text)
109-
}
110-
111-
func (options *TextRenderer) TableHeaderCell(out *bytes.Buffer, text []byte, flags int) {
112-
out.Write(text)
113-
}
114-
115-
func (options *TextRenderer) TableCell(out *bytes.Buffer, text []byte, flags int) {
116-
out.Write(text)
117-
}
118-
119-
func (options *TextRenderer) Footnotes(out *bytes.Buffer, text func() bool) {
120-
text()
121-
}
122-
123-
func (options *TextRenderer) FootnoteItem(out *bytes.Buffer, name, text []byte, flags int) {
124-
out.WriteString("[")
125-
out.Write(name)
126-
out.WriteString("]")
127-
out.Write(text)
128-
}
129-
130-
func (options *TextRenderer) TitleBlock(out *bytes.Buffer, text []byte) {
131-
}
132-
133-
// Span-level callbacks
134-
func (options *TextRenderer) AutoLink(out *bytes.Buffer, link []byte, kind int) {
135-
out.Write(link)
136-
}
137-
138-
func (options *TextRenderer) CodeSpan(out *bytes.Buffer, text []byte) {
139-
out.Write(text)
140-
}
141-
142-
func (options *TextRenderer) DoubleEmphasis(out *bytes.Buffer, text []byte) {
143-
out.Write(text)
144-
}
145-
146-
func (options *TextRenderer) Emphasis(out *bytes.Buffer, text []byte) {
147-
out.Write(text)
148-
}
149-
150-
func (options *TextRenderer) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) {
151-
out.Write(alt)
152-
}
153-
154-
func (options *TextRenderer) LineBreak(out *bytes.Buffer) {
155-
out.WriteString("\n")
156-
}
157-
158-
func (options *TextRenderer) Link(out *bytes.Buffer, link []byte, title []byte, content []byte) {
159-
out.Write(content)
160-
out.WriteString(" (")
161-
out.Write(link)
162-
out.WriteString(")")
163-
}
164-
165-
func (options *TextRenderer) RawHtmlTag(out *bytes.Buffer, tag []byte) {
166-
}
167-
168-
func (options *TextRenderer) TripleEmphasis(out *bytes.Buffer, text []byte) {
169-
out.Write(text)
170-
}
171-
172-
func (options *TextRenderer) StrikeThrough(out *bytes.Buffer, text []byte) {
173-
out.Write(text)
174-
}
175-
176-
func (options *TextRenderer) FootnoteRef(out *bytes.Buffer, ref []byte, id int) {
177-
}
178-
179-
// Low-level callbacks
180-
func (options *TextRenderer) Entity(out *bytes.Buffer, entity []byte) {
181-
out.Write(entity)
182-
}
183-
184-
func (options *TextRenderer) NormalText(out *bytes.Buffer, text []byte) {
185-
out.Write(text)
186-
}
187-
188-
// Header and footer
189-
func (options *TextRenderer) DocumentHeader(out *bytes.Buffer) {
190-
}
191-
192-
func (options *TextRenderer) DocumentFooter(out *bytes.Buffer) {
193-
}
194-
195-
func (options *TextRenderer) GetFlags() int {
196-
return 0
98+
return markdownToLongDescription(content)
19799
}

description.json

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
{
2+
"document": {
3+
"block_prefix": "\n",
4+
"block_suffix": "\n",
5+
"margin": 0
6+
},
7+
"block_quote": {
8+
"indent": 1,
9+
"indent_token": " | "
10+
},
11+
"paragraph": {},
12+
"list": {
13+
"indent": 1,
14+
"level_indent": 4
15+
},
16+
"heading": {
17+
"block_suffix": "\n"
18+
},
19+
"h1": {
20+
"prefix": ""
21+
},
22+
"h2": {
23+
"prefix": ""
24+
},
25+
"h3": {
26+
"prefix": ""
27+
},
28+
"h4": {
29+
"prefix": ""
30+
},
31+
"h5": {
32+
"prefix": ""
33+
},
34+
"h6": {
35+
"prefix": ""
36+
},
37+
"text": {},
38+
"strikethrough": {
39+
"block_prefix": "~~",
40+
"block_suffix": "~~"
41+
},
42+
"emph": {
43+
"block_prefix": "*",
44+
"block_suffix": "*"
45+
},
46+
"strong": {
47+
"block_prefix": "**",
48+
"block_suffix": "**"
49+
},
50+
"hr": {
51+
"format": "\n------------------------------------------------------------------------\n"
52+
},
53+
"item": {
54+
"block_prefix": "* ",
55+
"indent": 2
56+
},
57+
"enumeration": {
58+
"block_prefix": ". ",
59+
"indent": 2
60+
},
61+
"task": {
62+
"ticked": "[x] ",
63+
"unticked": "[ ] "
64+
},
65+
"link": {
66+
"prefix": "(",
67+
"suffix": ")"
68+
},
69+
"link_text": {},
70+
"image": {
71+
"prefix": "(",
72+
"suffix": ")"
73+
},
74+
"image_text": {
75+
"format": "[Image: {{.text}}]"
76+
},
77+
"code": {
78+
"block_prefix": "",
79+
"block_suffix": ""
80+
},
81+
"code_block": {
82+
"margin": 2
83+
},
84+
"table": {
85+
"center_separator": "+",
86+
"column_separator": "|",
87+
"row_separator": "-"
88+
},
89+
"definition_list": {},
90+
"definition_term": {},
91+
"definition_description": {
92+
"block_prefix": "\n* "
93+
},
94+
"html_block": {},
95+
"html_span": {}
96+
}

0 commit comments

Comments
 (0)