From 56df907b05d4c989e70254daf5679a689e82afbe Mon Sep 17 00:00:00 2001 From: hthieu1110 Date: Mon, 21 Aug 2023 09:31:27 +0700 Subject: [PATCH] chore: add daodao pkg --- .../p/demo/daodao/core_v3/dao_core.gno | 70 ++++ .../gno.land/p/demo/daodao/core_v3/gno.mod | 6 + .../daodao/interfaces_v3/dao_interfaces.gno | 172 ++++++++++ .../daodao/interfaces_v3/dao_messages.gno | 66 ++++ .../p/demo/daodao/interfaces_v3/gno.mod | 1 + .../daodao/interfaces_v3/proposal_test.gno | 59 ++++ .../p/demo/daodao/interfaces_v3/threshold.gno | 36 +++ .../p/demo/daodao/jsonutil_v2/jsonutil.gno | 180 +++++++++++ .../p/demo/daodao/jsonutil_v2/tables.gno | 216 +++++++++++++ .../dao_proposal_single.gno | 305 ++++++++++++++++++ .../p/demo/daodao/proposal_single_v4/gno.mod | 7 + .../proposal_single_v4/update_settings.gno | 114 +++++++ .../p/demo/daodao/voting_group_v5/gno.mod | 7 + .../daodao/voting_group_v5/voting_group.gno | 42 +++ .../voting_group_v5/voting_group_test.gno | 19 ++ .../r/demo/dao_registry_v5/dao_registry.gno | 177 ++++++++++ .../dao_registry_v5/dao_registry_test.gno | 54 ++++ .../gno.land/r/demo/dao_registry_v5/gno.mod | 1 + .../r/demo/dao_registry_v5/tables.gno | 216 +++++++++++++ examples/gno.land/r/demo/groups_v6/gno.mod | 1 + examples/gno.land/r/demo/groups_v6/group.gno | 115 +++++++ examples/gno.land/r/demo/groups_v6/groups.gno | 21 ++ examples/gno.land/r/demo/groups_v6/member.gno | 40 +++ .../gno.land/r/demo/groups_v6/messages.gno | 138 ++++++++ examples/gno.land/r/demo/groups_v6/misc.gno | 96 ++++++ examples/gno.land/r/demo/groups_v6/public.gno | 116 +++++++ examples/gno.land/r/demo/groups_v6/render.gno | 40 +++ examples/gno.land/r/demo/groups_v6/role.gno | 8 + 28 files changed, 2323 insertions(+) create mode 100644 examples/gno.land/p/demo/daodao/core_v3/dao_core.gno create mode 100644 examples/gno.land/p/demo/daodao/core_v3/gno.mod create mode 100644 examples/gno.land/p/demo/daodao/interfaces_v3/dao_interfaces.gno create mode 100644 examples/gno.land/p/demo/daodao/interfaces_v3/dao_messages.gno create mode 100644 examples/gno.land/p/demo/daodao/interfaces_v3/gno.mod create mode 100644 examples/gno.land/p/demo/daodao/interfaces_v3/proposal_test.gno create mode 100644 examples/gno.land/p/demo/daodao/interfaces_v3/threshold.gno create mode 100644 examples/gno.land/p/demo/daodao/jsonutil_v2/jsonutil.gno create mode 100644 examples/gno.land/p/demo/daodao/jsonutil_v2/tables.gno create mode 100644 examples/gno.land/p/demo/daodao/proposal_single_v4/dao_proposal_single.gno create mode 100644 examples/gno.land/p/demo/daodao/proposal_single_v4/gno.mod create mode 100644 examples/gno.land/p/demo/daodao/proposal_single_v4/update_settings.gno create mode 100644 examples/gno.land/p/demo/daodao/voting_group_v5/gno.mod create mode 100644 examples/gno.land/p/demo/daodao/voting_group_v5/voting_group.gno create mode 100644 examples/gno.land/p/demo/daodao/voting_group_v5/voting_group_test.gno create mode 100644 examples/gno.land/r/demo/dao_registry_v5/dao_registry.gno create mode 100644 examples/gno.land/r/demo/dao_registry_v5/dao_registry_test.gno create mode 100644 examples/gno.land/r/demo/dao_registry_v5/gno.mod create mode 100644 examples/gno.land/r/demo/dao_registry_v5/tables.gno create mode 100644 examples/gno.land/r/demo/groups_v6/gno.mod create mode 100644 examples/gno.land/r/demo/groups_v6/group.gno create mode 100644 examples/gno.land/r/demo/groups_v6/groups.gno create mode 100644 examples/gno.land/r/demo/groups_v6/member.gno create mode 100644 examples/gno.land/r/demo/groups_v6/messages.gno create mode 100644 examples/gno.land/r/demo/groups_v6/misc.gno create mode 100644 examples/gno.land/r/demo/groups_v6/public.gno create mode 100644 examples/gno.land/r/demo/groups_v6/render.gno create mode 100644 examples/gno.land/r/demo/groups_v6/role.gno diff --git a/examples/gno.land/p/demo/daodao/core_v3/dao_core.gno b/examples/gno.land/p/demo/daodao/core_v3/dao_core.gno new file mode 100644 index 00000000000..4a72bbfd7e7 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/core_v3/dao_core.gno @@ -0,0 +1,70 @@ +package core + +import ( + "std" + "strings" + + dao_interfaces "gno.land/p/demo/daodao/interfaces_v3" + "gno.land/p/demo/markdown_utils" +) + +// TODO: add wrapper message handler to handle multiple proposal modules messages + +type IDAOCore interface { + AddProposalModule(proposalMod dao_interfaces.IProposalModule) + + VotingModule() dao_interfaces.IVotingModule + ProposalModules() []dao_interfaces.IProposalModule + + Render(path string) string +} + +type daoCore struct { + IDAOCore + + votingModule dao_interfaces.IVotingModule + proposalModules []dao_interfaces.IProposalModule +} + +func NewDAOCore( + votingModule dao_interfaces.IVotingModule, + proposalModules []dao_interfaces.IProposalModule, +) IDAOCore { + return &daoCore{ + votingModule: votingModule, + proposalModules: proposalModules, + } +} + +func (d *daoCore) VotingModule() dao_interfaces.IVotingModule { + return d.votingModule +} + +func (d *daoCore) ProposalModules() []dao_interfaces.IProposalModule { + return d.proposalModules +} + +func (d *daoCore) AddProposalModule(proposalMod dao_interfaces.IProposalModule) { + d.proposalModules = append(d.proposalModules, proposalMod) +} + +func (d *daoCore) Render(path string) string { + s := "# DAO Core\n" + s += "This is a port of [DA0-DA0 contracts](https://github.com/DA0-DA0/dao-contracts)\n" + s += markdown_utils.Indent(d.votingModule.Render(path)) + "\n" + for _, propMod := range d.proposalModules { + s += markdown_utils.Indent(propMod.Render(path)) + "\n" + } + return s +} + +func GetProposalModule(core IDAOCore, moduleIndex int) dao_interfaces.IProposalModule { + if moduleIndex < 0 { + panic("Module index must be >= 0") + } + mods := core.ProposalModules() + if moduleIndex >= len(mods) { + panic("invalid module index") + } + return mods[moduleIndex] +} diff --git a/examples/gno.land/p/demo/daodao/core_v3/gno.mod b/examples/gno.land/p/demo/daodao/core_v3/gno.mod new file mode 100644 index 00000000000..810ec7e040c --- /dev/null +++ b/examples/gno.land/p/demo/daodao/core_v3/gno.mod @@ -0,0 +1,6 @@ +module gno.land/p/demo/daodao/core_v3 + +require ( + "gno.land/p/demo/daodao/interfaces_v3" v0.0.0-latest + "gno.land/p/demo/markdown_utils" v0.0.0-latest +) \ No newline at end of file diff --git a/examples/gno.land/p/demo/daodao/interfaces_v3/dao_interfaces.gno b/examples/gno.land/p/demo/daodao/interfaces_v3/dao_interfaces.gno new file mode 100644 index 00000000000..7ffab4650a8 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/interfaces_v3/dao_interfaces.gno @@ -0,0 +1,172 @@ +package dao_interfaces + +import ( + "std" + "strconv" + + "gno.land/p/demo/avl" + "gno.land/p/demo/jsonutil_v2" +) + +type IVotingModule interface { + VotingPower(addr std.Address) uint64 + TotalPower() uint64 + Render(path string) string +} + +type Ballot struct { + Power uint64 + Vote Vote + Rationale string +} + +func (b Ballot) ToJSON() string { + return jsonutil.FormatObject([]jsonutil.KeyValue{ + {Key: "power", Value: b.Power}, + {Key: "vote", Value: b.Vote}, + {Key: "rationale", Value: b.Rationale}, + }) +} + +type Votes struct { + Yes uint64 + No uint64 + Abstain uint64 +} + +func (v *Votes) Add(vote Vote, power uint64) { + switch vote { + case VoteYes: + v.Yes += power + case VoteNo: + v.No += power + case VoteAbstain: + v.Abstain += power + default: + panic("unknown vote kind") + } +} + +func (v *Votes) Remove(vote Vote, power uint64) { + switch vote { + case VoteYes: + v.Yes -= power + case VoteNo: + v.No -= power + case VoteAbstain: + v.Abstain -= power + default: + panic("unknown vote kind") + } +} + +func (v *Votes) Total() uint64 { + return v.Yes + v.No + v.Abstain +} + +func (v Votes) ToJSON() string { + return jsonutil.FormatObject([]jsonutil.KeyValue{ + {Key: "yes", Value: v.Yes}, + {Key: "no", Value: v.No}, + {Key: "abstain", Value: v.Abstain}, + }) +} + +type Proposal struct { + ID int + Title string + Description string + Proposer std.Address + Messages []ExecutableMessage + Ballots *avl.Tree // dev + // Ballots *avl.MutTree // test3 + Votes Votes + Status ProposalStatus +} + +var _ jsonutil.JSONAble = (*Proposal)(nil) + +func (p Proposal) ToJSON() string { + return jsonutil.FormatObject([]jsonutil.KeyValue{ + {Key: "id", Value: p.ID}, + {Key: "title", Value: p.Title}, + {Key: "description", Value: p.Description}, + {Key: "proposer", Value: p.Proposer}, + {Key: "messages", Value: jsonutil.FormatSlice(p.Messages), Raw: true}, + {Key: "ballots", Value: p.Ballots}, + {Key: "votes", Value: p.Votes}, + {Key: "status", Value: p.Status}, + }) +} + +type ProposalStatus int + +const ( + ProposalStatusOpen ProposalStatus = iota + ProposalStatusPassed + ProposalStatusExecuted +) + +func (p ProposalStatus) ToJSON() string { + return jsonutil.FormatString(p.String()) +} + +func (p ProposalStatus) String() string { + switch p { + case ProposalStatusOpen: + return "Open" + case ProposalStatusPassed: + return "Passed" + case ProposalStatusExecuted: + return "Executed" + default: + return "Unknown(" + strconv.Itoa(int(p)) + ")" + } +} + +type Vote int + +const ( + VoteYes Vote = iota + VoteNo + VoteAbstain +) + +func (v Vote) ToJSON() string { + return jsonutil.FormatString(v.String()) +} + +func (v Vote) String() string { + switch v { + case VoteYes: + return "Yes" + case VoteNo: + return "No" + case VoteAbstain: + return "Abstain" + default: + return "Unknown(" + strconv.Itoa(int(v)) + ")" + } +} + +type IProposalModule interface { + Propose( + title string, + description string, + actions []ExecutableMessage, + ) + Vote(proposalId int, vote Vote, rationale string) + Execute(proposalId int) + Threshold() Threshold + + Proposals() []Proposal + GetBallot(proposalId int, addr std.Address) Ballot + + Render(path string) string +} + +type ExecutableMessage interface { + String() string + Binary() []byte + Type() string +} diff --git a/examples/gno.land/p/demo/daodao/interfaces_v3/dao_messages.gno b/examples/gno.land/p/demo/daodao/interfaces_v3/dao_messages.gno new file mode 100644 index 00000000000..8ab887157dd --- /dev/null +++ b/examples/gno.land/p/demo/daodao/interfaces_v3/dao_messages.gno @@ -0,0 +1,66 @@ +package dao_interfaces + +import ( + "encoding/base64" + "encoding/binary" + "strings" + + "gno.land/p/demo/avl" +) + +type MessageHandler interface { + Execute(message ExecutableMessage) + FromBinary(b []byte) ExecutableMessage + Type() string +} + +type MessagesRegistry struct { + handlers *avl.Tree +} + +func NewMessagesRegistry() *MessagesRegistry { + return &MessagesRegistry{handlers: avl.NewTree()} +} + +func (r *MessagesRegistry) Register(handler MessageHandler) { + r.handlers.Set(handler.Type(), handler) +} + +func (r *MessagesRegistry) FromBinary(b []byte) ExecutableMessage { + if len(b) < 2 { + panic("invalid ExecutableMessage: invalid length") + } + l := binary.BigEndian.Uint16(b[:2]) + if len(b) < int(l+2) { + panic("invalid ExecutableMessage: invalid length") + } + t := string(b[2 : l+2]) + + h, ok := r.handlers.Get(t) + if !ok { + panic("invalid ExecutableMessage: invalid message type") + } + return h.(MessageHandler).FromBinary(b) +} + +func (r *MessagesRegistry) FromBase64String(s string) ExecutableMessage { + b, err := base64.RawURLEncoding.DecodeString(s) + if err != nil { + panic("invalid ExecutableMessage: invalid base64 string") + } + return r.FromBinary(b) +} + +func (r *MessagesRegistry) Execute(msg ExecutableMessage) { + h, ok := r.handlers.Get(msg.Type()) + if !ok { + panic("invalid ExecutableMessage: invalid message type") + } + return h.(MessageHandler).Execute(msg) +} + +func (r *MessagesRegistry) ExecuteMessages(msgs []ExecutableMessage) { + for _, msg := range msgs { + r.Execute(msg) + } +} diff --git a/examples/gno.land/p/demo/daodao/interfaces_v3/gno.mod b/examples/gno.land/p/demo/daodao/interfaces_v3/gno.mod new file mode 100644 index 00000000000..1d1fcc9450e --- /dev/null +++ b/examples/gno.land/p/demo/daodao/interfaces_v3/gno.mod @@ -0,0 +1 @@ +module gno.land/p/demo/daodao/interfaces_v3 diff --git a/examples/gno.land/p/demo/daodao/interfaces_v3/proposal_test.gno b/examples/gno.land/p/demo/daodao/interfaces_v3/proposal_test.gno new file mode 100644 index 00000000000..efc31fa1a08 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/interfaces_v3/proposal_test.gno @@ -0,0 +1,59 @@ +package dao_interfaces + +import ( + "testing" + + "gno.land/p/demo/avl" + "gno.land/p/demo/jsonutil_v2" +) + +type NoopMessage struct{} + +var _ ExecutableMessage = (*NoopMessage)(nil) + +func (m NoopMessage) String() string { + return "noop" +} + +func (m NoopMessage) Binary() []byte { + return nil +} + +func (m NoopMessage) Type() string { + return "noop-type" +} + +func (m NoopMessage) ToJSON() string { + return jsonutil.FormatString(m.String()) +} + +func TestProposalJSON(t *testing.T) { + props := []Proposal{ + { + ID: 0, + Title: "Prop #0", + Description: "Wolol0\n\t\r", + Proposer: "0x1234567890", + Votes: Votes{ + Yes: 7, + No: 21, + Abstain: 42, + }, + Ballots: avl.NewTree(), + }, + { + ID: 1, + Title: "Prop #1", + Description: `Wolol1\"`, + Proposer: "0x1234567890", + Status: ProposalStatusExecuted, + Messages: []ExecutableMessage{NoopMessage{}, NoopMessage{}, NoopMessage{}}, + }, + } + props[0].Ballots.Set("0x1234567890", Ballot{Power: 1, Vote: VoteYes, Rationale: "test"}) + str := jsonutil.FormatSlice(props) + expected := `[{"id":0,"title":"Prop #0","description":"Wolol0\n\t\r","proposer":"0x1234567890","messages":[],"ballots":{"0x1234567890":{"power":1,"vote":"Yes","rationale":"test"}},"votes":{"yes":7,"no":21,"abstain":42},"status":"Open"},{"id":1,"title":"Prop #1","description":"Wolol1\\\"","proposer":"0x1234567890","messages":["noop","noop","noop"],"ballots":{},"votes":{"yes":0,"no":0,"abstain":0},"status":"Executed"}]` + if expected != str { + t.Fatalf("JSON does not match, expected %s, got %s", expected, str) + } +} diff --git a/examples/gno.land/p/demo/daodao/interfaces_v3/threshold.gno b/examples/gno.land/p/demo/daodao/interfaces_v3/threshold.gno new file mode 100644 index 00000000000..7b34ba7b4d2 --- /dev/null +++ b/examples/gno.land/p/demo/daodao/interfaces_v3/threshold.gno @@ -0,0 +1,36 @@ +package dao_interfaces + +import ( + "strconv" +) + +type Percent uint16 // 4 decimals fixed point + +type PercentageThreshold struct { + Percent *Percent +} + +func (p *PercentageThreshold) String() string { + if p == nil || p.Percent == nil { + return "nil" + } + return p.Percent.String() +} + +type ThresholdQuorum struct { + Threshold PercentageThreshold + Quorum PercentageThreshold +} + +type Threshold struct { + ThresholdQuorum *ThresholdQuorum +} + +func (p Percent) String() string { + s := strconv.FormatUint(uint64(p)/100, 10) + decPart := uint64(p) % 100 + if decPart != 0 { + s += "." + strconv.FormatUint(decPart, 10) + } + return s + "%" +} diff --git a/examples/gno.land/p/demo/daodao/jsonutil_v2/jsonutil.gno b/examples/gno.land/p/demo/daodao/jsonutil_v2/jsonutil.gno new file mode 100644 index 00000000000..51fdc369b0e --- /dev/null +++ b/examples/gno.land/p/demo/daodao/jsonutil_v2/jsonutil.gno @@ -0,0 +1,180 @@ +package jsonutil + +// This package strives to have the same behavior as json.Marshal but has no support for nested slices and returns strings + +import ( + "std" + "strconv" + "strings" + "unicode/utf8" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ufmt" +) + +type JSONAble interface { + ToJSON() string +} + +type KeyValue struct { + Key string + Value interface{} + Raw bool +} + +// does not work for slices, use FormatSlice instead +func FormatAny(p interface{}) string { + switch p.(type) { + case std.Address: + return FormatString(string(p.(std.Address))) + case *avl.Tree: + return FormatAVLTree(p.(*avl.Tree)) + case avl.Tree: + return FormatAVLTree(&p.(avl.Tree)) + case JSONAble: + return p.(JSONAble).ToJSON() + case string: + return FormatString(p.(string)) + case uint64: + return FormatUint64(p.(uint64)) + case uint32: + return FormatUint64(uint64(p.(uint32))) + case uint: + return FormatUint64(uint64(p.(uint))) + case int64: + return FormatInt64(p.(int64)) + case int32: + return FormatInt64(int64(p.(int32))) + case int: + return FormatInt64(int64(p.(int))) + case bool: + return FormatBool(p.(bool)) + default: + return "null" + } +} + +// Ported from https://cs.opensource.google/go/go/+/refs/tags/go1.20.6:src/encoding/json/encode.go +func FormatString(s string) string { + const escapeHTML = true + e := `"` // e.WriteByte('"') + start := 0 + for i := 0; i < len(s); { + if b := s[i]; b < utf8.RuneSelf { + if htmlSafeSet[b] || (!escapeHTML && safeSet[b]) { + i++ + continue + } + if start < i { + e += s[start:i] // e.WriteString(s[start:i]) + } + e += "\\" // e.WriteByte('\\') + switch b { + case '\\', '"': + e += string(b) // e.WriteByte(b) + case '\n': + e += "n" // e.WriteByte('n') + case '\r': + e += "r" // e.WriteByte('r') + case '\t': + e += "t" // e.WriteByte('t') + default: + // This encodes bytes < 0x20 except for \t, \n and \r. + // If escapeHTML is set, it also escapes <, >, and & + // because they can lead to security holes when + // user-controlled strings are rendered into JSON + // and served to some browsers. + e += `u00` // e.WriteString(`u00`) + e += string(hex[b>>4]) // e.WriteByte(hex[b>>4]) + e += string(hex[b&0xF]) // e.WriteByte(hex[b&0xF]) + } + i++ + start = i + continue + } + c, size := utf8.DecodeRuneInString(s[i:]) + if c == utf8.RuneError && size == 1 { + if start < i { + e += s[start:i] // e.WriteString(s[start:i]) + } + e += `\ufffd` // e.WriteString(`\ufffd`) + i += size + start = i + continue + } + // U+2028 is LINE SEPARATOR. + // U+2029 is PARAGRAPH SEPARATOR. + // They are both technically valid characters in JSON strings, + // but don't work in JSONP, which has to be evaluated as JavaScript, + // and can lead to security holes there. It is valid JSON to + // escape them, so we do so unconditionally. + // See http://timelessrepo.com/json-isnt-a-javascript-subset for discussion. + if c == '\u2028' || c == '\u2029' { + if start < i { + e += s[start:i] // e.WriteString(s[start:i]) + } + e += `\u202` // e.WriteString(`\u202`) + e += string(hex[c&0xF]) // e.WriteByte(hex[c&0xF]) + i += size + start = i + continue + } + i += size + } + if start < len(s) { + e += s[start:] // e.WriteString(s[start:]) + } + e += `"` // e.WriteByte('"') + return e +} + +func FormatUint64(i uint64) string { + return strconv.FormatUint(i, 10) +} + +func FormatInt64(i int64) string { + return strconv.FormatInt(i, 10) +} + +func FormatSlice(s []interface{}) string { + elems := make([]string, len(s)) + for i, elem := range s { + elems[i] = FormatAny(elem) + } + return "[" + strings.Join(elems, ",") + "]" +} + +func FormatObject(kv []KeyValue) string { + elems := make([]string, len(kv)) + i := 0 + for _, elem := range kv { + var val string + if elem.Raw { + val = elem.Value.(string) + } else { + val = FormatAny(elem.Value) + } + elems[i] = FormatString(elem.Key) + ":" + val + i++ + } + return "{" + strings.Join(elems, ",") + "}" +} + +func FormatBool(b bool) string { + if b { + return "true" + } + return "false" +} + +func FormatAVLTree(t *avl.Tree) string { + if t == nil { + return "{}" + } + kv := make([]KeyValue, 0, t.Size()) + t.Iterate("", "", func(key string, value interface{}) bool { + kv = append(kv, KeyValue{key, value, false}) + return false + }) + return FormatObject(kv) +} diff --git a/examples/gno.land/p/demo/daodao/jsonutil_v2/tables.gno b/examples/gno.land/p/demo/daodao/jsonutil_v2/tables.gno new file mode 100644 index 00000000000..e761c1faa2f --- /dev/null +++ b/examples/gno.land/p/demo/daodao/jsonutil_v2/tables.gno @@ -0,0 +1,216 @@ +package jsonutil + +import "unicode/utf8" + +var hex = "0123456789abcdef" + +// safeSet holds the value true if the ASCII character with the given array +// position can be represented inside a JSON string without any further +// escaping. +// +// All values are true except for the ASCII control characters (0-31), the +// double quote ("), and the backslash character ("\"). +var safeSet = [utf8.RuneSelf]bool{ + ' ': true, + '!': true, + '"': false, + '#': true, + '$': true, + '%': true, + '&': true, + '\'': true, + '(': true, + ')': true, + '*': true, + '+': true, + ',': true, + '-': true, + '.': true, + '/': true, + '0': true, + '1': true, + '2': true, + '3': true, + '4': true, + '5': true, + '6': true, + '7': true, + '8': true, + '9': true, + ':': true, + ';': true, + '<': true, + '=': true, + '>': true, + '?': true, + '@': true, + 'A': true, + 'B': true, + 'C': true, + 'D': true, + 'E': true, + 'F': true, + 'G': true, + 'H': true, + 'I': true, + 'J': true, + 'K': true, + 'L': true, + 'M': true, + 'N': true, + 'O': true, + 'P': true, + 'Q': true, + 'R': true, + 'S': true, + 'T': true, + 'U': true, + 'V': true, + 'W': true, + 'X': true, + 'Y': true, + 'Z': true, + '[': true, + '\\': false, + ']': true, + '^': true, + '_': true, + '`': true, + 'a': true, + 'b': true, + 'c': true, + 'd': true, + 'e': true, + 'f': true, + 'g': true, + 'h': true, + 'i': true, + 'j': true, + 'k': true, + 'l': true, + 'm': true, + 'n': true, + 'o': true, + 'p': true, + 'q': true, + 'r': true, + 's': true, + 't': true, + 'u': true, + 'v': true, + 'w': true, + 'x': true, + 'y': true, + 'z': true, + '{': true, + '|': true, + '}': true, + '~': true, + '\u007f': true, +} + +// htmlSafeSet holds the value true if the ASCII character with the given +// array position can be safely represented inside a JSON string, embedded +// inside of HTML