form.gno
3.24 Kb · 96 lines
1package toolbox
2
3import "strings"
4
5// Query builds the gnoweb form a read-only tool needs: a text box whose
6// submission navigates to one of the realm's own Render paths, with the typed
7// values as query parameters.
8//
9// This is the "template of a query" a toolbox page is for. Rendered by gnoweb,
10//
11// NewQuery("lookup").Field("q", "username or g1 address").String()
12//
13// posts to the realm and redirects to `/r/moul/toolbox/usernames:lookup?q=moul`
14// (`gno.land/pkg/gnoweb/handler_http.go`, `Post`: the `path` attribute travels
15// as the hidden `__gno_path` field and becomes the render args, every other
16// field becomes a query parameter). Nothing is signed and no transaction is
17// built: the result is a `Render` call, which is exactly what
18// `gnokey query vm/qrender` would have done from a terminal.
19//
20// A form whose fields should CALL a function instead is a different element,
21// `<gno-form exec="Fn">`, and it is not what this type builds. A query form is
22// free and a call is not, so the two are deliberately not one constructor with
23// a flag.
24type Query struct {
25 path string
26 fields []queryField
27}
28
29type queryField struct {
30 name string
31 placeholder string
32}
33
34// NewQuery starts a form submitting to the Render path args.
35//
36// Pass "" to submit to the realm's root page. Do not include a leading colon
37// or the realm path itself: gnoweb supplies both, and a value that looks like
38// a URL is percent-encoded into one path element rather than followed.
39func NewQuery(path string) *Query {
40 return &Query{path: path}
41}
42
43// Field adds a text input. name is the query parameter the realm will read;
44// placeholder is the grey text in the empty box.
45//
46// A field with an empty name is ignored: gnoweb rejects the whole form for it,
47// and losing one input beats losing the page.
48func (q *Query) Field(name, placeholder string) *Query {
49 if name == "" {
50 return q
51 }
52 q.fields = append(q.fields, queryField{name: name, placeholder: placeholder})
53 return q
54}
55
56// Len is how many fields the form carries.
57func (q *Query) Len() int { return len(q.fields) }
58
59// String renders the form as the markdown-embedded HTML gnoweb parses.
60//
61// A form with no field renders as the empty string rather than as a submit
62// button with nothing to submit.
63func (q *Query) String() string {
64 if len(q.fields) == 0 {
65 return ""
66 }
67 var b strings.Builder
68 b.WriteString(`<gno-form`)
69 if q.path != "" {
70 b.WriteString(` path="` + attr(q.path) + `"`)
71 }
72 b.WriteString(">\n")
73 for _, f := range q.fields {
74 b.WriteString(`<gno-input name="` + attr(f.name) + `" type="text"`)
75 if f.placeholder != "" {
76 b.WriteString(` placeholder="` + attr(f.placeholder) + `"`)
77 }
78 b.WriteString(" />\n")
79 }
80 b.WriteString("</gno-form>\n")
81 return b.String()
82}
83
84// attr escapes a value for an HTML attribute in double quotes.
85//
86// gnoweb parses these with a real HTML tokenizer, so an unescaped quote does
87// not merely look wrong: it ends the attribute and whatever follows becomes
88// further attributes of the same tag. Ampersand first, or the escapes of the
89// later replacements would themselves be escaped.
90func attr(s string) string {
91 s = strings.ReplaceAll(s, "&", "&")
92 s = strings.ReplaceAll(s, `"`, """)
93 s = strings.ReplaceAll(s, "<", "<")
94 s = strings.ReplaceAll(s, ">", ">")
95 return s
96}