diff --git a/examples/gno.land/p/jeronimoalbi/mdform/README.md b/examples/gno.land/p/jeronimoalbi/mdform/README.md
new file mode 100644
index 00000000000..e3457ab7563
--- /dev/null
+++ b/examples/gno.land/p/jeronimoalbi/mdform/README.md
@@ -0,0 +1,64 @@
+# Markdown Form Package
+
+The package provides a very simplistic [Gno-Flavored Markdown form](/r/docs/markdown#forms) generator.
+
+Forms can be created by sequentially calling form methods to create each one of the form fields.
+
+Example usage:
+
+```go
+import "gno.land/p/jeronimoalbi/mdform"
+
+func Render(string) string {
+ form := mdform.New()
+
+ // Add a text input field
+ form.Input(
+ "name",
+ "placeholder", "Name",
+ "value", "John Doe",
+ )
+
+ // Add a select field with three possible values
+ form.Select(
+ "country",
+ "United States",
+ "description", "Select your country",
+ )
+ form.Select(
+ "country",
+ "Spain",
+ )
+ form.Select(
+ "country",
+ "Germany",
+ )
+
+ // Add a checkbox group with two possible values
+ form.Checkbox(
+ "interests",
+ "music",
+ "description", "What do you like to do?",
+ )
+ form.Checkbox(
+ "interests",
+ "tech",
+ "checked", "true",
+ )
+
+ return form.String()
+}
+```
+
+Form output:
+
+```html
+
+
+
+
+
+
+
+
+```
diff --git a/examples/gno.land/p/jeronimoalbi/mdform/gnomod.toml b/examples/gno.land/p/jeronimoalbi/mdform/gnomod.toml
new file mode 100644
index 00000000000..1e87ec592dd
--- /dev/null
+++ b/examples/gno.land/p/jeronimoalbi/mdform/gnomod.toml
@@ -0,0 +1,2 @@
+module = "gno.land/p/jeronimoalbi/mdform"
+gno = "0.9"
diff --git a/examples/gno.land/p/jeronimoalbi/mdform/mdform.gno b/examples/gno.land/p/jeronimoalbi/mdform/mdform.gno
new file mode 100644
index 00000000000..3ba404e2c8a
--- /dev/null
+++ b/examples/gno.land/p/jeronimoalbi/mdform/mdform.gno
@@ -0,0 +1,198 @@
+package mdform
+
+import (
+ "html"
+ "strings"
+)
+
+const (
+ InputTypeText = "text"
+ InputTypeNumber = "number"
+ InputTypeEmail = "email"
+ InputTypePhone = "tel"
+ InputTypePassword = "password"
+ InputTypeRadio = "radio"
+ InputTypeCheckbox = "checkbox"
+)
+
+var (
+ formAttributes = []string{"exec", "path"}
+ inputAttributes = []string{"checked", "description", "placeholder", "readonly", "type", "value"}
+ textareaAttributes = []string{"placeholder", "readonly", "rows", "value"}
+ selectAttributes = []string{"description", "readonly", "selected"}
+)
+
+// New creates a new form.
+func New(attributes ...string) *Form {
+ assertEvenAttributes(attributes)
+
+ form := &Form{}
+ for i := 0; i < len(attributes); i += 2 {
+ name, value := attributes[i], attributes[i+1]
+
+ assertIsValidAttribute(name, formAttributes)
+
+ form.attrs = append(form.attrs, formatAttribute(name, value))
+ }
+ return form
+}
+
+// Form is a form that can be rendered to Gno-Flavored Markdown.
+type Form struct {
+ attrs []string
+ fields []string
+}
+
+// Input appends a new input to form fields.
+// Use `Form.Radio()` or `Form.Checkbox()` to append those types of inputs to the form.
+// Method panics when appending inputs of type radio or checkbox, or when attributes are not valid.
+func (f *Form) Input(name string, attributes ...string) *Form {
+ name = strings.TrimSpace(name)
+ if name == "" {
+ panic("form input name is required")
+ }
+
+ assertEvenAttributes(attributes)
+
+ attrs := []string{formatAttribute("name", name)}
+ for i := 0; i < len(attributes); i += 2 {
+ name, value := attributes[i], attributes[i+1]
+ if name == "type" {
+ switch value {
+ case InputTypeRadio:
+ panic("use form.Radio() to create inputs of type radio")
+ case InputTypeCheckbox:
+ panic("use form.Checkbox() to create inputs of type checkbox")
+ }
+ }
+
+ assertIsValidAttribute(name, inputAttributes)
+
+ attrs = append(attrs, formatAttribute(name, value))
+ }
+
+ f.fields = append(f.fields, "")
+ return f
+}
+
+// Radio appends a new input of type radio to form fields.
+// Method panics when attributes are not valid.
+func (f *Form) Radio(name, value string, attributes ...string) *Form {
+ return f.appendInputType(InputTypeRadio, name, value, attributes...)
+}
+
+// Checkbox appends a new input of type checkbox to form fields.
+// Method panics when attributes are not valid.
+func (f *Form) Checkbox(name, value string, attributes ...string) *Form {
+ return f.appendInputType(InputTypeCheckbox, name, value, attributes...)
+}
+
+// Textarea appends a new textarea to form fields.
+// Method panics when attributes are not valid.
+func (f *Form) Textarea(name string, attributes ...string) *Form {
+ name = strings.TrimSpace(name)
+ if name == "" {
+ panic("form textarea name is required")
+ }
+
+ assertEvenAttributes(attributes)
+
+ attrs := []string{formatAttribute("name", name)}
+ for i := 0; i < len(attributes); i += 2 {
+ name, value := attributes[i], attributes[i+1]
+
+ assertIsValidAttribute(name, textareaAttributes)
+
+ attrs = append(attrs, formatAttribute(name, value))
+ }
+
+ f.fields = append(f.fields, "")
+ return f
+}
+
+// Select appends a new select to form fields.
+// Method panics when attributes are not valid.
+func (f *Form) Select(name, value string, attributes ...string) *Form {
+ name = strings.TrimSpace(name)
+ if name == "" {
+ panic("form select name is required")
+ }
+
+ assertEvenAttributes(attributes)
+
+ attrs := []string{
+ formatAttribute("name", name),
+ formatAttribute("value", value),
+ }
+
+ for i := 0; i < len(attributes); i += 2 {
+ name, value := attributes[i], attributes[i+1]
+
+ assertIsValidAttribute(name, selectAttributes)
+
+ attrs = append(attrs, formatAttribute(name, value))
+ }
+
+ f.fields = append(f.fields, "")
+ return f
+}
+
+// String returns the form as Gno-Flavored Markdown.
+func (f Form) String() string {
+ fields := strings.Join(f.fields, "\n")
+ attrs := strings.Join(f.attrs, " ")
+ if len(attrs) > 0 {
+ attrs = " " + attrs
+ }
+
+ return "\n" + fields + "\n\n"
+}
+
+func (f *Form) appendInputType(typeName, name, value string, attributes ...string) *Form {
+ name = strings.TrimSpace(name)
+ if name == "" {
+ panic("form " + typeName + " input name is required")
+ }
+
+ assertEvenAttributes(attributes)
+
+ attrs := []string{
+ formatAttribute("type", typeName),
+ formatAttribute("name", name),
+ formatAttribute("value", value),
+ }
+
+ for i := 0; i < len(attributes); i += 2 {
+ name, value := attributes[i], attributes[i+1]
+ if name == "type" || name == "value" {
+ continue
+ }
+
+ assertIsValidAttribute(name, inputAttributes)
+
+ attrs = append(attrs, formatAttribute(name, value))
+ }
+
+ f.fields = append(f.fields, "")
+ return f
+}
+
+func formatAttribute(name, value string) string {
+ return name + `="` + html.EscapeString(value) + `"`
+}
+
+func assertEvenAttributes(attrs []string) {
+ if len(attrs)%2 != 0 {
+ panic("expected an even number of attribute arguments")
+ }
+}
+
+func assertIsValidAttribute(attr string, attrs []string) {
+ for _, name := range attrs {
+ if name == attr {
+ return
+ }
+ }
+
+ panic("invalid attribute: " + attr)
+}
diff --git a/examples/gno.land/p/jeronimoalbi/mdform/mdform_filetest.gno b/examples/gno.land/p/jeronimoalbi/mdform/mdform_filetest.gno
new file mode 100644
index 00000000000..c0a7abeb8cb
--- /dev/null
+++ b/examples/gno.land/p/jeronimoalbi/mdform/mdform_filetest.gno
@@ -0,0 +1,49 @@
+package main
+
+import "gno.land/p/jeronimoalbi/mdform"
+
+func main() {
+ form := mdform.
+ New(
+ "exec", "FunctionName",
+ ).
+ Input(
+ "name",
+ "placeholder", "Name",
+ "value", "John Doe",
+ ).
+ Select(
+ "country",
+ "United States",
+ "description", "Select your country",
+ ).
+ Select(
+ "country",
+ "Spain",
+ ).
+ Select(
+ "country",
+ "Germany",
+ ).
+ Checkbox(
+ "interests",
+ "music",
+ "description", "What do you like to do?",
+ ).
+ Checkbox(
+ "interests",
+ "tech",
+ "checked", "true",
+ )
+ println(form.String())
+}
+
+// Output:
+//
+//
+//
+//
+//
+//
+//
+//
diff --git a/examples/gno.land/p/jeronimoalbi/mdform/mdform_test.gno b/examples/gno.land/p/jeronimoalbi/mdform/mdform_test.gno
new file mode 100644
index 00000000000..72b5e7fc718
--- /dev/null
+++ b/examples/gno.land/p/jeronimoalbi/mdform/mdform_test.gno
@@ -0,0 +1,392 @@
+package mdform_test
+
+import (
+ "strings"
+ "testing"
+
+ "gno.land/p/jeronimoalbi/mdform"
+ "gno.land/p/nt/uassert"
+ "gno.land/p/nt/urequire"
+)
+
+func TestNew(t *testing.T) {
+ tests := []struct {
+ name string
+ attrs []string
+ markdown string
+ err string
+ }{
+ {
+ name: "ok",
+ attrs: []string{"exec", "FunctionName"},
+ markdown: ``,
+ },
+ {
+ name: "no attributes",
+ markdown: ``,
+ },
+ {
+ name: "uneven attributes",
+ attrs: []string{"exec"},
+ err: "expected an even number of attribute arguments",
+ },
+ {
+ name: "invalid attribute",
+ attrs: []string{"foo", ""},
+ err: "invalid attribute: foo",
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ var markdown string
+ fn := func() {
+ f := mdform.New(tc.attrs...)
+ markdown = strings.ReplaceAll(f.String(), "\n", "")
+ }
+
+ if tc.err != "" {
+ urequire.PanicsWithMessage(t, tc.err, fn, "expect form to fail")
+ return
+ }
+
+ urequire.NotPanics(t, fn, "expect form to be created")
+ uassert.Equal(t, tc.markdown, markdown, "expected markdown to match")
+ })
+ }
+}
+
+func TestFormInput(t *testing.T) {
+ tests := []struct {
+ name string
+ inputName string
+ attrs []string
+ markdown string
+ err string
+ }{
+ {
+ name: "ok",
+ inputName: "test",
+ attrs: []string{"value", "foo"},
+ markdown: ``,
+ },
+ {
+ name: "no attributes",
+ inputName: "test",
+ markdown: ``,
+ },
+ {
+ name: "empty name",
+ inputName: " ",
+ err: "form input name is required",
+ },
+ {
+ name: "radio type",
+ inputName: "test",
+ attrs: []string{"type", "radio"},
+ err: "use form.Radio() to create inputs of type radio",
+ },
+ {
+ name: "checkbox type",
+ inputName: "test",
+ attrs: []string{"type", "checkbox"},
+ err: "use form.Checkbox() to create inputs of type checkbox",
+ },
+ {
+ name: "uneven attributes",
+ inputName: "test",
+ attrs: []string{"value"},
+ err: "expected an even number of attribute arguments",
+ },
+ {
+ name: "invalid attribute",
+ inputName: "test",
+ attrs: []string{"foo", ""},
+ err: "invalid attribute: foo",
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ var markdown string
+ fn := func() {
+ f := mdform.New()
+ f.Input(tc.inputName, tc.attrs...)
+ markdown = strings.ReplaceAll(f.String(), "\n", "")
+ }
+
+ if tc.err != "" {
+ urequire.PanicsWithMessage(t, tc.err, fn, "expect form input to fail")
+ return
+ }
+
+ urequire.NotPanics(t, fn, "expect form input to be created")
+ uassert.Equal(t, tc.markdown, markdown, "expected markdown to match")
+ })
+ }
+}
+
+func TestFormRadio(t *testing.T) {
+ tests := []struct {
+ name string
+ inputName string
+ value string
+ attrs []string
+ markdown string
+ err string
+ }{
+ {
+ name: "ok",
+ inputName: "test",
+ value: "foo",
+ attrs: []string{"readonly", "true"},
+ markdown: ``,
+ },
+ {
+ name: "no attributes",
+ inputName: "test",
+ value: "foo",
+ markdown: ``,
+ },
+ {
+ name: "empty name",
+ inputName: " ",
+ err: "form radio input name is required",
+ },
+ {
+ name: "ignore type attribute",
+ inputName: "test",
+ attrs: []string{"type", "text"},
+ markdown: ``,
+ },
+ {
+ name: "ignore value attribute",
+ inputName: "test",
+ attrs: []string{"value", "foo"},
+ markdown: ``,
+ },
+ {
+ name: "uneven attributes",
+ inputName: "test",
+ attrs: []string{"readonly"},
+ err: "expected an even number of attribute arguments",
+ },
+ {
+ name: "invalid attribute",
+ inputName: "test",
+ attrs: []string{"foo", ""},
+ err: "invalid attribute: foo",
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ var markdown string
+ fn := func() {
+ f := mdform.New()
+ f.Radio(tc.inputName, tc.value, tc.attrs...)
+ markdown = strings.ReplaceAll(f.String(), "\n", "")
+ }
+
+ if tc.err != "" {
+ urequire.PanicsWithMessage(t, tc.err, fn, "expect form radio input to fail")
+ return
+ }
+
+ urequire.NotPanics(t, fn, "expect form radio input to be created")
+ uassert.Equal(t, tc.markdown, markdown, "expected markdown to match")
+ })
+ }
+}
+
+func TestFormCheckbox(t *testing.T) {
+ tests := []struct {
+ name string
+ inputName string
+ value string
+ attrs []string
+ markdown string
+ err string
+ }{
+ {
+ name: "ok",
+ inputName: "test",
+ value: "foo",
+ attrs: []string{"readonly", "true"},
+ markdown: ``,
+ },
+ {
+ name: "no attributes",
+ inputName: "test",
+ value: "foo",
+ markdown: ``,
+ },
+ {
+ name: "empty name",
+ inputName: " ",
+ err: "form checkbox input name is required",
+ },
+ {
+ name: "ignore type attribute",
+ inputName: "test",
+ attrs: []string{"type", "text"},
+ markdown: ``,
+ },
+ {
+ name: "ignore value attribute",
+ inputName: "test",
+ attrs: []string{"value", "foo"},
+ markdown: ``,
+ },
+ {
+ name: "uneven attributes",
+ inputName: "test",
+ attrs: []string{"readonly"},
+ err: "expected an even number of attribute arguments",
+ },
+ {
+ name: "invalid attribute",
+ inputName: "test",
+ attrs: []string{"foo", ""},
+ err: "invalid attribute: foo",
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ var markdown string
+ fn := func() {
+ f := mdform.New()
+ f.Checkbox(tc.inputName, tc.value, tc.attrs...)
+ markdown = strings.ReplaceAll(f.String(), "\n", "")
+ }
+
+ if tc.err != "" {
+ urequire.PanicsWithMessage(t, tc.err, fn, "expect form checkbox input to fail")
+ return
+ }
+
+ urequire.NotPanics(t, fn, "expect form checkbox input to be created")
+ uassert.Equal(t, tc.markdown, markdown, "expected markdown to match")
+ })
+ }
+}
+
+func TestFormTextarea(t *testing.T) {
+ tests := []struct {
+ name string
+ inputName string
+ attrs []string
+ markdown string
+ err string
+ }{
+ {
+ name: "ok",
+ inputName: "test",
+ attrs: []string{"value", "foo"},
+ markdown: ``,
+ },
+ {
+ name: "no attributes",
+ inputName: "test",
+ markdown: ``,
+ },
+ {
+ name: "empty name",
+ inputName: " ",
+ err: "form textarea name is required",
+ },
+ {
+ name: "uneven attributes",
+ inputName: "test",
+ attrs: []string{"value"},
+ err: "expected an even number of attribute arguments",
+ },
+ {
+ name: "invalid attribute",
+ inputName: "test",
+ attrs: []string{"foo", ""},
+ err: "invalid attribute: foo",
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ var markdown string
+ fn := func() {
+ f := mdform.New()
+ f.Textarea(tc.inputName, tc.attrs...)
+ markdown = strings.ReplaceAll(f.String(), "\n", "")
+ }
+
+ if tc.err != "" {
+ urequire.PanicsWithMessage(t, tc.err, fn, "expect form textarea to fail")
+ return
+ }
+
+ urequire.NotPanics(t, fn, "expect form textarea to be created")
+ uassert.Equal(t, tc.markdown, markdown, "expected markdown to match")
+ })
+ }
+}
+
+func TestFormSelect(t *testing.T) {
+ tests := []struct {
+ name string
+ inputName string
+ value string
+ attrs []string
+ markdown string
+ err string
+ }{
+ {
+ name: "ok",
+ inputName: "test",
+ value: "foo",
+ attrs: []string{"readonly", "true"},
+ markdown: ``,
+ },
+ {
+ name: "no attributes",
+ inputName: "test",
+ value: "foo",
+ markdown: ``,
+ },
+ {
+ name: "empty name",
+ inputName: " ",
+ err: "form select name is required",
+ },
+ {
+ name: "uneven attributes",
+ inputName: "test",
+ attrs: []string{"value"},
+ err: "expected an even number of attribute arguments",
+ },
+ {
+ name: "invalid attribute",
+ inputName: "test",
+ attrs: []string{"foo", ""},
+ err: "invalid attribute: foo",
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ var markdown string
+ fn := func() {
+ f := mdform.New()
+ f.Select(tc.inputName, tc.value, tc.attrs...)
+ markdown = strings.ReplaceAll(f.String(), "\n", "")
+ }
+
+ if tc.err != "" {
+ urequire.PanicsWithMessage(t, tc.err, fn, "expect form textarea to fail")
+ return
+ }
+
+ urequire.NotPanics(t, fn, "expect form textarea to be created")
+ uassert.Equal(t, tc.markdown, markdown, "expected markdown to match")
+ })
+ }
+}