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") + }) + } +}