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