From f3a1a3997f7673e7a2eaa92f53a0826ffd29fef3 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Thu, 17 Nov 2022 22:28:30 +0000 Subject: [PATCH] chore: refactor blog Signed-off-by: Manfred Touron <94029+moul@users.noreply.github.com> --- examples/gno.land/p/demo/blog/blog.gno | 78 ++++++++++--------- examples/gno.land/p/demo/blog/util.gno | 7 ++ examples/gno.land/r/gnoland/blog/gnoblog.gno | 5 +- .../gno.land/r/gnoland/blog/gnoblog_test.gno | 42 +++------- gnoland/genesis/genesis_txs.txt | 2 + 5 files changed, 66 insertions(+), 68 deletions(-) create mode 100644 examples/gno.land/p/demo/blog/util.gno diff --git a/examples/gno.land/p/demo/blog/blog.gno b/examples/gno.land/p/demo/blog/blog.gno index a2b4b2ed0fa..4c352a1c1d4 100644 --- a/examples/gno.land/p/demo/blog/blog.gno +++ b/examples/gno.land/p/demo/blog/blog.gno @@ -12,6 +12,7 @@ import ( ) type Blog struct { + Title string Prefix string // i.e. r/gnoland/blog: Posts avl.MutTree // slug -> Post } @@ -25,11 +26,13 @@ func (b Blog) Render(path string) string { switch { case isHome: + output := breadcrumb([]string{b.Title}) + if b.Posts.Size() == 0 { - return "No posts." + output += "No posts." + return output } - output := "" b.Posts.Iterate("", "", func(n *avl.Tree) bool { post := n.Value().(*Post) output += post.RenderListItem() @@ -46,26 +49,52 @@ func (b Blog) Render(path string) string { if !found { return "404" } - typedPost := post.(*Post) - return typedPost.RenderPage() + p := post.(*Post) + + output := breadcrumb([]string{ + ufmt.Sprintf("[%s](%s)", b.Title, b.Prefix), + "p", + p.Title, + }) + + // output += ufmt.Sprintf("## [%s](%s)\n", p.Title, p.URL()) + output += p.Body + "\n\n" + output += p.RenderTagList() + "\n\n" + output += formatAuthorAndDate(p.Author, p.CreatedAt) + "\n" + output += "\n" + + // comments + p.Comments.IterateReverse("", "", func(n *avl.Tree) bool { + comment := n.Value().(*Comment) + output += comment.RenderListItem() + return false + }) + + return output case isViewTag: tagSlug := parts[1] - output := "" + output := breadcrumb([]string{ + ufmt.Sprintf("[%s](%s)", b.Title, b.Prefix), + "t", + tagSlug, + }) + + nb := 0 b.Posts.Iterate("", "", func(n *avl.Tree) bool { post := n.Value().(*Post) if !post.HasTag(tagSlug) { return false } output += post.RenderListItem() + nb++ return false }) - if output != "" { - return output + if nb == 0 { + output += "No posts." } - - return "No posts." + return output } return "404" @@ -181,35 +210,14 @@ func (p *Post) RenderListItem() string { return "error: no such post\n" } output := "" - output += ufmt.Sprintf("## [%s](%s)\n", p.Title, p.URL()) - output += p.Summary() + "\n\n" - output += p.RenderTagList() + "\n\n" - output += formatAuthorAndDate(p.Author, p.CreatedAt) + "\n" + output += ufmt.Sprintf("## [▸ %s](%s)\n", p.Title, p.URL()) + // output += p.Summary() + "\n\n" + // output += p.RenderTagList() + "\n\n" + // output += formatAuthorAndDate(p.Author, p.CreatedAt) + "\n" output += "\n" return output } -func (p *Post) RenderPage() string { - if p == nil { - return "error: no such post\n" - } - output := "" - output += ufmt.Sprintf("## [%s](%s)\n", p.Title, p.URL()) - output += p.Body + "\n\n" - output += p.RenderTagList() + "\n\n" - output += formatAuthorAndDate(p.Author, p.CreatedAt) + "\n" - output += "\n" - - // comments - p.Comments.IterateReverse("", "", func(n *avl.Tree) bool { - comment := n.Value().(*Comment) - output += comment.RenderListItem() - return false - }) - - return output -} - func (p *Post) RenderTagList() string { if p == nil { return "error: no such post\n" @@ -220,7 +228,7 @@ func (p *Post) RenderTagList() string { output += " " } tagURL := p.Blog.Prefix + "t/" + tag - output += ufmt.Sprintf("[%s](%s)", tag, tagURL) + output += ufmt.Sprintf("[#%s](%s)", tag, tagURL) } return output } diff --git a/examples/gno.land/p/demo/blog/util.gno b/examples/gno.land/p/demo/blog/util.gno new file mode 100644 index 00000000000..b4a8652af72 --- /dev/null +++ b/examples/gno.land/p/demo/blog/util.gno @@ -0,0 +1,7 @@ +package blog + +import "strings" + +func breadcrumb(parts []string) string { + return "# " + strings.Join(parts, " / ") + "\n\n" +} diff --git a/examples/gno.land/r/gnoland/blog/gnoblog.gno b/examples/gno.land/r/gnoland/blog/gnoblog.gno index d850725d614..2982ea88489 100644 --- a/examples/gno.land/r/gnoland/blog/gnoblog.gno +++ b/examples/gno.land/r/gnoland/blog/gnoblog.gno @@ -7,6 +7,7 @@ import ( ) var b = &blog.Blog{ + Title: "Gnoland's Blog", Prefix: "/r/gnoland/blog:", } @@ -20,7 +21,5 @@ func AddComment(postSlug, comment string) { } func Render(path string) string { - output := "# Gnoland's Blog\n\n" - output += b.Render(path) - return output + return b.Render(path) } diff --git a/examples/gno.land/r/gnoland/blog/gnoblog_test.gno b/examples/gno.land/r/gnoland/blog/gnoblog_test.gno index 624806bc974..46dfecd4494 100644 --- a/examples/gno.land/r/gnoland/blog/gnoblog_test.gno +++ b/examples/gno.land/r/gnoland/blog/gnoblog_test.gno @@ -30,19 +30,9 @@ No posts. expected := ` # Gnoland's Blog -## [title1](/r/gnoland/blog:p/slug1) -body1 +## [▸ title1](/r/gnoland/blog:p/slug1) -[tag1](/r/gnoland/blog:t/tag1) [tag2](/r/gnoland/blog:t/tag2) - -by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 2009-02-13 11:31pm UTC - -## [title2](/r/gnoland/blog:p/slug2) -body2 - -[tag1](/r/gnoland/blog:t/tag1) [tag3](/r/gnoland/blog:t/tag3) - -by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 2009-02-13 11:31pm UTC +## [▸ title2](/r/gnoland/blog:p/slug2) ` assertMDEquals(t, got, expected) } @@ -51,12 +41,11 @@ by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 2009-02-13 11:31pm UTC { got := Render("p/slug2") expected := ` -# Gnoland's Blog +# [Gnoland's Blog](/r/gnoland/blog:) / p / title2 -## [title2](/r/gnoland/blog:p/slug2) body2 -[tag1](/r/gnoland/blog:t/tag1) [tag3](/r/gnoland/blog:t/tag3) +[#tag1](/r/gnoland/blog:t/tag1) [#tag3](/r/gnoland/blog:t/tag3) by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 2009-02-13 11:31pm UTC ` @@ -66,19 +55,14 @@ by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 2009-02-13 11:31pm UTC // list by tags. { got := Render("t/invalid") - expected := "# Gnoland's Blog\n\nNo posts." + expected := "# [Gnoland's Blog](/r/gnoland/blog:) / t / invalid\n\nNo posts." assertMDEquals(t, got, expected) got = Render("t/tag2") expected = ` -# Gnoland's Blog - -## [title1](/r/gnoland/blog:p/slug1) -body1 - -[tag1](/r/gnoland/blog:t/tag1) [tag2](/r/gnoland/blog:t/tag2) +# [Gnoland's Blog](/r/gnoland/blog:) / t / tag2 -by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 2009-02-13 11:31pm UTC +## [▸ title1](/r/gnoland/blog:p/slug1) ` assertMDEquals(t, got, expected) } @@ -92,12 +76,11 @@ by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 2009-02-13 11:31pm UTC AddComment("slug1", "comment5") got := Render("p/slug2") expected := ` -# Gnoland's Blog +# [Gnoland's Blog](/r/gnoland/blog:) / p / title2 -## [title2](/r/gnoland/blog:p/slug2) body2 -[tag1](/r/gnoland/blog:t/tag1) [tag3](/r/gnoland/blog:t/tag3) +[#tag1](/r/gnoland/blog:t/tag1) [#tag3](/r/gnoland/blog:t/tag3) by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 2009-02-13 11:31pm UTC @@ -115,12 +98,11 @@ comment2 ModEditPost("slug2", "title2++", "body2++", "tag1,tag4") got := Render("p/slug2") expected := ` -# Gnoland's Blog +# [Gnoland's Blog](/r/gnoland/blog:) / p / title2++ -## [title2++](/r/gnoland/blog:p/slug2) body2++ -[tag1](/r/gnoland/blog:t/tag1) [tag4](/r/gnoland/blog:t/tag4) +[#tag1](/r/gnoland/blog:t/tag1) [#tag4](/r/gnoland/blog:t/tag4) by g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq on 2009-02-13 11:31pm UTC @@ -150,7 +132,7 @@ comment2 } for _, notFoundPath := range notFoundPaths { got := Render(notFoundPath) - expected := "# Gnoland's Blog\n\n404" + expected := "404" if got != expected { t.Errorf("path %q: expected %q, got %q.", notFoundPath, expected, got) } diff --git a/gnoland/genesis/genesis_txs.txt b/gnoland/genesis/genesis_txs.txt index e7944ad9089..33257380260 100644 --- a/gnoland/genesis/genesis_txs.txt +++ b/gnoland/genesis/genesis_txs.txt @@ -13,3 +13,5 @@ {"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","Simple echo example with coins","This is a simple test realm contract that demonstrates how to use the banker.\n\nSee [gno.land/r/demo/banktest/banktest.gno](/r/banktest/banktest.gno) to see the original contract code.\n\nThis article will go through each line to explain how it works.\n\n```go\npackage banktest\n```\n\nThis package is locally named \"banktest\" (could be anything).\n\n```go\nimport (\n\t\"std\"\n)\n```\n\nThe \"std\" package is defined by the gno code in stdlibs/std/. \u003c/br\u003e\nSelf explanatory; and you'll see more usage from std later.\n\n```go\ntype activity struct {\n\tcaller std.Address\n\tsent std.Coins\n\treturned std.Coins\n\ttime std.Time\n}\n\nfunc (act *activity) String() string {\n\treturn act.caller.String() + \" \" +\n\t\tact.sent.String() + \" sent, \" +\n\t\tact.returned.String() + \" returned, at \" +\n\t\tstd.FormatTimestamp(act.time, \"2006-01-02 3:04pm MST\")\n}\n\nvar latest [10]*activity\n```\n\nThis is just maintaining a list of recent activity to this contract.\nNotice that the \"latest\" variable is defined \"globally\" within\nthe context of the realm with path \"gno.land/r/demo/banktest\".\n\nThis means that calls to functions defined within this package\nare encapsulated within this \"data realm\", where the data is \nmutated based on transactions that can potentially cross many\nrealm and non-realm packge boundaries (in the call stack).\n\n```go\n// Deposit will take the coins (to the realm's pkgaddr) or return them to user.\nfunc Deposit(returnDenom string, returnAmount int64) string {\n\tstd.AssertOriginCall()\n\tcaller := std.GetOrigCaller()\n\tsend := std.Coins{{returnDenom, returnAmount}}\n```\n\nThis is the beginning of the definition of the contract function named\n\"Deposit\". `std.AssertOriginCall() asserts that this function was called by a\ngno transactional Message. The caller is the user who signed off on this\ntransactional message. Send is the amount of deposit sent along with this\nmessage.\n\n```go\n\t// record activity\n\tact := \u0026activity{\n\t\tcaller: caller,\n\t\tsent: std.GetOrigSend(),\n\t\treturned: send,\n\t\ttime: std.GetTimestamp(),\n\t}\n\tfor i := len(latest) - 2; i \u003e= 0; i-- {\n\t\tlatest[i+1] = latest[i] // shift by +1.\n\t}\n\tlatest[0] = act\n```\n\nUpdating the \"latest\" array for viewing at gno.land/r/demo/banktest: (w/ trailing colon).\n\n```go\n\t// return if any.\n\tif returnAmount \u003e 0 {\n```\n\nIf the user requested the return of coins...\n\n```go\n\t\tbanker := std.GetBanker(std.BankerTypeOrigSend)\n```\n\nuse a std.Banker instance to return any deposited coins to the original sender.\n\n```go\n\t\tpkgaddr := std.GetOrigPkgAddr()\n\t\t// TODO: use std.Coins constructors, this isn't generally safe.\n\t\tbanker.SendCoins(pkgaddr, caller, send)\n\t\treturn \"returned!\"\n```\n\nNotice that each realm package has an associated Cosmos address.\n\n\nFinally, the results are rendered via an ABCI query call when you visit [/r/banktest:](/r/banktest:).\n\n```go\nfunc Render(path string) string {\n\t// get realm coins.\n\tbanker := std.GetBanker(std.BankerTypeReadonly)\n\tcoins := banker.GetCoins(std.GetOrigPkgAddr())\n\n\t// render\n\tres := \"\"\n\tres += \"## recent activity\\n\"\n\tres += \"\\n\"\n\tfor _, act := range latest {\n\t\tif act == nil {\n\t\t\tbreak\n\t\t}\n\t\tres += \" * \" + act.String() + \"\\n\"\n\t}\n\tres += \"\\n\"\n\tres += \"## total deposits\\n\"\n\tres += coins.String()\n\treturn res\n}\n```\n\nYou can call this contract yourself, by vistiing [/r/banktest](/r/banktest) and the [quickstart guide](/r/boards:testboard/4).\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"A+FhNtsXHjLfSJk1lB8FbiL4mGPjc50Kt81J7EKDnJ2y"},"signature":"iZX/llZlNTdZMLv1goCTgK2bWqzT8enlTq56wMTCpVxJGA0BTvuEM5Nnt9vrnlG6Taqj2GuTrmEnJBkDFTmt9g=="}],"memo":""} {"msg":[{"@type":"/vm.m_call","caller":"g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","TASK: Describe in your words","Describe in an essay (250+ words), on your favorite medium, why you are interested in gno.land and gnolang.\n\nReply here with a URL link to your written piece as a comment, for rewards.\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AmG6kzznyo1uNqWPAYU6wDpsmzQKDaEOrVRaZ08vOyX0"},"signature":"4HBNtrta8HdeHj4JTN56PBTRK8GOe31NMRRXDiyYtjozuyRdWfOGEsGjGgHWcoBUJq6DepBgD4FetdqfhZ6TNQ=="}],"memo":""} {"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","send":"","pkg_path":"gno.land/r/demo/boards","func":"CreateThread","args":["1","Getting Started","This is a demo of Gno smart contract programming. This document was\nconstructed by Gno onto a smart contract hosted on the data Realm\nname [\"gno.land/r/demo/boards\"](https://gno.land/r/demo/boards/)\n([github](https://github.com/gnolang/gno/tree/master/examples/gno.land/r/demo/boards)).\n\n\n\n## Build `gnokey`, create your account, and interact with Gno.\n\nNOTE: Where you see `--remote %%REMOTE%%` here, that flag can be replaced\nwith `--remote localhost:26657` for local testnets.\n\n### Build `gnokey`.\n\n```bash\ngit clone git@github.com:gnolang/gno.git\ncd ./gno\nmake\n```\n\n### Generate a seed/mnemonic code.\n\n```bash\n./build/gnokey generate\n```\n\nNOTE: You can generate 24 words with any good bip39 generator.\n\n### Create a new account using your mnemonic.\n\n```bash\n./build/gnokey add KEYNAME --recover\n```\n\nNOTE: `KEYNAME` is your key identifier, and should be changed.\n\n### Verify that you can see your account locally.\n\n```bash\n./build/gnokey list\n```\n\n## Interact with the blockchain:\n\n### Get your current balance, account number, and sequence number.\n\n```bash\n./build/gnokey query auth/accounts/ACCOUNT_ADDR --remote %%REMOTE%%\n```\n\nNOTE: you can retrieve your `ACCOUNT_ADDR` with `./build/gnokey list`.\n\n### Acquire testnet tokens using the official faucet.\n\nGo to https://gno.land/faucet\n\n### Create a board with a smart contract call.\n\nNOTE: `BOARDNAME` will be the slug of the board, and should be changed.\n\n```bash\n./build/gnokey maketx call KEYNAME --pkgpath \"gno.land/r/demo/boards\" --func \"CreateBoard\" --args \"BOARDNAME\" --gas-fee \"1000000ugnot\" --gas-wanted \"2000000\" --broadcast true --chainid %%CHAINID%% --remote %%REMOTE%%\n```\n\nInteractive documentation: https://gno.land/r/demo/boards?help\u0026__func=CreateBoard\n\nNext, query for the permanent board ID by querying (you need this to create a new post):\n\n```bash\n./build/gnokey query \"vm/qeval\" --data \"gno.land/r/demo/boards\nGetBoardIDFromName(\\\"BOARDNAME\\\")\" --remote %%REMOTE%%\n```\n\n### Create a post of a board with a smart contract call.\n\nNOTE: If a board was created successfully, your SEQUENCE_NUMBER would have increased.\n\n```bash\n./build/gnokey maketx call KEYNAME --pkgpath \"gno.land/r/demo/boards\" --func \"CreateThread\" --args BOARD_ID --args \"Hello gno.land\" --args\\#file \"./examples/gno.land/r/demo/boards/example_post.md\" --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast true --chainid %%CHAINID%% --remote %%REMOTE%%\n```\n\nInteractive documentation: https://gno.land/r/demo/boards?help\u0026__func=CreateThread\n\n### Create a comment to a post.\n\n```bash\n./build/gnokey maketx call KEYNAME --pkgpath \"gno.land/r/demo/boards\" --func \"CreateReply\" --args \"BOARD_ID\" --args \"1\" --args \"1\" --args \"Nice to meet you too.\" --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast true --chainid %%CHAINID%% --remote %%REMOTE%%\n```\n\nInteractive documentation: https://gno.land/r/demo/boards?help\u0026__func=CreateReply\n\n```bash\n./build/gnokey query \"vm/qrender\" --data \"gno.land/r/demo/boards\nBOARDNAME/1\" --remote %%REMOTE%%\n```\n\n### Render page with optional path expression.\n\nThe contents of `https://gno.land/r/demo/boards:` and `https://gno.land/r/demo/boards:gnolang` are rendered by calling\nthe `Render(path string)` function like so:\n\n```bash\n./build/gnokey query \"vm/qrender\" --data \"gno.land/r/demo/boards\ngnolang\"\n```\n\n## Starting a local `gnoland` node:\n\n### Add test account.\n\n```bash\n./build/gnokey add test1 --recover\n```\n\nUse this mneonic:\n\u003e source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast\n\n### Start `gnoland` node.\n\n```bash\n./build/gnoland\n```\n\nNOTE: This can be reset with `make reset`\n\n### Publish the \"gno.land/p/demo/avl\" package.\n\n```bash\n./build/gnokey maketx addpkg test1 --pkgpath \"gno.land/p/demo/avl\" --pkgdir \"examples/gno.land/p/demo/avl\" --deposit 100000000ugnot --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast true --chainid %%CHAINID%% --remote localhost:26657\n```\n\n### Publish the \"gno.land/r/demo/boards\" realm package.\n\n```bash\n./build/gnokey maketx addpkg test1 --pkgpath \"gno.land/r/demo/boards\" --pkgdir \"examples/gno.land/r/demo/boards\" --deposit 100000000ugnot --gas-fee 1000000ugnot --gas-wanted 300000000 --broadcast true --chainid %%CHAINID%% --remote localhost:26657\n```\n"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"sHjOGXZEi9wt2FSXFHmkDDoVQyepvFHKRDDU0zgedHYnCYPx5/YndyihsDD5Y2Z7/RgNYBh4JlJwDMGFNStzBQ=="}],"memo":""} +{"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","send":"","pkg_path":"gno.land/r/gnoland/blog","func":"ModAddPost","args":["post1","First post","Lorem Ipsum", "tag1,tag2"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"sHjOGXZEi9wt2FSXFHmkDDoVQyepvFHKRDDU0zgedHYnCYPx5/YndyihsDD5Y2Z7/RgNYBh4JlJwDMGFNStzBQ=="}],"memo":""} +{"msg":[{"@type":"/vm.m_call","caller":"g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq","send":"","pkg_path":"gno.land/r/gnoland/blog","func":"ModAddPost","args":["post2","Second post","Lorem Ipsum", "tag1,tag3"]}],"fee":{"gas_wanted":"2000000","gas_fee":"1000000ugnot"},"signatures":[{"pub_key":{"@type":"/tm.PubKeySecp256k1","value":"AnK+a6mcFDjY6b/v6p7r8QFW1M1PgIoQxBgrwOoyY7v3"},"signature":"sHjOGXZEi9wt2FSXFHmkDDoVQyepvFHKRDDU0zgedHYnCYPx5/YndyihsDD5Y2Z7/RgNYBh4JlJwDMGFNStzBQ=="}],"memo":""}