md.gno
9.24 Kb ยท 318 lines
1// Package md provides helper functions for generating Markdown content programmatically.
2//
3// It includes utilities for text formatting, creating lists, blockquotes, code blocks,
4// links, images, and more.
5//
6// Highlights:
7// - Supports basic Markdown syntax such as bold, italic, strikethrough, headers, and lists.
8// - Manages multiline support in lists (e.g., bullet, ordered, and todo lists).
9// - Includes advanced helpers like inline images with links and nested list prefixes.
10package md
11
12import (
13 "strconv"
14 "strings"
15)
16
17// Bold returns bold text for markdown.
18// Example: Bold("foo") => "**foo**"
19func Bold(text string) string {
20 return "**" + text + "**"
21}
22
23// Italic returns italicized text for markdown.
24// Example: Italic("foo") => "*foo*"
25func Italic(text string) string {
26 return "*" + text + "*"
27}
28
29// Strikethrough returns strikethrough text for markdown.
30// Example: Strikethrough("foo") => "~~foo~~"
31func Strikethrough(text string) string {
32 return "~~" + text + "~~"
33}
34
35// H1 returns a level 1 header for markdown.
36// Example: H1("foo") => "# foo\n"
37func H1(text string) string {
38 return "# " + text + "\n"
39}
40
41// H2 returns a level 2 header for markdown.
42// Example: H2("foo") => "## foo\n"
43func H2(text string) string {
44 return "## " + text + "\n"
45}
46
47// H3 returns a level 3 header for markdown.
48// Example: H3("foo") => "### foo\n"
49func H3(text string) string {
50 return "### " + text + "\n"
51}
52
53// H4 returns a level 4 header for markdown.
54// Example: H4("foo") => "#### foo\n"
55func H4(text string) string {
56 return "#### " + text + "\n"
57}
58
59// H5 returns a level 5 header for markdown.
60// Example: H5("foo") => "##### foo\n"
61func H5(text string) string {
62 return "##### " + text + "\n"
63}
64
65// H6 returns a level 6 header for markdown.
66// Example: H6("foo") => "###### foo\n"
67func H6(text string) string {
68 return "###### " + text + "\n"
69}
70
71// BulletList returns a bullet list for markdown.
72// Example: BulletList([]string{"foo", "bar"}) => "- foo\n- bar\n"
73func BulletList(items []string) string {
74 var sb strings.Builder
75 for _, item := range items {
76 sb.WriteString(BulletItem(item))
77 }
78 return sb.String()
79}
80
81// BulletItem returns a bullet item for markdown.
82// Example: BulletItem("foo") => "- foo\n"
83func BulletItem(item string) string {
84 var sb strings.Builder
85 lines := strings.Split(item, "\n")
86 sb.WriteString("- " + lines[0] + "\n")
87 for _, line := range lines[1:] {
88 sb.WriteString(" " + line + "\n")
89 }
90 return sb.String()
91}
92
93// OrderedList returns an ordered list for markdown.
94// Example: OrderedList([]string{"foo", "bar"}) => "1. foo\n2. bar\n"
95func OrderedList(items []string) string {
96 var sb strings.Builder
97 for i, item := range items {
98 lines := strings.Split(item, "\n")
99 sb.WriteString(strconv.Itoa(i+1) + ". " + lines[0] + "\n")
100 for _, line := range lines[1:] {
101 sb.WriteString(" " + line + "\n")
102 }
103 }
104 return sb.String()
105}
106
107// TodoList returns a list of todo items with checkboxes for markdown.
108// Example: TodoList([]string{"foo", "bar\nmore bar"}, []bool{true, false}) => "- [x] foo\n- [ ] bar\n more bar\n"
109func TodoList(items []string, done []bool) string {
110 var sb strings.Builder
111 for i, item := range items {
112 sb.WriteString(TodoItem(item, done[i]))
113 }
114 return sb.String()
115}
116
117// TodoItem returns a todo item with checkbox for markdown.
118// Example: TodoItem("foo", true) => "- [x] foo\n"
119func TodoItem(item string, done bool) string {
120 var sb strings.Builder
121 checkbox := " "
122 if done {
123 checkbox = "x"
124 }
125 lines := strings.Split(item, "\n")
126 sb.WriteString("- [" + checkbox + "] " + lines[0] + "\n")
127 for _, line := range lines[1:] {
128 sb.WriteString(" " + line + "\n")
129 }
130 return sb.String()
131}
132
133// Nested prefixes each line with a given prefix, enabling nested lists.
134// Example: Nested("- foo\n- bar", " ") => " - foo\n - bar\n"
135func Nested(content, prefix string) string {
136 lines := strings.Split(content, "\n")
137 for i := range lines {
138 if strings.TrimSpace(lines[i]) != "" {
139 lines[i] = prefix + lines[i]
140 }
141 }
142 return strings.Join(lines, "\n")
143}
144
145// Blockquote returns a blockquote for markdown.
146// Example: Blockquote("foo\nbar") => "> foo\n> bar\n"
147func Blockquote(text string) string {
148 lines := strings.Split(text, "\n")
149 var sb strings.Builder
150 for _, line := range lines {
151 sb.WriteString("> " + line + "\n")
152 }
153 return sb.String()
154}
155
156// InlineCode returns inline code for markdown.
157// Example: InlineCode("foo") => "`foo`"
158func InlineCode(code string) string {
159 return "`" + strings.ReplaceAll(code, "`", "\\`") + "`"
160}
161
162// CodeBlock creates a markdown code block.
163// Example: CodeBlock("foo") => "```\nfoo\n```"
164func CodeBlock(content string) string {
165 return "```\n" + strings.ReplaceAll(content, "```", "\\```") + "\n```"
166}
167
168// LanguageCodeBlock creates a markdown code block with language-specific syntax highlighting.
169// Example: LanguageCodeBlock("go", "foo") => "```go\nfoo\n```"
170func LanguageCodeBlock(language, content string) string {
171 return "```" + language + "\n" + strings.ReplaceAll(content, "```", "\\```") + "\n```"
172}
173
174// HorizontalRule returns a horizontal rule for markdown.
175// Example: HorizontalRule() => "---\n"
176func HorizontalRule() string {
177 return "---\n"
178}
179
180// Link returns a hyperlink for markdown.
181// Example: Link("foo", "http://example.com") => "[foo](http://example.com)"
182func Link(text, url string) string {
183 return "[" + EscapeText(text) + "](" + url + ")"
184}
185
186// UserLink returns a user profile link for markdown.
187// For usernames, it adds @ prefix to the display text.
188// Example: UserLink("moul") => "[@moul](/u/moul)"
189// Example: UserLink("g1blah") => "[g1blah](/u/g1blah)"
190func UserLink(user string) string {
191 if strings.HasPrefix(user, "g1") {
192 return "[" + EscapeText(user) + "](/u/" + user + ")"
193 }
194 return "[@" + EscapeText(user) + "](/u/" + user + ")"
195}
196
197// InlineImageWithLink creates an inline image wrapped in a hyperlink for markdown.
198// Example: InlineImageWithLink("alt text", "image-url", "link-url") => "[](link-url)"
199func InlineImageWithLink(altText, imageUrl, linkUrl string) string {
200 return "[" + Image(altText, imageUrl) + "](" + linkUrl + ")"
201}
202
203// Image returns an image for markdown.
204// Example: Image("foo", "http://example.com") => ""
205func Image(altText, url string) string {
206 return ""
207}
208
209// Footnote returns a footnote for markdown.
210// Example: Footnote("foo", "bar") => "[foo]: bar"
211func Footnote(reference, text string) string {
212 return "[" + EscapeText(reference) + "]: " + text
213}
214
215// Paragraph wraps the given text in a Markdown paragraph.
216// Example: Paragraph("foo") => "foo\n"
217func Paragraph(content string) string {
218 return content + "\n\n"
219}
220
221// CollapsibleSection creates a collapsible section for markdown using
222// HTML <details> and <summary> tags.
223// Example:
224// CollapsibleSection("Click to expand", "Hidden content")
225// =>
226// <details><summary>Click to expand</summary>
227//
228// Hidden content
229// </details>
230func CollapsibleSection(title, content string) string {
231 return "<details><summary>" + EscapeText(title) + "</summary>\n\n" + content + "\n</details>\n"
232}
233
234// EscapeText escapes special Markdown characters in regular text where needed.
235func EscapeText(text string) string {
236 replacer := strings.NewReplacer(
237 `*`, `\*`,
238 `_`, `\_`,
239 `[`, `\[`,
240 `]`, `\]`,
241 `(`, `\(`,
242 `)`, `\)`,
243 `~`, `\~`,
244 `>`, `\>`,
245 `|`, `\|`,
246 `-`, `\-`,
247 `+`, `\+`,
248 ".", `\.`,
249 "!", `\!`,
250 "`", "\\`",
251 )
252 return replacer.Replace(text)
253}
254
255// Columns returns a formatted row of columns using the Gno syntax.
256// If you want a specific number of columns per row (<=4), use ColumnsN.
257// Check /r/docs/markdown#columns for more info.
258// If padded=true & the final <gno-columns> tag is missing column content, an empty
259// column element will be placed to keep the cols per row constant.
260// Padding works only with colsPerRow > 0.
261func Columns(contentByColumn []string, padded bool) string {
262 if len(contentByColumn) == 0 {
263 return ""
264 }
265 maxCols := 4
266 if padded && len(contentByColumn)%maxCols != 0 {
267 missing := maxCols - len(contentByColumn)%maxCols
268 contentByColumn = append(contentByColumn, make([]string, missing)...)
269 }
270
271 var sb strings.Builder
272 sb.WriteString("<gno-columns>\n")
273
274 for i, column := range contentByColumn {
275 if i > 0 {
276 sb.WriteString("|||\n")
277 }
278 sb.WriteString(column + "\n")
279 }
280
281 sb.WriteString("</gno-columns>\n")
282 return sb.String()
283}
284
285const maxColumnsPerRow = 4
286
287// ColumnsN splits content into multiple rows of N columns each and formats them.
288// If colsPerRow <= 0, all items are placed in one <gno-columns> block.
289// If padded=true & the final <gno-columns> tag is missing column content, an empty
290// column element will be placed to keep the cols per row constant.
291// Padding works only with colsPerRow > 0.
292// Note: On standard-size screens, gnoweb handles a max of 4 cols per row.
293func ColumnsN(content []string, colsPerRow int, padded bool) string {
294 if len(content) == 0 {
295 return ""
296 }
297 if colsPerRow <= 0 {
298 return Columns(content, padded)
299 }
300
301 var sb strings.Builder
302 // Case 2: Multiple blocks with max 4 columns
303 for i := 0; i < len(content); i += colsPerRow {
304 end := i + colsPerRow
305 if end > len(content) {
306 end = len(content)
307 }
308 row := content[i:end]
309
310 // Add padding if needed
311 if padded && len(row) < colsPerRow {
312 row = append(row, make([]string, colsPerRow-len(row))...)
313 }
314
315 sb.WriteString(Columns(row, false))
316 }
317 return sb.String()
318}