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