md.gno
8.35 Kb ยท 290 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// InlineImageWithLink creates an inline image wrapped in a hyperlink for markdown.
187// Example: InlineImageWithLink("alt text", "image-url", "link-url") => "[](link-url)"
188func InlineImageWithLink(altText, imageUrl, linkUrl string) string {
189 return "[" + Image(altText, imageUrl) + "](" + linkUrl + ")"
190}
191
192// Image returns an image for markdown.
193// Example: Image("foo", "http://example.com") => ""
194func Image(altText, url string) string {
195 return ""
196}
197
198// Footnote returns a footnote for markdown.
199// Example: Footnote("foo", "bar") => "[foo]: bar"
200func Footnote(reference, text string) string {
201 return "[" + EscapeText(reference) + "]: " + text
202}
203
204// Paragraph wraps the given text in a Markdown paragraph.
205// Example: Paragraph("foo") => "foo\n"
206func Paragraph(content string) string {
207 return content + "\n\n"
208}
209
210// CollapsibleSection creates a collapsible section for markdown using
211// HTML <details> and <summary> tags.
212// Example:
213// CollapsibleSection("Click to expand", "Hidden content")
214// =>
215// <details><summary>Click to expand</summary>
216//
217// Hidden content
218// </details>
219func CollapsibleSection(title, content string) string {
220 return "<details><summary>" + EscapeText(title) + "</summary>\n\n" + content + "\n</details>\n"
221}
222
223// EscapeText escapes special Markdown characters in regular text where needed.
224func EscapeText(text string) string {
225 replacer := strings.NewReplacer(
226 `*`, `\*`,
227 `_`, `\_`,
228 `[`, `\[`,
229 `]`, `\]`,
230 `(`, `\(`,
231 `)`, `\)`,
232 `~`, `\~`,
233 `>`, `\>`,
234 `|`, `\|`,
235 `-`, `\-`,
236 `+`, `\+`,
237 ".", `\.`,
238 "!", `\!`,
239 "`", "\\`",
240 )
241 return replacer.Replace(text)
242}
243
244// Columns returns a formatted row of columns using the Gno syntax.
245// If you want a specific number of columns per row (<=4), use ColumnsN.
246// Check /r/docs/markdown#columns for more info.
247func Columns(contentByColumn []string) string {
248 var sb strings.Builder
249 sb.WriteString("<gno-columns>\n")
250
251 for i, column := range contentByColumn {
252 if i > 0 {
253 sb.WriteString("|||\n")
254 }
255 sb.WriteString(column + "\n")
256 }
257
258 sb.WriteString("</gno-columns>\n")
259 return sb.String()
260}
261
262// ColumnsN splits content into multiple rows of N columns each and formats them.
263// If colsPerRow <= 0, all items are placed in one <gno-columns> block.
264// If padded=true & the final <gno-columns> tag is missing column content, an empty
265// column element will be placed to keep the cols per row constant.
266// Padding works only with colsPerRow > 0.
267// Note: On standard-size screens, gnoweb handles a max of 4 cols per row.
268func ColumnsN(content []string, colsPerRow int, padded bool) string {
269 if len(content) == 0 || colsPerRow <= 0 {
270 return Columns(content)
271 }
272
273 var sb strings.Builder
274 // Case 2: Multiple blocks with max 4 columns
275 for i := 0; i < len(content); i += colsPerRow {
276 end := i + colsPerRow
277 if end > len(content) {
278 end = len(content)
279 }
280 row := content[i:end]
281
282 // Add padding if needed
283 if padded && len(row) < colsPerRow {
284 row = append(row, make([]string, colsPerRow-len(row))...)
285 }
286
287 sb.WriteString(Columns(row))
288 }
289 return sb.String()
290}