PathrockNetwork Gno Explorer
HomeBlocksTransactionsRealmsPackagesValidatorsAnalytics

PathrockNetwork Gno Explorer — an independent explorer for Gno.land Mainnet (gnoland-1), operated by PathrockNetwork. Not an official Gno.land service.

gnowebarchive RPC

gno.land/p/moul/md/v0

Package
Open in gnoweb ↗

Overview

Kind
Pure package
Name
v0
Namespace
moul / md
Files
4 (gnomod.toml)
Exported functions
n/a — not supported for pure packages by the node (vm/qfuncs)
Module
gno.land/p/moul/md/v0
gno
0.9

Files (4)

  • gnomod.tomltoml
  • md.gnogno
  • example_test.gnogno
  • md_test.gnogno
md.gnogno
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_md13//14// # Sanitization contract15//16// Some helpers in this package sanitize their user-derived arguments17// INTERNALLY (via p/nt/markdown/sanitize/v0). When using these, pass18// raw user input — do NOT pre-wrap with sanitize.*, or you will19// double-wrap (the escapers are not idempotent and double-wrap is a20// bug):21//22//	Link, UserLink, Image, InlineImageWithLink, FootnoteDefinition,23//	LinkReferenceDefinition, CollapsibleSection (title only),24//	InlineCode, CodeBlock, LanguageCodeBlock, Blockquote25//26// The other helpers DO NOT sanitize — they are pure builders that27// wrap their input in markdown chrome. User-derived input reaches28// the output unmodified, so callers MUST wrap with sanitize.* at the29// call site:30//31//	Bold, Italic, Strikethrough, H1-H6, BulletList, BulletItem,32//	OrderedList, TodoList, TodoItem, Nested, Paragraph, Columns,33//	ColumnsN, HorizontalRule34//35// Examples:36//37//	// Sanitizing helper — pass raw:38//	out += md.Link(post.Title, post.URL)                                 // good39//	out += md.Link(sanitize.InlineText(post.Title), sanitize.URL(post.URL)) // BAD: double-wrap40//41//	// Non-sanitizing helper — wrap once:42//	out += md.H2(sanitize.InlineText(post.Title))                        // good43//	out += md.H2(post.Title)                                             // BAD: raw user input44//	out += md.H2(sanitize.InlineText(sanitize.InlineText(post.Title)))   // BAD: double-wrap45//46// Composition: outputs of sanitizing helpers are safe markdown chrome47// and can be embedded inside non-sanitizing helpers freely:48//49//	out += md.H2(md.Link(post.Title, post.URL))   // good — H2 doesn't re-escape Link's output50//51// The reverse is unsafe: do NOT embed a non-sanitizing helper's52// markdown chrome inside a sanitizing helper's arg, or the inner53// markdown gets re-escaped:54//55//	out += md.Link(md.Bold(post.Title), post.URL) // BAD: Link's internal sanitize56//	                                              // escapes the ** chars from md.Bold57package md5859import (60	"strconv"61	"strings"6263	"gno.land/p/nt/markdown/sanitize/v0"64)6566// Bold returns bold text for markdown.67// Example: Bold("foo") => "**foo**"68func Bold(text string) string {69	return "**" + text + "**"70}7172// Italic returns italicized text for markdown.73// Example: Italic("foo") => "*foo*"74func Italic(text string) string {75	return "*" + text + "*"76}7778// Strikethrough returns strikethrough text for markdown.79// Example: Strikethrough("foo") => "~~foo~~"80func Strikethrough(text string) string {81	return "~~" + text + "~~"82}8384// H1 returns a level 1 header for markdown.85// Example: H1("foo") => "# foo\n"86func H1(text string) string {87	return "# " + text + "\n"88}8990// H2 returns a level 2 header for markdown.91// Example: H2("foo") => "## foo\n"92func H2(text string) string {93	return "## " + text + "\n"94}9596// H3 returns a level 3 header for markdown.97// Example: H3("foo") => "### foo\n"98func H3(text string) string {99	return "### " + text + "\n"100}101102// H4 returns a level 4 header for markdown.103// Example: H4("foo") => "#### foo\n"104func H4(text string) string {105	return "#### " + text + "\n"106}107108// H5 returns a level 5 header for markdown.109// Example: H5("foo") => "##### foo\n"110func H5(text string) string {111	return "##### " + text + "\n"112}113114// H6 returns a level 6 header for markdown.115// Example: H6("foo") => "###### foo\n"116func H6(text string) string {117	return "###### " + text + "\n"118}119120// BulletList returns a bullet list for markdown.121// Example: BulletList([]string{"foo", "bar"}) => "- foo\n- bar\n"122func BulletList(items []string) string {123	var sb strings.Builder124	for _, item := range items {125		sb.WriteString(BulletItem(item))126	}127	return sb.String()128}129130// BulletItem returns a bullet item for markdown.131// Example: BulletItem("foo") => "- foo\n"132func BulletItem(item string) string {133	var sb strings.Builder134	lines := strings.Split(item, "\n")135	sb.WriteString("- " + lines[0] + "\n")136	for _, line := range lines[1:] {137		sb.WriteString("  " + line + "\n")138	}139	return sb.String()140}141142// OrderedList returns an ordered list for markdown.143// Example: OrderedList([]string{"foo", "bar"}) => "1. foo\n2. bar\n"144func OrderedList(items []string) string {145	var sb strings.Builder146	for i, item := range items {147		lines := strings.Split(item, "\n")148		sb.WriteString(strconv.Itoa(i+1) + ". " + lines[0] + "\n")149		for _, line := range lines[1:] {150			sb.WriteString("   " + line + "\n")151		}152	}153	return sb.String()154}155156// TodoList returns a list of todo items with checkboxes for markdown.157// Example: TodoList([]string{"foo", "bar\nmore bar"}, []bool{true, false}) => "- [x] foo\n- [ ] bar\n  more bar\n"158func TodoList(items []string, done []bool) string {159	var sb strings.Builder160	for i, item := range items {161		sb.WriteString(TodoItem(item, done[i]))162	}163	return sb.String()164}165166// TodoItem returns a todo item with checkbox for markdown.167// Example: TodoItem("foo", true) => "- [x] foo\n"168func TodoItem(item string, done bool) string {169	var sb strings.Builder170	checkbox := " "171	if done {172		checkbox = "x"173	}174	lines := strings.Split(item, "\n")175	sb.WriteString("- [" + checkbox + "] " + lines[0] + "\n")176	for _, line := range lines[1:] {177		sb.WriteString("  " + line + "\n")178	}179	return sb.String()180}181182// Nested prefixes each line with a given prefix, enabling nested lists.183// Example: Nested("- foo\n- bar", "  ") => "  - foo\n  - bar\n"184func Nested(content, prefix string) string {185	lines := strings.Split(content, "\n")186	for i := range lines {187		if strings.TrimSpace(lines[i]) != "" {188			lines[i] = prefix + lines[i]189		}190	}191	return strings.Join(lines, "\n")192}193194// Blockquote returns the text as a CommonMark blockquote.195// Example: Blockquote("foo\nbar") => "\n> foo\n> bar\n\n"196//197// Delegates to sanitize.Blockquote, which cleans the content (bidi-strip,198// CR/CRLF/U+2028/U+2029/NEL line-ending normalize, LRD strip, ref-link199// escape, block-marker escape, fence auto-close), line-prefixes each200// line with "> ", and wraps with "\n" / "\n\n" so the quote opens201// cleanly and cannot pull appended chrome into the quote via CM §5.2202// lazy continuation. Callers do NOT need to pre-wrap the input.203func Blockquote(text string) string {204	return sanitize.Blockquote(text)205}206207// InlineCode wraps the given text as a CommonMark inline code span.208// Example: InlineCode("foo") => "`foo`"209//210// Delegates to sanitize.InlineCode, which cleans the input (bidi-strip,211// CR/CRLF + NEL + U+2028/U+2029 folded to single space, NUL→U+FFFD)212// and picks a backtick-run length that outscans any internal backticks.213// Callers do NOT need to pre-wrap the input.214func InlineCode(code string) string {215	return sanitize.InlineCode(code)216}217218// CodeBlock creates a markdown code block.219// Example: CodeBlock("foo") => "```\nfoo\n```"220//221// Delegates to sanitize.CodeBlock, which cleans the content (bidi-strip,222// CR/CRLF normalize, NEL/U+2028/U+2029 fold, NUL→U+FFFD) and picks a223// fence wide enough to outscan any backticks in content. Callers do NOT224// need to pre-wrap content with another sanitize helper.225func CodeBlock(content string) string {226	return sanitize.CodeBlock(content)227}228229// LanguageCodeBlock creates a markdown code block with language-specific syntax highlighting.230// Example: LanguageCodeBlock("go", "foo") => "```go\nfoo\n```"231//232// Delegates to sanitize.LanguageCodeBlock, which validates the language233// tag (charset ^[a-zA-Z0-9_+-]{1,32}$, falling back to a tagless fence234// if invalid) and cleans the content as CodeBlock does. Callers do NOT235// need to pre-wrap either argument.236func LanguageCodeBlock(language, content string) string {237	return sanitize.LanguageCodeBlock(language, content)238}239240// HorizontalRule returns a horizontal rule for markdown.241// Example: HorizontalRule() => "---\n"242func HorizontalRule() string {243	return "---\n"244}245246// Link returns a hyperlink for markdown.247// Example: Link("foo", "http://example.com") => "[foo](http://example.com)"248//249// The text and url args are sanitized internally — text via250// sanitize.InlineText, url via sanitize.URL (allowlists http/https/251// mailto/relative/fragment; rejects javascript:, data:, blob:, etc.).252// Callers do NOT need to pre-wrap either argument; double-wrapping is253// a bug (the inline-text escaper is non-idempotent).254//255// If the URL fails the scheme allowlist, the href is rendered empty —256// the link becomes inert rather than carrying a malicious destination.257func Link(text, url string) string {258	return "[" + sanitize.InlineText(text) + "](" + sanitize.URL(url) + ")"259}260261// UserLink returns a user profile link for markdown.262// Example: UserLink("moul") => "[@moul](/u/moul)"263// Example: UserLink("g1blah...") => "[g1blah...](/u/g1blah...)"264//265// Validates the user identifier — if it matches the gno bech32 address266// pattern (g1...), produces an address-style link; otherwise tries the267// r/sys/users-charset username and produces an @-style link. Returns268// "" if the identifier matches neither — callers should treat "" as269// "skip the user mention" rather than emit a broken link.270func UserLink(user string) string {271	if addr := sanitize.BechString(user, "g"); addr != "" {272		return "[" + addr + "](/u/" + addr + ")"273	}274	if name := sanitize.UserName(user); name != "" {275		return "[@" + name + "](/u/" + name + ")"276	}277	return ""278}279280// InlineImageWithLink creates an inline image wrapped in a hyperlink for markdown.281// Example: InlineImageWithLink("alt text", "image-url", "link-url") => "[![alt text](image-url)](link-url)"282//283// altText and imageUrl are sanitized via Image (sanitize.InlineText +284// sanitize.ImageURL); linkUrl is sanitized via sanitize.URL. Callers285// do NOT need to pre-wrap any argument.286func InlineImageWithLink(altText, imageUrl, linkUrl string) string {287	return "[" + Image(altText, imageUrl) + "](" + sanitize.URL(linkUrl) + ")"288}289290// Image returns an image for markdown.291// Example: Image("foo", "http://example.com") => "![foo](http://example.com)"292//293// altText is sanitized via sanitize.InlineText, url via294// sanitize.ImageURL (allowlists http/https/relative + data:image/*;295// rejects mailto:, javascript:, data:text/html, etc.). Callers do NOT296// need to pre-wrap either argument.297func Image(altText, url string) string {298	return "![" + sanitize.InlineText(altText) + "](" + sanitize.ImageURL(url) + ")"299}300301// FootnoteDefinition emits a GFM footnote definition — `[^name]: body` —302// for a footnote that is referenced elsewhere in the document by303// `[^name]`.304//305// Example: FootnoteDefinition("note1", "Long form of the citation.")306// renders as:307//308//	[^note1]:309//	    Long form of the citation.310//311// The `name` is validated as a FootnoteLabel (^[A-Za-z0-9_-]{1,64}$);312// `text` is user-supplied multi-paragraph prose, sanitized via Block.313// An invalid name or empty body returns "".314//315// Delegates to sanitize.FootnoteDefinition. Callers do NOT need to316// pre-wrap either argument.317func FootnoteDefinition(name, text string) string {318	return sanitize.FootnoteDefinition(name, text)319}320321// LinkReferenceDefinition emits a CommonMark link reference definition322// (CM §4.7) — `[label]: url "title"` — for a reference link that is323// invoked elsewhere by `[text][label]` or by the shortcut form324// `[label]`.325//326// Example: LinkReferenceDefinition("r/docs/help", "/r/docs/help", "")327// renders as:328//329//	[r/docs/help]: /r/docs/help330//331// The `label` is validated as a FootnoteLabel (^[A-Za-z0-9_-]{1,64}$);332// `url` is sanitized via URL (allowlist); `title` is sanitized via333// LinkTitle. An invalid label or rejected URL returns "".334//335// Realms should choose a namespaced label using dashes336// (e.g. `r-myrealm-help`) so that shortcut-reference invocations from337// user content can't collide with bare words a user is likely to write.338// `/` is not in the FootnoteLabel charset.339//340// Delegates to sanitize.LinkReferenceDefinition. Callers do NOT need to341// pre-wrap any argument.342func LinkReferenceDefinition(label, url, title string) string {343	return sanitize.LinkReferenceDefinition(label, url, title)344}345346// Paragraph wraps the given text in a Markdown paragraph.347// Example: Paragraph("foo") => "foo\n"348func Paragraph(content string) string {349	return content + "\n\n"350}351352// CollapsibleSection creates a collapsible section for markdown using353// HTML <details> and <summary> tags.354// Example:355// CollapsibleSection("Click to expand", "Hidden content")356// =>357// <details><summary>Click to expand</summary>358//359// Hidden content360// </details>361//362// The title argument is sanitized via sanitize.HTMLEscape (it lands in363// an HTML element body inside <summary>, not a markdown context — so364// HTML entity escaping is the correct policy, not markdown backslash365// escaping). The content argument is passed through unchanged because366// <details> with a blank-line-separated body allows markdown inside367// (CM §4.6); callers must pre-wrap content with sanitize.Block if it368// derives from user input.369func CollapsibleSection(title, content string) string {370	return "<details><summary>" + sanitize.HTMLEscape(title) + "</summary>\n\n" + content + "\n</details>\n"371}372373// EscapeURL escapes characters in a URL for use in markdown link syntax.374//375// Deprecated: use sanitize.URL (for link href) or sanitize.ImageURL376// (for image src) directly. EscapeURL previously only percent-encoded377// ( and ) and did not validate the URL scheme — a security footgun378// (EscapeURL("javascript:alert(1)") returned a working XSS payload).379// It now delegates to sanitize.URL, which allowlists schemes (rejects380// javascript:, data:text/html, vbscript:, blob:, etc.) and381// percent-encodes all unsafe bytes (including the ( ) this function382// handled). The other helpers in this package (Link, UserLink, Image,383// InlineImageWithLink) sanitize their URL args internally now, so you384// rarely need to call any URL-escape helper directly.385//386// Behavior change vs. the original implementation: invalid schemes387// now return "" instead of passing through with ( ) escaped. Non-ASCII388// bytes get percent-encoded to standard RFC 3986 wire form instead of389// passing through as raw UTF-8.390func EscapeURL(url string) string {391	return sanitize.URL(url)392}393394// EscapeText escapes special Markdown characters in regular text for395// use in inline contexts.396//397// Deprecated: use sanitize.InlineText directly. EscapeText was398// INCOMPLETE — it missed \, #, <, & and did not strip bidi/zero-width399// characters, replace NUL with U+FFFD, or normalize line endings.400// User input could inject backslash escapes (\* cancels neighboring401// escapes), autolinks (<https://x>), raw HTML (<script>), HTML entity402// references (&amp;), and bidi spoofing (RLO before an address). It403// now delegates to sanitize.InlineText. The other helpers in this404// package (Link, UserLink, Image, InlineImageWithLink, CollapsibleSection)405// sanitize their text args internally now, so you rarely need to call406// any inline-text-escape helper directly.407//408// Behavior change vs. the original implementation: \, #, <, & are now409// escaped; | is no longer escaped (the original was over-escaping —410// outside GFM table-cell context, | is markdown-inert; for table411// cells use sanitize.TableCell which adds the | escape on top of the412// inline-text set). Bidi controls are stripped, NUL becomes U+FFFD,413// and line endings normalize.414func EscapeText(text string) string {415	return sanitize.InlineText(text)416}417418// Columns returns a formatted row of columns using the Gno syntax.419// If you want a specific number of columns per row (<=4), use ColumnsN.420// Check /r/docs/markdown#columns for more info.421// If padded=true & the final <gno-columns> tag is missing column content, an empty422// column element will be placed to keep the cols per row constant.423// Padding works only with colsPerRow > 0.424//425// Example:426//427//	Columns([]string{"A", "B"}, false)428//	// Returns:429//	// <gno-columns>430//	// A431//	// <gno-columns-sep>432//	// B433//	// </gno-columns>434func Columns(contentByColumn []string, padded bool) string {435	if len(contentByColumn) == 0 {436		return ""437	}438	maxCols := 4439	if padded && len(contentByColumn)%maxCols != 0 {440		missing := maxCols - len(contentByColumn)%maxCols441		contentByColumn = append(contentByColumn, make([]string, missing)...)442	}443444	var sb strings.Builder445	sb.WriteString("<gno-columns>\n")446447	for i, column := range contentByColumn {448		if i > 0 {449			sb.WriteString("<gno-columns-sep>\n")450		}451		sb.WriteString(column + "\n")452	}453454	sb.WriteString("</gno-columns>\n")455	return sb.String()456}457458const maxColumnsPerRow = 4459460// ColumnsN splits content into multiple rows of N columns each and formats them.461// If colsPerRow <= 0, all items are placed in one <gno-columns> block.462// If padded=true & the final <gno-columns> tag is missing column content, an empty463// column element will be placed to keep the cols per row constant.464// Padding works only with colsPerRow > 0.465// Note: On standard-size screens, gnoweb handles a max of 4 cols per row.466//467// Example:468//469//	ColumnsN([]string{"A", "B", "C"}, 2, false)470//	// Returns:471//	// <gno-columns>472//	// A473//	// <gno-columns-sep>474//	// B475//	// </gno-columns>476//	// <gno-columns>477//	// C478//	// </gno-columns>479func ColumnsN(content []string, colsPerRow int, padded bool) string {480	if len(content) == 0 {481		return ""482	}483	if colsPerRow <= 0 {484		return Columns(content, padded)485	}486487	var sb strings.Builder488	// Case 2: Multiple blocks with max 4 columns489	for i := 0; i < len(content); i += colsPerRow {490		end := i + colsPerRow491		if end > len(content) {492			end = len(content)493		}494		row := content[i:end]495496		// Add padding if needed497		if padded && len(row) < colsPerRow {498			row = append(row, make([]string, colsPerRow-len(row))...)499		}500501		sb.WriteString(Columns(row, false))502	}503	return sb.String()504}505

Functions

not supported for pure packages by the node (vm/qfuncs)

Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.