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") => "[![alt text](image-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") => "![foo](http://example.com)"
208func Image(altText, url string) string {
209	return "![" + EscapeText(altText) + "](" + url + ")"
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}