From dcd3834890d3f68543f423614c67f9396017c352 Mon Sep 17 00:00:00 2001
From: SunSpirit <48086732+sunspirit99@users.noreply.github.com>
Date: Fri, 7 Feb 2025 00:58:08 +0700
Subject: [PATCH] feat(examples): Implement markdown package (#2912)
From https://github.com/gnolang/gno/issues/2753
I keep this PR open to see if I am on the right approach. If it's
suitable, I'll investigate to make further improvements and provide
implementation examples in the `Render()` functions of some current demo
realms to demonstrate the use of this package
![Screenshot from 2024-10-23
19-28-43](https://github.com/user-attachments/assets/a321f13a-01c7-432f-9f06-b02b5e86951a)
cc @moul
Contributors' checklist...
- [ ] Added new tests, or not needed, or not feasible
- [ ] Provided an example (e.g. screenshot) to aid review or the PR is
self-explanatory
- [ ] Updated the official documentation or not needed
- [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message
was included in the description
- [ ] Added references to related issues and PRs
- [ ] Provided any useful hints for running manual tests
- [ ] Added new benchmarks to [generated
graphs](https://gnoland.github.io/benchmarks), if any. More info
[here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
---------
Co-authored-by: Leon Hudak <33522493+leohhhn@users.noreply.github.com>
Co-authored-by: leohhhn
---
examples/gno.land/p/sunspirit/md/gno.mod | 1 +
examples/gno.land/p/sunspirit/md/md.gno | 179 ++++++++++++++++++
examples/gno.land/p/sunspirit/md/md_test.gno | 175 +++++++++++++++++
examples/gno.land/p/sunspirit/table/gno.mod | 1 +
examples/gno.land/p/sunspirit/table/table.gno | 106 +++++++++++
.../gno.land/p/sunspirit/table/table_test.gno | 146 ++++++++++++++
examples/gno.land/r/sunspirit/home/gno.mod | 1 +
examples/gno.land/r/sunspirit/home/home.gno | 34 ++++
examples/gno.land/r/sunspirit/md/gno.mod | 1 +
examples/gno.land/r/sunspirit/md/md.gno | 158 ++++++++++++++++
examples/gno.land/r/sunspirit/md/md_test.gno | 13 ++
11 files changed, 815 insertions(+)
create mode 100644 examples/gno.land/p/sunspirit/md/gno.mod
create mode 100644 examples/gno.land/p/sunspirit/md/md.gno
create mode 100644 examples/gno.land/p/sunspirit/md/md_test.gno
create mode 100644 examples/gno.land/p/sunspirit/table/gno.mod
create mode 100644 examples/gno.land/p/sunspirit/table/table.gno
create mode 100644 examples/gno.land/p/sunspirit/table/table_test.gno
create mode 100644 examples/gno.land/r/sunspirit/home/gno.mod
create mode 100644 examples/gno.land/r/sunspirit/home/home.gno
create mode 100644 examples/gno.land/r/sunspirit/md/gno.mod
create mode 100644 examples/gno.land/r/sunspirit/md/md.gno
create mode 100644 examples/gno.land/r/sunspirit/md/md_test.gno
diff --git a/examples/gno.land/p/sunspirit/md/gno.mod b/examples/gno.land/p/sunspirit/md/gno.mod
new file mode 100644
index 00000000000..caee634f66f
--- /dev/null
+++ b/examples/gno.land/p/sunspirit/md/gno.mod
@@ -0,0 +1 @@
+module gno.land/p/sunspirit/md
diff --git a/examples/gno.land/p/sunspirit/md/md.gno b/examples/gno.land/p/sunspirit/md/md.gno
new file mode 100644
index 00000000000..965373bee85
--- /dev/null
+++ b/examples/gno.land/p/sunspirit/md/md.gno
@@ -0,0 +1,179 @@
+package md
+
+import (
+ "strings"
+
+ "gno.land/p/demo/ufmt"
+)
+
+// Builder helps to build a Markdown string from individual elements
+type Builder struct {
+ elements []string
+}
+
+// NewBuilder creates a new Builder instance
+func NewBuilder() *Builder {
+ return &Builder{}
+}
+
+// Add adds a Markdown element to the builder
+func (m *Builder) Add(md ...string) *Builder {
+ m.elements = append(m.elements, md...)
+ return m
+}
+
+// Render returns the final Markdown string joined with the specified separator
+func (m *Builder) Render(separator string) string {
+ return strings.Join(m.elements, separator)
+}
+
+// Bold returns bold text for markdown
+func Bold(text string) string {
+ return ufmt.Sprintf("**%s**", text)
+}
+
+// Italic returns italicized text for markdown
+func Italic(text string) string {
+ return ufmt.Sprintf("*%s*", text)
+}
+
+// Strikethrough returns strikethrough text for markdown
+func Strikethrough(text string) string {
+ return ufmt.Sprintf("~~%s~~", text)
+}
+
+// H1 returns a level 1 header for markdown
+func H1(text string) string {
+ return ufmt.Sprintf("# %s\n", text)
+}
+
+// H2 returns a level 2 header for markdown
+func H2(text string) string {
+ return ufmt.Sprintf("## %s\n", text)
+}
+
+// H3 returns a level 3 header for markdown
+func H3(text string) string {
+ return ufmt.Sprintf("### %s\n", text)
+}
+
+// H4 returns a level 4 header for markdown
+func H4(text string) string {
+ return ufmt.Sprintf("#### %s\n", text)
+}
+
+// H5 returns a level 5 header for markdown
+func H5(text string) string {
+ return ufmt.Sprintf("##### %s\n", text)
+}
+
+// H6 returns a level 6 header for markdown
+func H6(text string) string {
+ return ufmt.Sprintf("###### %s\n", text)
+}
+
+// BulletList returns an bullet list for markdown
+func BulletList(items []string) string {
+ var sb strings.Builder
+ for _, item := range items {
+ sb.WriteString(ufmt.Sprintf("- %s\n", item))
+ }
+ return sb.String()
+}
+
+// OrderedList returns an ordered list for markdown
+func OrderedList(items []string) string {
+ var sb strings.Builder
+ for i, item := range items {
+ sb.WriteString(ufmt.Sprintf("%d. %s\n", i+1, item))
+ }
+ return sb.String()
+}
+
+// TodoList returns a list of todo items with checkboxes for markdown
+func TodoList(items []string, done []bool) string {
+ var sb strings.Builder
+
+ for i, item := range items {
+ checkbox := " "
+ if done[i] {
+ checkbox = "x"
+ }
+ sb.WriteString(ufmt.Sprintf("- [%s] %s\n", checkbox, item))
+ }
+ return sb.String()
+}
+
+// Blockquote returns a blockquote for markdown
+func Blockquote(text string) string {
+ lines := strings.Split(text, "\n")
+ var sb strings.Builder
+ for _, line := range lines {
+ sb.WriteString(ufmt.Sprintf("> %s\n", line))
+ }
+
+ return sb.String()
+}
+
+// InlineCode returns inline code for markdown
+func InlineCode(code string) string {
+ return ufmt.Sprintf("`%s`", code)
+}
+
+// CodeBlock creates a markdown code block
+func CodeBlock(content string) string {
+ return ufmt.Sprintf("```\n%s\n```", content)
+}
+
+// LanguageCodeBlock creates a markdown code block with language-specific syntax highlighting
+func LanguageCodeBlock(language, content string) string {
+ return ufmt.Sprintf("```%s\n%s\n```", language, content)
+}
+
+// LineBreak returns the specified number of line breaks for markdown
+func LineBreak(count uint) string {
+ if count > 0 {
+ return strings.Repeat("\n", int(count)+1)
+ }
+ return ""
+}
+
+// HorizontalRule returns a horizontal rule for markdown
+func HorizontalRule() string {
+ return "---\n"
+}
+
+// Link returns a hyperlink for markdown
+func Link(text, url string) string {
+ return ufmt.Sprintf("[%s](%s)", text, url)
+}
+
+// Image returns an image for markdown
+func Image(altText, url string) string {
+ return ufmt.Sprintf("![%s](%s)", altText, url)
+}
+
+// Footnote returns a footnote for markdown
+func Footnote(reference, text string) string {
+ return ufmt.Sprintf("[%s]: %s", reference, text)
+}
+
+// Paragraph wraps the given text in a Markdown paragraph
+func Paragraph(content string) string {
+ return ufmt.Sprintf("%s\n", content)
+}
+
+// MdTable is an interface for table types that can be converted to Markdown format
+type MdTable interface {
+ String() string
+}
+
+// Table takes any MdTable implementation and returns its markdown representation
+func Table(table MdTable) string {
+ return table.String()
+}
+
+// EscapeMarkdown escapes special markdown characters in a string
+func EscapeMarkdown(text string) string {
+ return ufmt.Sprintf("``%s``", text)
+}
diff --git a/examples/gno.land/p/sunspirit/md/md_test.gno b/examples/gno.land/p/sunspirit/md/md_test.gno
new file mode 100644
index 00000000000..529cc2535bb
--- /dev/null
+++ b/examples/gno.land/p/sunspirit/md/md_test.gno
@@ -0,0 +1,175 @@
+package md
+
+import (
+ "testing"
+
+ "gno.land/p/demo/uassert"
+ "gno.land/p/sunspirit/table"
+)
+
+func TestNewBuilder(t *testing.T) {
+ mdBuilder := NewBuilder()
+
+ uassert.Equal(t, len(mdBuilder.elements), 0, "Expected 0 elements")
+}
+
+func TestAdd(t *testing.T) {
+ mdBuilder := NewBuilder()
+
+ header := H1("Hi")
+ body := Paragraph("This is a test")
+
+ mdBuilder.Add(header, body)
+
+ uassert.Equal(t, len(mdBuilder.elements), 2, "Expected 2 element")
+ uassert.Equal(t, mdBuilder.elements[0], header, "Expected element %s, got %s", header, mdBuilder.elements[0])
+ uassert.Equal(t, mdBuilder.elements[1], body, "Expected element %s, got %s", body, mdBuilder.elements[1])
+}
+
+func TestRender(t *testing.T) {
+ mdBuilder := NewBuilder()
+
+ header := H1("Hello")
+ body := Paragraph("This is a test")
+
+ seperator := "\n"
+ expected := header + seperator + body
+
+ output := mdBuilder.Add(header, body).Render(seperator)
+
+ uassert.Equal(t, output, expected, "Expected rendered string %s, got %s", expected, output)
+}
+
+func Test_Bold(t *testing.T) {
+ uassert.Equal(t, Bold("Hello"), "**Hello**")
+}
+
+func Test_Italic(t *testing.T) {
+ uassert.Equal(t, Italic("Hello"), "*Hello*")
+}
+
+func Test_Strikethrough(t *testing.T) {
+ uassert.Equal(t, Strikethrough("Hello"), "~~Hello~~")
+}
+
+func Test_H1(t *testing.T) {
+ uassert.Equal(t, H1("Header 1"), "# Header 1\n")
+}
+
+func Test_H2(t *testing.T) {
+ uassert.Equal(t, H2("Header 2"), "## Header 2\n")
+}
+
+func Test_H3(t *testing.T) {
+ uassert.Equal(t, H3("Header 3"), "### Header 3\n")
+}
+
+func Test_H4(t *testing.T) {
+ uassert.Equal(t, H4("Header 4"), "#### Header 4\n")
+}
+
+func Test_H5(t *testing.T) {
+ uassert.Equal(t, H5("Header 5"), "##### Header 5\n")
+}
+
+func Test_H6(t *testing.T) {
+ uassert.Equal(t, H6("Header 6"), "###### Header 6\n")
+}
+
+func Test_BulletList(t *testing.T) {
+ items := []string{"Item 1", "Item 2", "Item 3"}
+ result := BulletList(items)
+ expected := "- Item 1\n- Item 2\n- Item 3\n"
+ uassert.Equal(t, result, expected)
+}
+
+func Test_OrderedList(t *testing.T) {
+ items := []string{"Item 1", "Item 2", "Item 3"}
+ result := OrderedList(items)
+ expected := "1. Item 1\n2. Item 2\n3. Item 3\n"
+ uassert.Equal(t, result, expected)
+}
+
+func Test_TodoList(t *testing.T) {
+ items := []string{"Task 1", "Task 2"}
+ done := []bool{true, false}
+ result := TodoList(items, done)
+ expected := "- [x] Task 1\n- [ ] Task 2\n"
+ uassert.Equal(t, result, expected)
+}
+
+func Test_Blockquote(t *testing.T) {
+ text := "This is a blockquote.\nIt has multiple lines."
+ result := Blockquote(text)
+ expected := "> This is a blockquote.\n> It has multiple lines.\n"
+ uassert.Equal(t, result, expected)
+}
+
+func Test_InlineCode(t *testing.T) {
+ result := InlineCode("code")
+ uassert.Equal(t, result, "`code`")
+}
+
+func Test_LanguageCodeBlock(t *testing.T) {
+ result := LanguageCodeBlock("python", "print('Hello')")
+ expected := "```python\nprint('Hello')\n```"
+ uassert.Equal(t, result, expected)
+}
+
+func Test_CodeBlock(t *testing.T) {
+ result := CodeBlock("print('Hello')")
+ expected := "```\nprint('Hello')\n```"
+ uassert.Equal(t, result, expected)
+}
+
+func Test_LineBreak(t *testing.T) {
+ result := LineBreak(2)
+ expected := "\n\n\n"
+ uassert.Equal(t, result, expected)
+
+ result = LineBreak(0)
+ expected = ""
+ uassert.Equal(t, result, expected)
+}
+
+func Test_HorizontalRule(t *testing.T) {
+ result := HorizontalRule()
+ uassert.Equal(t, result, "---\n")
+}
+
+func Test_Link(t *testing.T) {
+ result := Link("Google", "http://google.com")
+ uassert.Equal(t, result, "[Google](http://google.com)")
+}
+
+func Test_Image(t *testing.T) {
+ result := Image("Alt text", "http://image.url")
+ uassert.Equal(t, result, "![Alt text](http://image.url)")
+}
+
+func Test_Footnote(t *testing.T) {
+ result := Footnote("1", "This is a footnote.")
+ uassert.Equal(t, result, "[1]: This is a footnote.")
+}
+
+func Test_Paragraph(t *testing.T) {
+ result := Paragraph("This is a paragraph.")
+ uassert.Equal(t, result, "This is a paragraph.\n")
+}
+
+func Test_Table(t *testing.T) {
+ tb, err := table.New([]string{"Header1", "Header2"}, [][]string{
+ {"Row1Col1", "Row1Col2"},
+ {"Row2Col1", "Row2Col2"},
+ })
+ uassert.NoError(t, err)
+
+ result := Table(tb)
+ expected := "| Header1 | Header2 |\n| ---|---|\n| Row1Col1 | Row1Col2 |\n| Row2Col1 | Row2Col2 |\n"
+ uassert.Equal(t, result, expected)
+}
+
+func Test_EscapeMarkdown(t *testing.T) {
+ result := EscapeMarkdown("- This is `code`")
+ uassert.Equal(t, result, "``- This is `code```")
+}
diff --git a/examples/gno.land/p/sunspirit/table/gno.mod b/examples/gno.land/p/sunspirit/table/gno.mod
new file mode 100644
index 00000000000..1814c50b25d
--- /dev/null
+++ b/examples/gno.land/p/sunspirit/table/gno.mod
@@ -0,0 +1 @@
+module gno.land/p/sunspirit/table
diff --git a/examples/gno.land/p/sunspirit/table/table.gno b/examples/gno.land/p/sunspirit/table/table.gno
new file mode 100644
index 00000000000..8c27516c962
--- /dev/null
+++ b/examples/gno.land/p/sunspirit/table/table.gno
@@ -0,0 +1,106 @@
+package table
+
+import (
+ "strings"
+
+ "gno.land/p/demo/ufmt"
+)
+
+// Table defines the structure for a markdown table
+type Table struct {
+ header []string
+ rows [][]string
+}
+
+// Validate checks if the number of columns in each row matches the number of columns in the header
+func (t *Table) Validate() error {
+ numCols := len(t.header)
+ for _, row := range t.rows {
+ if len(row) != numCols {
+ return ufmt.Errorf("row %v does not match header length %d", row, numCols)
+ }
+ }
+ return nil
+}
+
+// New creates a new Table instance, ensuring the header and rows match in size
+func New(header []string, rows [][]string) (*Table, error) {
+ t := &Table{
+ header: header,
+ rows: rows,
+ }
+
+ if err := t.Validate(); err != nil {
+ return nil, err
+ }
+
+ return t, nil
+}
+
+// Table returns a markdown string for the given Table
+func (t *Table) String() string {
+ if err := t.Validate(); err != nil {
+ panic(err)
+ }
+
+ var sb strings.Builder
+
+ sb.WriteString("| " + strings.Join(t.header, " | ") + " |\n")
+ sb.WriteString("| " + strings.Repeat("---|", len(t.header)) + "\n")
+
+ for _, row := range t.rows {
+ sb.WriteString("| " + strings.Join(row, " | ") + " |\n")
+ }
+
+ return sb.String()
+}
+
+// AddRow adds a new row to the table
+func (t *Table) AddRow(row []string) error {
+ if len(row) != len(t.header) {
+ return ufmt.Errorf("row %v does not match header length %d", row, len(t.header))
+ }
+ t.rows = append(t.rows, row)
+ return nil
+}
+
+// AddColumn adds a new column to the table with the specified values
+func (t *Table) AddColumn(header string, values []string) error {
+ if len(values) != len(t.rows) {
+ return ufmt.Errorf("values length %d does not match the number of rows %d", len(values), len(t.rows))
+ }
+
+ // Add the new header
+ t.header = append(t.header, header)
+
+ // Add the new column values to each row
+ for i, value := range values {
+ t.rows[i] = append(t.rows[i], value)
+ }
+ return nil
+}
+
+// RemoveRow removes a row from the table by its index
+func (t *Table) RemoveRow(index int) error {
+ if index < 0 || index >= len(t.rows) {
+ return ufmt.Errorf("index %d is out of range", index)
+ }
+ t.rows = append(t.rows[:index], t.rows[index+1:]...)
+ return nil
+}
+
+// RemoveColumn removes a column from the table by its index
+func (t *Table) RemoveColumn(index int) error {
+ if index < 0 || index >= len(t.header) {
+ return ufmt.Errorf("index %d is out of range", index)
+ }
+
+ // Remove the column from the header
+ t.header = append(t.header[:index], t.header[index+1:]...)
+
+ // Remove the corresponding column from each row
+ for i := range t.rows {
+ t.rows[i] = append(t.rows[i][:index], t.rows[i][index+1:]...)
+ }
+ return nil
+}
diff --git a/examples/gno.land/p/sunspirit/table/table_test.gno b/examples/gno.land/p/sunspirit/table/table_test.gno
new file mode 100644
index 00000000000..d4cd56ad0a8
--- /dev/null
+++ b/examples/gno.land/p/sunspirit/table/table_test.gno
@@ -0,0 +1,146 @@
+package table
+
+import (
+ "testing"
+
+ "gno.land/p/demo/uassert"
+ "gno.land/p/demo/urequire"
+)
+
+func TestNew(t *testing.T) {
+ header := []string{"Name", "Age", "Country"}
+ rows := [][]string{
+ {"Alice", "30", "USA"},
+ {"Bob", "25", "UK"},
+ }
+
+ table, err := New(header, rows)
+ urequire.NoError(t, err)
+
+ uassert.Equal(t, len(header), len(table.header))
+ uassert.Equal(t, len(rows), len(table.rows))
+}
+
+func Test_AddRow(t *testing.T) {
+ header := []string{"Name", "Age"}
+ rows := [][]string{
+ {"Alice", "30"},
+ {"Bob", "25"},
+ }
+
+ table, err := New(header, rows)
+ urequire.NoError(t, err)
+
+ // Add a valid row
+ err = table.AddRow([]string{"Charlie", "28"})
+ urequire.NoError(t, err)
+
+ expectedRows := [][]string{
+ {"Alice", "30"},
+ {"Bob", "25"},
+ {"Charlie", "28"},
+ }
+ uassert.Equal(t, len(expectedRows), len(table.rows))
+
+ // Attempt to add a row with a different number of columns
+ err = table.AddRow([]string{"David"})
+ uassert.Error(t, err)
+}
+
+func Test_AddColumn(t *testing.T) {
+ header := []string{"Name", "Age"}
+ rows := [][]string{
+ {"Alice", "30"},
+ {"Bob", "25"},
+ }
+
+ table, err := New(header, rows)
+ urequire.NoError(t, err)
+
+ // Add a valid column
+ err = table.AddColumn("Country", []string{"USA", "UK"})
+ urequire.NoError(t, err)
+
+ expectedHeader := []string{"Name", "Age", "Country"}
+ expectedRows := [][]string{
+ {"Alice", "30", "USA"},
+ {"Bob", "25", "UK"},
+ }
+ uassert.Equal(t, len(expectedHeader), len(table.header))
+ uassert.Equal(t, len(expectedRows), len(table.rows))
+
+ // Attempt to add a column with a different number of values
+ err = table.AddColumn("City", []string{"New York"})
+ uassert.Error(t, err)
+}
+
+func Test_RemoveRow(t *testing.T) {
+ header := []string{"Name", "Age", "Country"}
+ rows := [][]string{
+ {"Alice", "30", "USA"},
+ {"Bob", "25", "UK"},
+ }
+
+ table, err := New(header, rows)
+ urequire.NoError(t, err)
+
+ // Remove the first row
+ err = table.RemoveRow(0)
+ urequire.NoError(t, err)
+
+ expectedRows := [][]string{
+ {"Bob", "25", "UK"},
+ }
+ uassert.Equal(t, len(expectedRows), len(table.rows))
+
+ // Attempt to remove a row out of range
+ err = table.RemoveRow(5)
+ uassert.Error(t, err)
+}
+
+func Test_RemoveColumn(t *testing.T) {
+ header := []string{"Name", "Age", "Country"}
+ rows := [][]string{
+ {"Alice", "30", "USA"},
+ {"Bob", "25", "UK"},
+ }
+
+ table, err := New(header, rows)
+ urequire.NoError(t, err)
+
+ // Remove the second column (Age)
+ err = table.RemoveColumn(1)
+ urequire.NoError(t, err)
+
+ expectedHeader := []string{"Name", "Country"}
+ expectedRows := [][]string{
+ {"Alice", "USA"},
+ {"Bob", "UK"},
+ }
+ uassert.Equal(t, len(expectedHeader), len(table.header))
+ uassert.Equal(t, len(expectedRows), len(table.rows))
+
+ // Attempt to remove a column out of range
+ err = table.RemoveColumn(5)
+ uassert.Error(t, err)
+}
+
+func Test_Validate(t *testing.T) {
+ header := []string{"Name", "Age", "Country"}
+ rows := [][]string{
+ {"Alice", "30", "USA"},
+ {"Bob", "25"},
+ }
+
+ table, err := New(header, rows[:1])
+ urequire.NoError(t, err)
+
+ // Validate should pass
+ err = table.Validate()
+ urequire.NoError(t, err)
+
+ // Add an invalid row and validate again
+ table.rows = append(table.rows, rows[1])
+ err = table.Validate()
+ uassert.Error(t, err)
+}
diff --git a/examples/gno.land/r/sunspirit/home/gno.mod b/examples/gno.land/r/sunspirit/home/gno.mod
new file mode 100644
index 00000000000..2aea0280fff
--- /dev/null
+++ b/examples/gno.land/r/sunspirit/home/gno.mod
@@ -0,0 +1 @@
+module gno.land/r/sunspirit/home
diff --git a/examples/gno.land/r/sunspirit/home/home.gno b/examples/gno.land/r/sunspirit/home/home.gno
new file mode 100644
index 00000000000..fbf9709e8d4
--- /dev/null
+++ b/examples/gno.land/r/sunspirit/home/home.gno
@@ -0,0 +1,34 @@
+package home
+
+import (
+ "strings"
+
+ "gno.land/p/demo/ufmt"
+ "gno.land/p/sunspirit/md"
+)
+
+func Render(path string) string {
+ var sb strings.Builder
+
+ sb.WriteString(md.H1("Sunspirit's Home") + md.LineBreak(1))
+
+ sb.WriteString(md.Paragraph(ufmt.Sprintf(
+ "Welcome to Sunspirit’s home! This is where I’ll bring %s to Gno.land, crafted with my experience and creativity.",
+ md.Italic(md.Bold("simple, useful dapps")),
+ )) + md.LineBreak(1))
+
+ sb.WriteString(md.Paragraph(ufmt.Sprintf(
+ "📚 I’ve created a Markdown rendering library at %s. Feel free to use it for your own projects!",
+ md.Link("gno.land/p/sunspirit/md", "/p/sunspirit/md"),
+ )) + md.LineBreak(1))
+
+ sb.WriteString(md.Paragraph("💬 I’d love to hear your feedback to help improve this library!") + md.LineBreak(1))
+
+ sb.WriteString(md.Paragraph(ufmt.Sprintf(
+ "🌐 You can check out a demo of this package in action at %s.",
+ md.Link("gno.land/r/sunspirit/md", "/r/sunspirit/md"),
+ )) + md.LineBreak(1))
+ sb.WriteString(md.HorizontalRule())
+
+ return sb.String()
+}
diff --git a/examples/gno.land/r/sunspirit/md/gno.mod b/examples/gno.land/r/sunspirit/md/gno.mod
new file mode 100644
index 00000000000..ff3a7c54d96
--- /dev/null
+++ b/examples/gno.land/r/sunspirit/md/gno.mod
@@ -0,0 +1 @@
+module gno.land/r/sunspirit/md
diff --git a/examples/gno.land/r/sunspirit/md/md.gno b/examples/gno.land/r/sunspirit/md/md.gno
new file mode 100644
index 00000000000..8c21ea0215c
--- /dev/null
+++ b/examples/gno.land/r/sunspirit/md/md.gno
@@ -0,0 +1,158 @@
+package md
+
+import (
+ "gno.land/p/sunspirit/md"
+ "gno.land/p/sunspirit/table"
+)
+
+func Render(path string) string {
+ title := "A simple, flexible, and easy-to-use library for creating markdown documents in gno.land"
+
+ mdBuilder := md.NewBuilder().
+ Add(md.H1(md.Italic(md.Bold(title)))).
+
+ // Bold Text section
+ Add(
+ md.H3(md.Bold("1. Bold Text")),
+ md.Paragraph("To make text bold, use the `md.Bold()` function:"),
+ md.Bold("This is bold text"),
+ ).
+
+ // Italic Text section
+ Add(
+ md.H3(md.Bold("2. Italic Text")),
+ md.Paragraph("To make text italic, use the `md.Italic()` function:"),
+ md.Italic("This is italic text"),
+ ).
+
+ // Strikethrough Text section
+ Add(
+ md.H3(md.Bold("3. Strikethrough Text")),
+ md.Paragraph("To add strikethrough, use the `md.Strikethrough()` function:"),
+ md.Strikethrough("This text is strikethrough"),
+ ).
+
+ // Headers section
+ Add(
+ md.H3(md.Bold("4. Headers (H1 to H6)")),
+ md.Paragraph("You can create headers (H1 to H6) using the `md.H1()` to `md.H6()` functions:"),
+ md.H1("This is a level 1 header"),
+ md.H2("This is a level 2 header"),
+ md.H3("This is a level 3 header"),
+ md.H4("This is a level 4 header"),
+ md.H5("This is a level 5 header"),
+ md.H6("This is a level 6 header"),
+ ).
+
+ // Bullet List section
+ Add(
+ md.H3(md.Bold("5. Bullet List")),
+ md.Paragraph("To create bullet lists, use the `md.BulletList()` function:"),
+ md.BulletList([]string{"Item 1", "Item 2", "Item 3"}),
+ ).
+
+ // Ordered List section
+ Add(
+ md.H3(md.Bold("6. Ordered List")),
+ md.Paragraph("To create ordered lists, use the `md.OrderedList()` function:"),
+ md.OrderedList([]string{"First", "Second", "Third"}),
+ ).
+
+ // Todo List section
+ Add(
+ md.H3(md.Bold("7. Todo List")),
+ md.Paragraph("You can create a todo list using the `md.TodoList()` function, which supports checkboxes:"),
+ md.TodoList([]string{"Task 1", "Task 2"}, []bool{true, false}),
+ ).
+
+ // Blockquote section
+ Add(
+ md.H3(md.Bold("8. Blockquote")),
+ md.Paragraph("To create blockquotes, use the `md.Blockquote()` function:"),
+ md.Blockquote("This is a blockquote.\nIt can span multiple lines."),
+ ).
+
+ // Inline Code section
+ Add(
+ md.H3(md.Bold("9. Inline Code")),
+ md.Paragraph("To insert inline code, use the `md.InlineCode()` function:"),
+ md.InlineCode("fmt.Println() // inline code"),
+ ).
+
+ // Code Block section
+ Add(
+ md.H3(md.Bold("10. Code Block")),
+ md.Paragraph("For multi-line code blocks, use the `md.CodeBlock()` function:"),
+ md.CodeBlock("package main\n\nfunc main() {\n\t// Your code here\n}"),
+ ).
+
+ // Horizontal Rule section
+ Add(
+ md.H3(md.Bold("11. Horizontal Rule")),
+ md.Paragraph("To add a horizontal rule (separator), use the `md.HorizontalRule()` function:"),
+ md.LineBreak(1),
+ md.HorizontalRule(),
+ ).
+
+ // Language-specific Code Block section
+ Add(
+ md.H3(md.Bold("12. Language-specific Code Block")),
+ md.Paragraph("To create language-specific code blocks, use the `md.LanguageCodeBlock()` function:"),
+ md.LanguageCodeBlock("go", "package main\n\nfunc main() {}"),
+ ).
+
+ // Hyperlink section
+ Add(
+ md.H3(md.Bold("13. Hyperlink")),
+ md.Paragraph("To create a hyperlink, use the `md.Link()` function:"),
+ md.Link("Gnoland official docs", "https://docs.gno.land"),
+ ).
+
+ // Image section
+ Add(
+ md.H3(md.Bold("14. Image")),
+ md.Paragraph("To insert an image, use the `md.Image()` function:"),
+ md.LineBreak(1),
+ md.Image("Gnoland Logo", "https://gnolang.github.io/blog/2024-05-21_the-gnome/src/banner.png"),
+ ).
+
+ // Footnote section
+ Add(
+ md.H3(md.Bold("15. Footnote")),
+ md.Paragraph("To create footnotes, use the `md.Footnote()` function:"),
+ md.LineBreak(1),
+ md.Footnote("1", "This is a footnote."),
+ ).
+
+ // Table section
+ Add(
+ md.H3(md.Bold("16. Table")),
+ md.Paragraph("To create a table, use the `md.Table()` function. Here's an example of a table:"),
+ )
+
+ // Create a table using the table package
+ tb, _ := table.New([]string{"Feature", "Description"}, [][]string{
+ {"Bold", "Make text bold using " + md.Bold("double asterisks")},
+ {"Italic", "Make text italic using " + md.Italic("single asterisks")},
+ {"Strikethrough", "Cross out text using " + md.Strikethrough("double tildes")},
+ })
+ mdBuilder.Add(md.Table(tb))
+
+ // Escaping Markdown section
+ mdBuilder.Add(
+ md.H3(md.Bold("17. Escaping Markdown")),
+ md.Paragraph("Sometimes, you need to escape special Markdown characters (like *, _, and `). Use the `md.EscapeMarkdown()` function for this:"),
+ )
+
+ // Example of escaping markdown
+ text := "- Escape special chars like *, _, and ` in markdown"
+ mdBuilder.Add(
+ md.H4("Text Without Escape:"),
+ text,
+ md.LineBreak(1),
+ md.H4("Text With Escape:"),
+ md.EscapeMarkdown(text),
+ )
+
+ return mdBuilder.Render(md.LineBreak(1))
+}
diff --git a/examples/gno.land/r/sunspirit/md/md_test.gno b/examples/gno.land/r/sunspirit/md/md_test.gno
new file mode 100644
index 00000000000..2e1ce9b9931
--- /dev/null
+++ b/examples/gno.land/r/sunspirit/md/md_test.gno
@@ -0,0 +1,13 @@
+package md
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestRender(t *testing.T) {
+ output := Render("")
+ if !strings.Contains(output, "A simple, flexible, and easy-to-use library for creating markdown documents in gno.land") {
+ t.Errorf("invalid output")
+ }
+}