Skip to content

Commit

Permalink
feat: add p/moul/txlink + p/moul/helplink (#2887)
Browse files Browse the repository at this point in the history
This PR aimed to promote the use of a `p/` library for managing special
help links from contracts.

It also provided an opportunity for me to realize that our discussion
about changing the `$` symbol would require some parsing and detection
from the `gnoweb` perspective. If we want a simple library like this
one, the goal should be to ideally craft a link to the current package
without specifying the realm path. Relative URLs worked well with `?`,
but they won't function with `$`.

As an alternative, we can have this package look for
`std.PrevRealm().PkgAddr` if it is not specified.

cc @jeronimoalbi @thehowl @leohhhn 

Related with #2602
Related with #2876

---------

Signed-off-by: moul <[email protected]>
Co-authored-by: Leon Hudak <[email protected]>
  • Loading branch information
moul and leohhhn authored Oct 25, 2024
1 parent b849b5a commit 49e718c
Show file tree
Hide file tree
Showing 24 changed files with 327 additions and 50 deletions.
6 changes: 6 additions & 0 deletions examples/gno.land/p/moul/helplink/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module gno.land/p/moul/helplink

require (
gno.land/p/demo/urequire v0.0.0-latest
gno.land/p/moul/txlink v0.0.0-latest
)
79 changes: 79 additions & 0 deletions examples/gno.land/p/moul/helplink/helplink.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Package helplink provides utilities for creating help page links compatible
// with Gnoweb, Gnobro, and other clients that support the Gno contracts'
// flavored Markdown format.
//
// This package simplifies the generation of dynamic, context-sensitive help
// links, enabling users to navigate relevant documentation seamlessly within
// the Gno ecosystem.
//
// For a more lightweight alternative, consider using p/moul/txlink.
//
// The primary functions — Func, FuncURL, and Home — are intended for use with
// the "relative realm". When specifying a custom Realm, you can create links
// that utilize either the current realm path or a fully qualified path to
// another realm.
package helplink

import (
"strings"

"gno.land/p/moul/txlink"
)

const chainDomain = "gno.land" // XXX: std.ChainDomain (#2911)

// Func returns a markdown link for the specific function with optional
// key-value arguments, for the current realm.
func Func(title string, fn string, args ...string) string {
return Realm("").Func(title, fn, args...)
}

// FuncURL returns a URL for the specified function with optional key-value
// arguments, for the current realm.
func FuncURL(fn string, args ...string) string {
return Realm("").FuncURL(fn, args...)
}

// Home returns the URL for the help homepage of the current realm.
func Home() string {
return Realm("").Home()
}

// Realm represents a specific realm for generating help links.
type Realm string

// prefix returns the URL prefix for the realm.
func (r Realm) prefix() string {
// relative
if r == "" {
return ""
}

// local realm -> /realm
realm := string(r)
if strings.Contains(realm, chainDomain) {
return strings.TrimPrefix(realm, chainDomain)
}

// remote realm -> https://remote.land/realm
return "https://" + string(r)
}

// Func returns a markdown link for the specified function with optional
// key-value arguments.
func (r Realm) Func(title string, fn string, args ...string) string {
// XXX: escape title
return "[" + title + "](" + r.FuncURL(fn, args...) + ")"
}

// FuncURL returns a URL for the specified function with optional key-value
// arguments.
func (r Realm) FuncURL(fn string, args ...string) string {
tlr := txlink.Realm(r)
return tlr.URL(fn, args...)
}

// Home returns the base help URL for the specified realm.
func (r Realm) Home() string {
return r.prefix() + "?help"
}
78 changes: 78 additions & 0 deletions examples/gno.land/p/moul/helplink/helplink_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package helplink

import (
"testing"

"gno.land/p/demo/urequire"
)

func TestFunc(t *testing.T) {
tests := []struct {
title string
fn string
args []string
want string
realm Realm
}{
{"Example", "foo", []string{"bar", "1", "baz", "2"}, "[Example](?help&__func=foo&bar=1&baz=2)", ""},
{"Realm Example", "foo", []string{"bar", "1", "baz", "2"}, "[Realm Example](/r/lorem/ipsum?help&__func=foo&bar=1&baz=2)", "gno.land/r/lorem/ipsum"},
{"Single Arg", "testFunc", []string{"key", "value"}, "[Single Arg](?help&__func=testFunc&key=value)", ""},
{"No Args", "noArgsFunc", []string{}, "[No Args](?help&__func=noArgsFunc)", ""},
{"Odd Args", "oddArgsFunc", []string{"key"}, "[Odd Args](?help&__func=oddArgsFunc)", ""},
}

for _, tt := range tests {
t.Run(tt.title, func(t *testing.T) {
got := tt.realm.Func(tt.title, tt.fn, tt.args...)
urequire.Equal(t, tt.want, got)
})
}
}

func TestFuncURL(t *testing.T) {
tests := []struct {
fn string
args []string
want string
realm Realm
}{
{"foo", []string{"bar", "1", "baz", "2"}, "?help&__func=foo&bar=1&baz=2", ""},
{"testFunc", []string{"key", "value"}, "?help&__func=testFunc&key=value", ""},
{"noArgsFunc", []string{}, "?help&__func=noArgsFunc", ""},
{"oddArgsFunc", []string{"key"}, "?help&__func=oddArgsFunc", ""},
{"foo", []string{"bar", "1", "baz", "2"}, "/r/lorem/ipsum?help&__func=foo&bar=1&baz=2", "gno.land/r/lorem/ipsum"},
{"testFunc", []string{"key", "value"}, "/r/lorem/ipsum?help&__func=testFunc&key=value", "gno.land/r/lorem/ipsum"},
{"noArgsFunc", []string{}, "/r/lorem/ipsum?help&__func=noArgsFunc", "gno.land/r/lorem/ipsum"},
{"oddArgsFunc", []string{"key"}, "/r/lorem/ipsum?help&__func=oddArgsFunc", "gno.land/r/lorem/ipsum"},
{"foo", []string{"bar", "1", "baz", "2"}, "https://gno.world/r/lorem/ipsum?help&__func=foo&bar=1&baz=2", "gno.world/r/lorem/ipsum"},
{"testFunc", []string{"key", "value"}, "https://gno.world/r/lorem/ipsum?help&__func=testFunc&key=value", "gno.world/r/lorem/ipsum"},
{"noArgsFunc", []string{}, "https://gno.world/r/lorem/ipsum?help&__func=noArgsFunc", "gno.world/r/lorem/ipsum"},
{"oddArgsFunc", []string{"key"}, "https://gno.world/r/lorem/ipsum?help&__func=oddArgsFunc", "gno.world/r/lorem/ipsum"},
}

for _, tt := range tests {
title := tt.fn
t.Run(title, func(t *testing.T) {
got := tt.realm.FuncURL(tt.fn, tt.args...)
urequire.Equal(t, tt.want, got)
})
}
}

func TestHome(t *testing.T) {
tests := []struct {
realm Realm
want string
}{
{"", "?help"},
{"gno.land/r/lorem/ipsum", "/r/lorem/ipsum?help"},
{"gno.world/r/lorem/ipsum", "https://gno.world/r/lorem/ipsum?help"},
}

for _, tt := range tests {
t.Run(string(tt.realm), func(t *testing.T) {
got := tt.realm.Home()
urequire.Equal(t, tt.want, got)
})
}
}
3 changes: 3 additions & 0 deletions examples/gno.land/p/moul/txlink/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module gno.land/p/moul/txlink

require gno.land/p/demo/urequire v0.0.0-latest
74 changes: 74 additions & 0 deletions examples/gno.land/p/moul/txlink/txlink.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Package txlink provides utilities for creating transaction-related links
// compatible with Gnoweb, Gnobro, and other clients within the Gno ecosystem.
//
// This package is optimized for generating lightweight transaction links with
// flexible arguments, allowing users to build dynamic links that integrate
// seamlessly with various Gno clients.
//
// The primary function, URL, is designed to produce markdown links for
// transaction functions in the current "relative realm". By specifying a custom
// Realm, you can generate links that either use the current realm path or a
// fully qualified path for another realm.
//
// This package is a streamlined alternative to helplink, providing similar
// functionality for transaction links without the full feature set of helplink.
package txlink

import (
"std"
"strings"
)

const chainDomain = "gno.land" // XXX: std.ChainDomain (#2911)

// URL returns a URL for the specified function with optional key-value
// arguments, for the current realm.
func URL(fn string, args ...string) string {
return Realm("").URL(fn, args...)
}

// Realm represents a specific realm for generating tx links.
type Realm string

// prefix returns the URL prefix for the realm.
func (r Realm) prefix() string {
// relative
if r == "" {
curPath := std.CurrentRealm().PkgPath()
return strings.TrimPrefix(curPath, chainDomain)
}

// local realm -> /realm
realm := string(r)
if strings.Contains(realm, chainDomain) {
return strings.TrimPrefix(realm, chainDomain)
}

// remote realm -> https://remote.land/realm
return "https://" + string(r)
}

// URL returns a URL for the specified function with optional key-value
// arguments.
func (r Realm) URL(fn string, args ...string) string {
// Start with the base query
url := r.prefix() + "?help&__func=" + fn

// Check if args length is even
if len(args)%2 != 0 {
// If not even, we can choose to handle the error here.
// For example, we can just return the URL without appending
// more args.
return url
}

// Append key-value pairs to the URL
for i := 0; i < len(args); i += 2 {
key := args[i]
value := args[i+1]
// XXX: escape keys and args
url += "&" + key + "=" + value
}

return url
}
37 changes: 37 additions & 0 deletions examples/gno.land/p/moul/txlink/txlink_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package txlink

import (
"testing"

"gno.land/p/demo/urequire"
)

func TestURL(t *testing.T) {
tests := []struct {
fn string
args []string
want string
realm Realm
}{
{"foo", []string{"bar", "1", "baz", "2"}, "?help&__func=foo&bar=1&baz=2", ""},
{"testFunc", []string{"key", "value"}, "?help&__func=testFunc&key=value", ""},
{"noArgsFunc", []string{}, "?help&__func=noArgsFunc", ""},
{"oddArgsFunc", []string{"key"}, "?help&__func=oddArgsFunc", ""},
{"foo", []string{"bar", "1", "baz", "2"}, "/r/lorem/ipsum?help&__func=foo&bar=1&baz=2", "gno.land/r/lorem/ipsum"},
{"testFunc", []string{"key", "value"}, "/r/lorem/ipsum?help&__func=testFunc&key=value", "gno.land/r/lorem/ipsum"},
{"noArgsFunc", []string{}, "/r/lorem/ipsum?help&__func=noArgsFunc", "gno.land/r/lorem/ipsum"},
{"oddArgsFunc", []string{"key"}, "/r/lorem/ipsum?help&__func=oddArgsFunc", "gno.land/r/lorem/ipsum"},
{"foo", []string{"bar", "1", "baz", "2"}, "https://gno.world/r/lorem/ipsum?help&__func=foo&bar=1&baz=2", "gno.world/r/lorem/ipsum"},
{"testFunc", []string{"key", "value"}, "https://gno.world/r/lorem/ipsum?help&__func=testFunc&key=value", "gno.world/r/lorem/ipsum"},
{"noArgsFunc", []string{}, "https://gno.world/r/lorem/ipsum?help&__func=noArgsFunc", "gno.world/r/lorem/ipsum"},
{"oddArgsFunc", []string{"key"}, "https://gno.world/r/lorem/ipsum?help&__func=oddArgsFunc", "gno.world/r/lorem/ipsum"},
}

for _, tt := range tests {
title := tt.fn
t.Run(title, func(t *testing.T) {
got := tt.realm.URL(tt.fn, tt.args...)
urequire.Equal(t, tt.want, got)
})
}
}
5 changes: 2 additions & 3 deletions examples/gno.land/r/demo/boards/board.gno
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"time"

"gno.land/p/demo/avl"
"gno.land/p/moul/txlink"
)

//----------------------------------------
Expand Down Expand Up @@ -134,7 +135,5 @@ func (board *Board) GetURLFromThreadAndReplyID(threadID, replyID PostID) string
}

func (board *Board) GetPostFormURL() string {
return "/r/demo/boards?help&__func=CreateThread" +
"&bid=" + board.id.String() +
"&body.type=textarea"
return txlink.URL("CreateThread", "bid", board.id.String())
}
1 change: 1 addition & 0 deletions examples/gno.land/r/demo/boards/gno.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ module gno.land/r/demo/boards

require (
gno.land/p/demo/avl v0.0.0-latest
gno.land/p/moul/txlink v0.0.0-latest
gno.land/r/demo/users v0.0.0-latest
)
30 changes: 15 additions & 15 deletions examples/gno.land/r/demo/boards/post.gno
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"time"

"gno.land/p/demo/avl"
"gno.land/p/moul/txlink"
)

//----------------------------------------
Expand Down Expand Up @@ -155,27 +156,26 @@ func (post *Post) GetURL() string {
}

func (post *Post) GetReplyFormURL() string {
return "/r/demo/boards?help&__func=CreateReply" +
"&bid=" + post.board.id.String() +
"&threadid=" + post.threadID.String() +
"&postid=" + post.id.String() +
"&body.type=textarea"
return txlink.URL("CreateReply",
"bid", post.board.id.String(),
"threadid", post.threadID.String(),
"postid", post.id.String(),
)
}

func (post *Post) GetRepostFormURL() string {
return "/r/demo/boards?help&__func=CreateRepost" +
"&bid=" + post.board.id.String() +
"&postid=" + post.id.String() +
"&title.type=textarea" +
"&body.type=textarea" +
"&dstBoardID.type=textarea"
return txlink.URL("CreateRepost",
"bid", post.board.id.String(),
"postid", post.id.String(),
)
}

func (post *Post) GetDeleteFormURL() string {
return "/r/demo/boards?help&__func=DeletePost" +
"&bid=" + post.board.id.String() +
"&threadid=" + post.threadID.String() +
"&postid=" + post.id.String()
return txlink.URL("DeletePost",
"bid", post.board.id.String(),
"threadid", post.threadID.String(),
"postid", post.id.String(),
)
}

func (post *Post) RenderSummary() string {
Expand Down
2 changes: 1 addition & 1 deletion examples/gno.land/r/demo/boards/z_0_filetest.gno
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func main() {
}

// Output:
// \[[post](/r/demo/boards?help&__func=CreateThread&bid=1&body.type=textarea)]
// \[[post](/r/demo/boards?help&__func=CreateThread&bid=1)]
//
// ----------------------------------------
// ## [First Post (title)](/r/demo/boards:test_board/1)
Expand Down
6 changes: 3 additions & 3 deletions examples/gno.land/r/demo/boards/z_10_c_filetest.gno
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,14 @@ func main() {
// # First Post in (title)
//
// Body of the first post. (body)
// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=1&threadid=1&postid=1&body.type=textarea)] \[[repost](/r/demo/boards?help&__func=CreateRepost&bid=1&postid=1&title.type=textarea&body.type=textarea&dstBoardID.type=textarea)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=1&postid=1)]
// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards?help&__func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=1&postid=1)]
//
// > First reply of the First post
// >
// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=1&threadid=1&postid=2&body.type=textarea)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=1&postid=2)]
// > \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1/2) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=1&threadid=1&postid=2)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=1&postid=2)]
//
// ----------------------------------------------------
// # First Post in (title)
//
// Body of the first post. (body)
// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=1&threadid=1&postid=1&body.type=textarea)] \[[repost](/r/demo/boards?help&__func=CreateRepost&bid=1&postid=1&title.type=textarea&body.type=textarea&dstBoardID.type=textarea)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=1&postid=1)]
// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards?help&__func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=1&postid=1)]
2 changes: 1 addition & 1 deletion examples/gno.land/r/demo/boards/z_10_filetest.gno
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func main() {
// # First Post in (title)
//
// Body of the first post. (body)
// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=1&threadid=1&postid=1&body.type=textarea)] \[[repost](/r/demo/boards?help&__func=CreateRepost&bid=1&postid=1&title.type=textarea&body.type=textarea&dstBoardID.type=textarea)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=1&postid=1)]
// \- [@gnouser](/r/demo/users:gnouser), [2009-02-13 11:31pm (UTC)](/r/demo/boards:test_board/1) \[[reply](/r/demo/boards?help&__func=CreateReply&bid=1&threadid=1&postid=1)] \[[repost](/r/demo/boards?help&__func=CreateRepost&bid=1&postid=1)] \[[x](/r/demo/boards?help&__func=DeletePost&bid=1&threadid=1&postid=1)]
//
// ----------------------------------------------------
// thread does not exist with id: 1
Loading

0 comments on commit 49e718c

Please sign in to comment.