diff --git a/examples/gno.land/p/demo/fqname/fqname.gno b/examples/gno.land/p/demo/fqname/fqname.gno new file mode 100644 index 00000000000..d28453e5c1b --- /dev/null +++ b/examples/gno.land/p/demo/fqname/fqname.gno @@ -0,0 +1,72 @@ +// Package fqname provides utilities for handling fully qualified identifiers in +// Gno. A fully qualified identifier typically includes a package path followed +// by a dot (.) and then the name of a variable, function, type, or other +// package-level declaration. +package fqname + +import "strings" + +// Parse splits a fully qualified identifier into its package path and name +// components. It handles cases with and without slashes in the package path. +// +// pkgpath, name := fqname.Parse("gno.land/p/demo/avl.Tree") +// ufmt.Sprintf("Package: %s, Name: %s\n", id.Package, id.Name) +// // Output: Package: gno.land/p/demo/avl, Name: Tree +func Parse(fqname string) (pkgpath, name string) { + // Find the index of the last slash. + lastSlashIndex := strings.LastIndex(fqname, "/") + if lastSlashIndex == -1 { + // No slash found, handle it as a simple package name with dot notation. + dotIndex := strings.LastIndex(fqname, ".") + if dotIndex == -1 { + return fqname, "" + } + return fqname[:dotIndex], fqname[dotIndex+1:] + } + + // Get the part after the last slash. + afterSlash := fqname[lastSlashIndex+1:] + + // Check for a dot in the substring after the last slash. + dotIndex := strings.Index(afterSlash, ".") + if dotIndex == -1 { + // No dot found after the last slash + return fqname, "" + } + + // Split at the dot to separate the base and the suffix. + base := fqname[:lastSlashIndex+1+dotIndex] + suffix := afterSlash[dotIndex+1:] + + return base, suffix +} + +// Construct a qualified identifier. +// +// fqName := fqname.Construct("gno.land/r/demo/foo20", "GRC20") +// fmt.Println("Fully Qualified Name:", fqName) +// // Output: gno.land/r/demo/foo20.GRC20 +func Construct(pkgpath, name string) string { + // TODO: ensure pkgpath is valid - and as such last part does not contain a dot. + if name == "" { + return pkgpath + } + return pkgpath + "." + name +} + +// RenderLink creates a formatted link for a fully qualified identifier. +// If the package path starts with "gno.land", it converts it to a markdown link. +// If the domain is different or missing, it returns the input as is. +func RenderLink(pkgPath, slug string) string { + if strings.HasPrefix(pkgPath, "gno.land") { + pkgLink := strings.TrimPrefix(pkgPath, "gno.land") + if slug != "" { + return "[" + pkgPath + "](" + pkgLink + ")." + slug + } + return "[" + pkgPath + "](" + pkgLink + ")" + } + if slug != "" { + return pkgPath + "." + slug + } + return pkgPath +} diff --git a/examples/gno.land/p/demo/fqname/fqname_test.gno b/examples/gno.land/p/demo/fqname/fqname_test.gno new file mode 100644 index 00000000000..55a220776be --- /dev/null +++ b/examples/gno.land/p/demo/fqname/fqname_test.gno @@ -0,0 +1,74 @@ +package fqname + +import ( + "testing" + + "gno.land/p/demo/uassert" +) + +func TestParse(t *testing.T) { + tests := []struct { + input string + expectedPkgPath string + expectedName string + }{ + {"gno.land/p/demo/avl.Tree", "gno.land/p/demo/avl", "Tree"}, + {"gno.land/p/demo/avl", "gno.land/p/demo/avl", ""}, + {"gno.land/p/demo/avl.Tree.Node", "gno.land/p/demo/avl", "Tree.Node"}, + {"gno.land/p/demo/avl/nested.Package.Func", "gno.land/p/demo/avl/nested", "Package.Func"}, + {"path/filepath.Split", "path/filepath", "Split"}, + {"path.Split", "path", "Split"}, + {"path/filepath", "path/filepath", ""}, + {"path", "path", ""}, + {"", "", ""}, + } + + for _, tt := range tests { + pkgpath, name := Parse(tt.input) + uassert.Equal(t, tt.expectedPkgPath, pkgpath, "Package path did not match") + uassert.Equal(t, tt.expectedName, name, "Name did not match") + } +} + +func TestConstruct(t *testing.T) { + tests := []struct { + pkgpath string + name string + expected string + }{ + {"gno.land/r/demo/foo20", "GRC20", "gno.land/r/demo/foo20.GRC20"}, + {"gno.land/r/demo/foo20", "", "gno.land/r/demo/foo20"}, + {"path", "", "path"}, + {"path", "Split", "path.Split"}, + {"path/filepath", "", "path/filepath"}, + {"path/filepath", "Split", "path/filepath.Split"}, + {"", "JustName", ".JustName"}, + {"", "", ""}, + } + + for _, tt := range tests { + result := Construct(tt.pkgpath, tt.name) + uassert.Equal(t, tt.expected, result, "Constructed FQName did not match expected") + } +} + +func TestRenderLink(t *testing.T) { + tests := []struct { + pkgPath string + slug string + expected string + }{ + {"gno.land/p/demo/avl", "Tree", "[gno.land/p/demo/avl](/p/demo/avl).Tree"}, + {"gno.land/p/demo/avl", "", "[gno.land/p/demo/avl](/p/demo/avl)"}, + {"github.com/a/b", "C", "github.com/a/b.C"}, + {"example.com/pkg", "Func", "example.com/pkg.Func"}, + {"gno.land/r/demo/foo20", "GRC20", "[gno.land/r/demo/foo20](/r/demo/foo20).GRC20"}, + {"gno.land/r/demo/foo20", "", "[gno.land/r/demo/foo20](/r/demo/foo20)"}, + {"", "", ""}, + } + + for _, tt := range tests { + result := RenderLink(tt.pkgPath, tt.slug) + uassert.Equal(t, tt.expected, result, "Rendered link did not match expected") + } +} diff --git a/examples/gno.land/p/demo/fqname/gno.mod b/examples/gno.land/p/demo/fqname/gno.mod new file mode 100644 index 00000000000..1282e262303 --- /dev/null +++ b/examples/gno.land/p/demo/fqname/gno.mod @@ -0,0 +1,3 @@ +module gno.land/p/demo/fqname + +require gno.land/p/demo/uassert v0.0.0-latest