From e1e14518980e01cf0159f05c6508caa568ea449b Mon Sep 17 00:00:00 2001 From: Norman Meier Date: Thu, 2 May 2024 22:22:49 +0200 Subject: [PATCH 01/11] feat: project manager Signed-off-by: Norman Meier --- .../r/demo/teritori/projects_manager/gno.mod | 3 + .../projects_manager/projects_manager.gno | 830 ++++++++++++++++++ .../projects_manager_test.gno | 74 ++ 3 files changed, 907 insertions(+) create mode 100644 examples/gno.land/r/demo/teritori/projects_manager/gno.mod create mode 100644 examples/gno.land/r/demo/teritori/projects_manager/projects_manager.gno create mode 100644 examples/gno.land/r/demo/teritori/projects_manager/projects_manager_test.gno diff --git a/examples/gno.land/r/demo/teritori/projects_manager/gno.mod b/examples/gno.land/r/demo/teritori/projects_manager/gno.mod new file mode 100644 index 00000000000..c22db665640 --- /dev/null +++ b/examples/gno.land/r/demo/teritori/projects_manager/gno.mod @@ -0,0 +1,3 @@ +module gno.land/r/demo/teritori/projects_manager + +require gno.land/p/demo/json v0.0.0-latest diff --git a/examples/gno.land/r/demo/teritori/projects_manager/projects_manager.gno b/examples/gno.land/r/demo/teritori/projects_manager/projects_manager.gno new file mode 100644 index 00000000000..93af1876b29 --- /dev/null +++ b/examples/gno.land/r/demo/teritori/projects_manager/projects_manager.gno @@ -0,0 +1,830 @@ +package projects_manager + +import ( + "std" + "strconv" + "strings" + "time" + + "gno.land/p/demo/json" +) + +type ContractStatus uint32 + +const ( + CREATED ContractStatus = 1 + ACCEPTED ContractStatus = 2 + CANCELED ContractStatus = 3 + COMPLETED ContractStatus = 5 + REJECTED ContractStatus = 6 + CONFLICT ContractStatus = 7 + ABORTED_IN_FAVOR_OF_CONTRACTOR ContractStatus = 8 + ABORTED_IN_FAVOR_OF_FUNDER ContractStatus = 9 +) + +func (x ContractStatus) String() string { + switch x { + case CREATED: + return "CREATED" + case ACCEPTED: + return "ACCEPTED" + case CANCELED: + return "CANCELED" + case COMPLETED: + return "COMPLETED" + case REJECTED: + return "REJECTED" + case CONFLICT: + return "CONFLICT" + case ABORTED_IN_FAVOR_OF_CONTRACTOR: + return "ABORTED_IN_FAVOR_OF_CONTRACTOR" + case ABORTED_IN_FAVOR_OF_FUNDER: + return "ABORTED_IN_FAVOR_OF_FUNDER" + } + return "UNKNOWN" +} + +func (x ContractStatus) ToJSON() *json.Node { + return json.StringNode("", x.String()) +} + +type ConflictOutcome uint32 + +const ( + RESUME_CONTRACT ConflictOutcome = 1 + REFUND_FUNDER ConflictOutcome = 2 + PAY_CONTRACTOR ConflictOutcome = 3 +) + +func (x ConflictOutcome) String() string { + switch x { + case RESUME_CONTRACT: + return "RESUME_CONTRACT" + case REFUND_FUNDER: + return "REFUND_FUNDER" + case PAY_CONTRACTOR: + return "PAY_CONTRACTOR" + } + return "UNKNOWN" +} + +func (x ConflictOutcome) ToJSON() *json.Node { + return json.StringNode("", x.String()) +} + +type MilestoneStatus uint32 + +const ( + MS_OPEN MilestoneStatus = 1 + MS_PROGRESS MilestoneStatus = 2 + MS_REVIEW MilestoneStatus = 3 + MS_COMPLETED MilestoneStatus = 4 +) + +func (x MilestoneStatus) String() string { + switch x { + case MS_OPEN: + return "MS_OPEN" + case MS_PROGRESS: + return "MS_PROGRESS" + case MS_REVIEW: + return "MS_REVIEW" + case MS_COMPLETED: + return "MS_COMPLETED" + } + return "UNKNOWN" +} + +func (x MilestoneStatus) ToJSON() *json.Node { + return json.StringNode("", x.String()) +} + +type MilestonePriority uint32 + +const ( + MS_PRIORITY_HIGH MilestonePriority = 1 + MS_PRIORITY_MEDIUM MilestonePriority = 2 + MS_PRIORITY_LOW MilestonePriority = 3 +) + +func (x MilestonePriority) String() string { + switch x { + case MS_PRIORITY_HIGH: + return "MS_PRIORITY_HIGH" + case MS_PRIORITY_MEDIUM: + return "MS_PRIORITY_MEDIUM" + case MS_PRIORITY_LOW: + return "MS_PRIORITY_LOW" + } + return "UNKNOWN" +} + +func (x MilestonePriority) ToJSON() *json.Node { + return json.StringNode("", x.String()) +} + +type Milestone struct { + id uint64 + title string + desc string + amount int64 + paid int64 + duration time.Duration + link string // milestone reference link + funded bool + priority MilestonePriority + status MilestoneStatus +} + +func (ms Milestone) ToJSON() *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + "id": json.StringNode("", strconv.FormatUint(ms.id, 10)), + "title": json.StringNode("", ms.title), + "desc": json.StringNode("", ms.desc), + "amount": json.StringNode("", strconv.FormatInt(ms.amount, 10)), + "paid": json.StringNode("", strconv.FormatInt(ms.paid, 10)), + "duration": json.NumberNode("", ms.duration.Seconds()), + "link": json.StringNode("", ms.link), + "funded": json.BoolNode("", ms.funded), + "priority": ms.priority.ToJSON(), + "status": ms.status.ToJSON(), + }) +} + +type Conflict struct { + initiator std.Address + createdAt time.Time + respondedAt *time.Time + resolvedAt *time.Time + initiatorMessage string + responseMessage *string + resolutionMessage *string + outcome *ConflictOutcome +} + +func (c Conflict) ToJSON() *json.Node { + responseMessage := json.NullNode("") + if c.responseMessage != nil { + responseMessage = json.StringNode("", *c.responseMessage) + } + resolutionMessage := json.NullNode("") + if c.resolutionMessage != nil { + resolutionMessage = json.StringNode("", *c.resolutionMessage) + } + return json.ObjectNode("", map[string]*json.Node{ + "initiator": json.StringNode("", c.initiator.String()), + "createdAt": json.StringNode("", c.createdAt.Format(time.RFC3339)), + "respondedAt": json.StringNode("", c.respondedAt.Format(time.RFC3339)), + "resolvedAt": json.StringNode("", c.resolvedAt.Format(time.RFC3339)), + "initiatorMessage": json.StringNode("", c.initiatorMessage), + "responseMessage": responseMessage, + "resolutionMessage": resolutionMessage, + "outcome": c.outcome.ToJSON(), + }) +} + +type Contract struct { + id uint64 + sender std.Address + contractor std.Address + contractorCandidates []std.Address + funder std.Address // funder address + paymentDenom string // banker denom + metadata string // store data forforimage, tags, name, description, links for twitter/github... + status ContractStatus + expireAt time.Time + funderFeedback string + contractorFeedback string + milestones []Milestone + pausedBy string + conflictHandler string // can be a realm path or a caller address + handlerCandidate string // conflict handler candidate suggested by one party + handlerSuggestor string // the suggestor off the conflict handler candidate + createdAt time.Time + budget int64 + funded bool + rejectReason string + conflicts []Conflict +} + +func (c Contract) ToJSON() *json.Node { + candidates := make([]*json.Node, len(c.contractorCandidates)) + for i, candidate := range c.contractorCandidates { + candidates[i] = json.StringNode("", candidate.String()) + } + + milestones := make([]*json.Node, len(c.milestones)) + for i, milestone := range c.milestones { + milestones[i] = milestone.ToJSON() + } + + conflicts := make([]*json.Node, len(c.conflicts)) + + return json.ObjectNode("", map[string]*json.Node{ + "id": json.StringNode("", strconv.FormatUint(c.id, 10)), + "sender": json.StringNode("", c.sender.String()), + "contractor": json.StringNode("", c.contractor.String()), + "contractorCandidates": json.ArrayNode("", candidates), + "funder": json.StringNode("", c.funder.String()), + "paymentDenom": json.StringNode("", c.paymentDenom), + "metadata": json.StringNode("", c.metadata), + "status": c.status.ToJSON(), + "expireAt": json.StringNode("", c.expireAt.Format(time.RFC3339)), + "funderFeedback": json.StringNode("", c.funderFeedback), + "contractorFeedback": json.StringNode("", c.contractorFeedback), + "milestones": json.ArrayNode("", milestones), + "pausedBy": json.StringNode("", c.pausedBy), + "conflictHandler": json.StringNode("", c.conflictHandler), + "handlerCandidate": json.StringNode("", c.handlerCandidate), + "handlerSuggestor": json.StringNode("", c.handlerSuggestor), + "createdAt": json.StringNode("", c.createdAt.Format(time.RFC3339)), + "budget": json.StringNode("", strconv.FormatInt(c.budget, 10)), + "funded": json.BoolNode("", c.funded), + "rejectReason": json.StringNode("", c.rejectReason), + "conflicts": json.ArrayNode("", conflicts), + }) +} + +// Escrow State +var contracts []Contract + +func CurrentRealm() string { + return std.CurrentRealm().Addr().String() +} + +func CreateContract( + contractor std.Address, + funder std.Address, + paymentDenom string, + metadata string, + expiryDurationSeconds uint64, + milestoneTitles string, + milestoneDescs string, + milestoneAmounts string, + milestoneDurationsSeconds string, + milestoneLinks string, + milestonePriorities string, + conflictHandler string, +) { + if contractor != "" && !contractor.IsValid() { + panic("invalid contractor address") + } + + if funder != "" && !funder.IsValid() { + panic("invalid funder address") + } + + caller := std.PrevRealm().Addr() + if expiryDurationSeconds == 0 { + panic("invalid expiryDuration") + } + if paymentDenom == "" { + panic("empty escrow token") + } + + // For now, one of funder or contract could be empty and can be set later + if contractor == "" && funder == "" { + panic("contractor and funder cannot be both empty") + } + + if contractor != caller && funder != caller { + panic("caller should be one of contractor or funder") + } + + milestoneTitleArr := strings.Split(milestoneTitles, ",") + milestoneDescArr := strings.Split(milestoneDescs, ",") + milestoneAmountArr := strings.Split(milestoneAmounts, ",") + milestoneDurationArr := strings.Split(milestoneDurationsSeconds, ",") + milestoneLinkArr := strings.Split(milestoneLinks, ",") + milestonePrioritiesArr := strings.Split(milestonePriorities, ",") + + if len(milestoneTitleArr) == 0 { + panic("no milestone titles provided") + } + + if len(milestoneTitleArr) != len(milestoneAmountArr) || + len(milestoneTitleArr) != len(milestoneDurationArr) || + len(milestoneTitleArr) != len(milestonePrioritiesArr) || + len(milestoneTitleArr) != len(milestoneDescArr) || + len(milestoneTitleArr) != len(milestoneLinkArr) { + panic("mismatch on milestones title, description, amount, duration, priority and link") + } + + milestones := []Milestone{} + projectBudget := int64(0) + for i, title := range milestoneTitleArr { + amount, err := strconv.Atoi(milestoneAmountArr[i]) + if err != nil { + panic(err) + } + if amount < 0 { + panic("milestone amount should be a positive number") + } + + durationSeconds, err := strconv.Atoi(milestoneDurationArr[i]) + if err != nil { + panic(err) + } + if durationSeconds <= 0 { + panic("milestone duration should be greater than 0") + } + + var prio MilestonePriority + + switch milestonePrioritiesArr[i] { + case "MS_PRIORITY_HIGH": + prio = MS_PRIORITY_HIGH + case "MS_PRIORITY_MEDIUM": + prio = MS_PRIORITY_MEDIUM + case "MS_PRIORITY_LOW": + prio = MS_PRIORITY_LOW + default: + panic("priority is not valid") + } + + duration := time.Duration(durationSeconds) * time.Second + + milestones = append(milestones, Milestone{ + id: uint64(i), + title: title, + desc: milestoneDescArr[i], + amount: int64(amount), + paid: 0, + duration: duration, + link: milestoneLinkArr[i], + priority: prio, + funded: false, + status: MS_OPEN, + }) + projectBudget += int64(amount) + } + + // If contract creator is funder then he needs to send all the needed fund to contract + funded := false + if caller == funder { + sent := std.GetOrigSend() + amount := sent.AmountOf(paymentDenom) + if amount != projectBudget { + panic("funder should send all the needed funds at instantiation") + } + funded = true + } + + expiryDuration := time.Duration(expiryDurationSeconds) * time.Second + now := time.Now() + + contractId := uint64(len(contracts)) + contracts = append(contracts, Contract{ + id: contractId, + sender: caller, + contractor: contractor, + funder: funder, + paymentDenom: paymentDenom, + metadata: metadata, + status: CREATED, + expireAt: now.Add(expiryDuration), + milestones: milestones, + conflictHandler: conflictHandler, + budget: projectBudget, + createdAt: now, + funded: funded, + }) +} + +func CancelContract(contractId uint64) { + caller := std.PrevRealm().Addr() + if int(contractId) >= len(contracts) { + panic("invalid contract id") + } + + contract := contracts[contractId] + if contract.status != CREATED { + panic("contract can only be cancelled at CREATED status") + } + + if contract.sender != caller { + panic("not authorized to cancel the contract") + } + + contracts[contractId].status = CANCELED +} + +func RejectContract(contractId uint64, rejectReason string) { + caller := std.PrevRealm().Addr() + if int(contractId) >= len(contracts) { + panic("invalid contract id") + } + + contract := contracts[contractId] + if contract.status != CREATED { + panic("contract can only be cancelled at CREATED status") + } + + if contract.sender == contract.contractor && caller != contract.funder { + // If contract creator is contractor then only funder can reject + panic("only funder can reject a request from contractor") + } else if contract.sender == contract.funder && caller != contract.contractor { + // If contract creator is funder then only contractor can reject + panic("only contractor can reject a request from funder") + } + + contracts[contractId].status = REJECTED + contracts[contractId].rejectReason = rejectReason +} + +func AcceptContract(contractId uint64) { + caller := std.PrevRealm().Addr() + if int(contractId) >= len(contracts) { + panic("invalid contract id") + } + + contract := contracts[contractId] + if contract.status != CREATED { + panic("contract can only be accepted at CREATED status") + } + + if time.Now().After(contract.expireAt) { + panic("contract already expired") + } + + if contract.sender == caller { + panic("contract sender is not able to accept the contract") + } + + if contract.funder != caller && contract.contractor != caller { + panic("only contract counterparty is allowed to accept") + } + contracts[contractId].status = ACCEPTED +} + +// Submit a funder by putting funds for specific milestones +func SubmitFunder(contractId uint64) { + caller := std.PrevRealm().Addr() + if int(contractId) >= len(contracts) { + panic("invalid contract id") + } + + contract := contracts[contractId] + + if contract.status != CREATED { + panic("can only submit candidate to a CREATED contract") + } + + if contract.funder != "" { + panic("the contract has already a funder") + } + + if caller == contract.contractor { + panic("you cannot become a funder of your requested contract") + } + + sent := std.GetOrigSend() + amount := sent.AmountOf(contract.paymentDenom) + if amount != contract.budget { + panic("wrong amount of funds sent") + } + + contracts[contractId].funded = true + contracts[contractId].status = ACCEPTED + contracts[contractId].funder = caller +} + +// Accept candidate as a contractor +func AcceptContractor(contractId uint64, contractor std.Address) { + if !contractor.IsValid() { + panic("invalid contractor address") + } + + caller := std.PrevRealm().Addr() + if int(contractId) >= len(contracts) { + panic("invalid contract id") + } + + contract := contracts[contractId] + + if contract.status != CREATED { + panic("can only submit candidate to a CREATED contract") + } + + if contract.contractor != "" { + panic("the contract has already a contractor") + } + + if caller != contract.funder { + panic("Only contract funder can accept contractor") + } + + candidates := contracts[contractId].contractorCandidates + for _, candidate := range candidates { + // Accept the contract if the address already submitted candidate request + if candidate == contractor { + contracts[contractId].status = ACCEPTED + } + } + + contracts[contractId].contractor = contractor +} + +func SubmitContractorCandidate(contractId uint64) { + caller := std.PrevRealm().Addr() + if int(contractId) >= len(contracts) { + panic("invalid contract id") + } + + contract := contracts[contractId] + + if contract.status != CREATED { + panic("can only submit candidate to a CREATED contract") + } + + if contract.contractor != "" { + panic("the contract has already a contractor") + } + + if caller == contract.funder { + panic("you cannot become a contractor of your funded contract") + } + + candidates := contracts[contractId].contractorCandidates + for _, candidate := range candidates { + if candidate == caller { + panic("already a contractor candidate") + } + } + + contracts[contractId].contractorCandidates = append(candidates, caller) +} + +// Complete any milestone in review status and pay the needed amount +func CompleteMilestoneAndPay(contractId uint64, milestoneId uint64) { + caller := std.PrevRealm().Addr() + if int(contractId) >= len(contracts) { + panic("invalid contract id") + } + + contract := contracts[contractId] + if contract.funder != caller { + panic("only contract funder can pay the milestone") + } + + if contract.status != ACCEPTED { + panic("only accepted contract can be paid") + } + + milestone := contract.milestones[milestoneId] + if milestone.status != MS_REVIEW { + panic("can only complete and pay a milestone which is in review status") + } + + // Pay the milestone + unpaid := milestone.amount - milestone.paid + if unpaid > 0 { + banker := std.GetBanker(std.BankerTypeRealmSend) + banker.SendCoins( + std.CurrentRealm().Addr(), + contract.contractor, + std.Coins{std.Coin{contract.paymentDenom, int64(unpaid)}}) + contracts[contractId].milestones[milestoneId].paid += unpaid + } + + contracts[contractId].milestones[milestoneId].status = MS_COMPLETED + + // If finish all milestone then complete the contract + completedCount := 0 + for _, milestone := range contract.milestones { + if milestone.status == MS_COMPLETED { + completedCount++ + } + } + + if completedCount == len(contract.milestones) { + contracts[contractId].status = COMPLETED + } +} + +// Set milestone status +func ChangeMilestoneStatus(contractId uint64, milestoneId int, newStatus MilestoneStatus) { + if newStatus == MS_COMPLETED { + panic("use CompleteMilestoneAndPay to complete and pay the milestone") + } + + if int(contractId) >= len(contracts) { + panic("invalid contract id") + } + contract := contracts[contractId] + + caller := std.PrevRealm().Addr() + if contract.funder != caller && contract.contractor != caller { + panic("only contract participant can execute the action") + } + + if contract.status != ACCEPTED { + panic("contract is not on accepted status") + } + + if len(contract.milestones) <= milestoneId { + panic("milestone Id does not exist in contract") + } + milestone := contract.milestones[milestoneId] + + if milestone.status == MS_COMPLETED { + panic("milestone is completed") + } + + contracts[contractId].milestones[milestoneId].status = newStatus +} + +func RequestConflictResolution(contractId uint64, message string) { + caller := std.PrevRealm().Addr() + if int(contractId) >= len(contracts) { + panic("invalid contract id") + } + + contract := contracts[contractId] + if contract.funder != caller && contract.contractor != caller { + panic("only contract participants can request conflict resolution") + } + + if contract.status != ACCEPTED { + panic("conflict resolution can only be requested at ACCEPTED status") + } + + contracts[contractId].status = CONFLICT + + contracts[contractId].conflicts = append(contract.conflicts, Conflict{ + initiator: caller, + createdAt: time.Now(), + initiatorMessage: message, + }) +} + +func RespondToConflict(contractId uint64, message string) { + caller := std.PrevRealm().Addr() + if int(contractId) >= len(contracts) { + panic("invalid contract id") + } + + contract := contracts[contractId] + if contract.status != CONFLICT { + panic("conflict can only be responded at CONFLICT status") + } + + if len(contract.conflicts) == 0 { + panic("no conflict exists, this should not happen") + } + + conflictId := len(contract.conflicts) - 1 + conflict := contract.conflicts[conflictId] + + if conflict.initiator == contract.funder { + if contract.contractor != caller { + panic("only contract funder can respond to this conflict") + } + } else if conflict.initiator == contract.contractor { + if contract.funder != caller { + panic("only contract contractor can respond to this conflict") + } + } else { + panic("conflict initiator is not valid") + } + + contracts[contractId].conflicts[conflictId].responseMessage = &message + now := time.Now() + contracts[contractId].conflicts[conflictId].respondedAt = &now +} + +func ResolveConflict(contractId uint64, outcome ConflictOutcome, resolutionMessage string) { + caller := std.PrevRealm().Addr() + if int(contractId) >= len(contracts) { + panic("invalid contract id") + } + + contract := contracts[contractId] + if contract.conflictHandler != caller.String() { + panic("only conflictHandler is allowed for this operation") + } + + if contract.status != CONFLICT { + panic("conflict can only be resolved at CONFLICT status") + } + + if len(contract.conflicts) == 0 { + panic("no conflict exists") + } + + conflictId := len(contract.conflicts) - 1 + + switch outcome { + case RESUME_CONTRACT: + contracts[contractId].status = ACCEPTED + case REFUND_FUNDER: + totalPaid := int64(0) + for _, milestone := range contract.milestones { + totalPaid += milestone.paid + } + banker := std.GetBanker(std.BankerTypeRealmSend) + banker.SendCoins( + std.CurrentRealm().Addr(), + contract.funder, + std.Coins{std.Coin{contract.paymentDenom, contract.budget - totalPaid}}) + contracts[contractId].status = ABORTED_IN_FAVOR_OF_FUNDER + case PAY_CONTRACTOR: + totalPaid := int64(0) + for _, milestone := range contract.milestones { + totalPaid += milestone.paid + } + banker := std.GetBanker(std.BankerTypeRealmSend) + banker.SendCoins( + std.CurrentRealm().Addr(), + contract.contractor, + std.Coins{std.Coin{contract.paymentDenom, contract.budget - totalPaid}}) + contracts[contractId].status = ABORTED_IN_FAVOR_OF_CONTRACTOR + default: + panic("invalid outcome") + } + + contracts[contractId].conflicts[conflictId].resolutionMessage = &resolutionMessage + contracts[contractId].conflicts[conflictId].outcome = &outcome + now := time.Now() + contracts[contractId].conflicts[conflictId].resolvedAt = &now +} + +func GetContractorCandidatesJSON(contractId uint64) string { + if int(contractId) >= len(contracts) { + panic("invalid contract id") + } + + candidates := contracts[contractId].contractorCandidates + candidatesJSON := make([]*json.Node, len(candidates)) + for i, candidate := range candidates { + candidatesJSON[i] = json.StringNode("", candidate.String()) + } + + ret, err := json.Marshal(json.ArrayNode("", candidatesJSON)) + if err != nil { + panic(err) + } + return string(ret) +} + +func GetContracts(startAfter, limit uint64, filterByFunder string, filterByContractor string) []Contract { + max := uint64(len(contracts)) + if startAfter+limit < max { + max = startAfter + limit + } + + var results []Contract + i := uint64(0) + + for _, c := range contracts { + if filterByFunder != "ALL" && c.funder.String() != filterByFunder { + continue + } + + if filterByContractor != "ALL" && c.contractor.String() != filterByContractor { + continue + } + + if i < startAfter { + i++ + continue + } + + if i > max { + break + } + + results = append(results, c) + i++ + } + + return results +} + +func RenderContractJSON(contractId uint64) string { + if int(contractId) >= len(contracts) { + panic("invalid contract id") + } + + c := contracts[contractId] + ret, err := json.Marshal(c.ToJSON()) + if err != nil { + panic(err) + } + + return string(ret) +} + +func RenderContractsJSON(startAfter uint64, limit uint64, filterByFunder string, filterByContractor string) string { + contracts := GetContracts(startAfter, limit, filterByFunder, filterByContractor) + contractsJSON := make([]*json.Node, len(contracts)) + for i, c := range contracts { + contractsJSON[i] = c.ToJSON() + } + + ret, err := json.Marshal(json.ArrayNode("", contractsJSON)) + if err != nil { + panic(err) + } + return string(ret) +} diff --git a/examples/gno.land/r/demo/teritori/projects_manager/projects_manager_test.gno b/examples/gno.land/r/demo/teritori/projects_manager/projects_manager_test.gno new file mode 100644 index 00000000000..78044c4b90b --- /dev/null +++ b/examples/gno.land/r/demo/teritori/projects_manager/projects_manager_test.gno @@ -0,0 +1,74 @@ +package projects_manager + +import ( + "std" + "testing" + "time" + + "gno.land/p/demo/json" +) + +func TestJSONRender(t *testing.T) { + createdAt := time.Date(2021, time.August, 1, 0, 0, 0, 0, time.UTC) + duration := time.Hour * 24 * 30 + expireAt := createdAt.Add(duration) + + // golden contract + contract := Contract{ + id: 1, + sender: std.Address("sender"), + contractor: std.Address("contractor2"), + contractorCandidates: []std.Address{"contractor1", "contractor2"}, + funder: "funder", + paymentDenom: "denom", + metadata: "metadata", + status: CREATED, + expireAt: expireAt, + funderFeedback: "funderFeedback", + contractorFeedback: "contractorFeedback", + milestones: []Milestone{ + { + id: 1, + title: "title", + desc: "desc", + amount: 100, + paid: 0, + duration: duration, + link: "link", + funded: false, + priority: MS_PRIORITY_HIGH, + status: MS_OPEN, + }, + }, + pausedBy: "pausedBy", + conflictHandler: "conflictHandler", + handlerCandidate: "handlerCandidate", + handlerSuggestor: "handlerSuggestor", + createdAt: createdAt, + budget: 1000, + funded: false, + rejectReason: "rejectReason", + conflicts: []Conflict{ + { + initiator: "initiator", + createdAt: createdAt, + respondedAt: nil, + resolvedAt: nil, + initiatorMessage: "initiatorMessage", + responseMessage: nil, + resolutionMessage: nil, + outcome: nil, + }, + }, + } + + output, err := json.Marshal(contract.ToJSON()) + if err != nil { + t.Fatalf("Error marshalling contract to JSON: %s", err) + } + + expected := `{"id":1,"sender":"sender","contractor":"contractor2","contractorCandidates":["contractor1","contractor2"],"funder":"funder","paymentDenom":"denom","metadata":"metadata","status":"CREATED","expireAt":"2021-08-31T00:00:00Z","funderFeedback":"funderFeedback","contractorFeedback":"contractorFeedback","milestones":[{"id":1,"title":"title","desc":"desc","amount":100,"paid":0,"duration":2592000000000000,"link":"link","funded":false,"priority":"MS_PRIORITY_HIGH","status":"MS_OPEN"}],"pausedBy":"pausedBy","conflictHandler":"conflictHandler","handlerCandidate":"handlerCandidate","handlerSuggestor":"handlerSuggestor","createdAt":"2021-08-01T00:00:00Z","budget":1000,"funded":false,"rejectReason":"rejectReason","conflicts":[{"initiator":"initiator","createdAt":"2021-08-01T00:00:00Z","respondedAt":null,"resolvedAt":null,"initiatorMessage":"initiatorMessage","responseMessage":null,"resolutionMessage":null,"outcome":null}]}` + if output != expected { + t.Errorf("Expected output to be `%s`, got `%s`", expected, output) + } +} From dbd63ac845d14a02f927302e6af7b59107dff4b1 Mon Sep 17 00:00:00 2001 From: Norman Meier Date: Thu, 2 May 2024 23:45:30 +0200 Subject: [PATCH 02/11] fix: avoid stack overflow Signed-off-by: Norman Meier --- .../projects_manager/projects_manager.gno | 36 +++++++++++-------- .../projects_manager_test.gno | 6 ++-- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/examples/gno.land/r/demo/teritori/projects_manager/projects_manager.gno b/examples/gno.land/r/demo/teritori/projects_manager/projects_manager.gno index 93af1876b29..bbf3150f6e0 100644 --- a/examples/gno.land/r/demo/teritori/projects_manager/projects_manager.gno +++ b/examples/gno.land/r/demo/teritori/projects_manager/projects_manager.gno @@ -163,24 +163,29 @@ type Conflict struct { } func (c Conflict) ToJSON() *json.Node { - responseMessage := json.NullNode("") + children := map[string]*json.Node{ + "initiator": json.StringNode("", c.initiator.String()), + "createdAt": json.StringNode("", c.createdAt.Format(time.RFC3339)), + "initiatorMessage": json.StringNode("", c.initiatorMessage), + } + if c.responseMessage != nil { - responseMessage = json.StringNode("", *c.responseMessage) + children["responseMessage"] = json.StringNode("", *c.responseMessage) + } + if c.respondedAt != nil { + children["respondedAt"] = json.StringNode("", c.respondedAt.Format(time.RFC3339)) + } + if c.resolvedAt != nil { + children["resolvedAt"] = json.StringNode("", c.resolvedAt.Format(time.RFC3339)) } - resolutionMessage := json.NullNode("") if c.resolutionMessage != nil { - resolutionMessage = json.StringNode("", *c.resolutionMessage) + children["resolutionMessage"] = json.StringNode("", *c.resolutionMessage) } - return json.ObjectNode("", map[string]*json.Node{ - "initiator": json.StringNode("", c.initiator.String()), - "createdAt": json.StringNode("", c.createdAt.Format(time.RFC3339)), - "respondedAt": json.StringNode("", c.respondedAt.Format(time.RFC3339)), - "resolvedAt": json.StringNode("", c.resolvedAt.Format(time.RFC3339)), - "initiatorMessage": json.StringNode("", c.initiatorMessage), - "responseMessage": responseMessage, - "resolutionMessage": resolutionMessage, - "outcome": c.outcome.ToJSON(), - }) + if c.outcome != nil { + children["outcome"] = c.outcome.ToJSON() + } + + return json.ObjectNode("", children) } type Contract struct { @@ -219,6 +224,9 @@ func (c Contract) ToJSON() *json.Node { } conflicts := make([]*json.Node, len(c.conflicts)) + for i, conflict := range c.conflicts { + conflicts[i] = conflict.ToJSON() + } return json.ObjectNode("", map[string]*json.Node{ "id": json.StringNode("", strconv.FormatUint(c.id, 10)), diff --git a/examples/gno.land/r/demo/teritori/projects_manager/projects_manager_test.gno b/examples/gno.land/r/demo/teritori/projects_manager/projects_manager_test.gno index 78044c4b90b..f9c5d7d9dcf 100644 --- a/examples/gno.land/r/demo/teritori/projects_manager/projects_manager_test.gno +++ b/examples/gno.land/r/demo/teritori/projects_manager/projects_manager_test.gno @@ -67,8 +67,8 @@ func TestJSONRender(t *testing.T) { t.Fatalf("Error marshalling contract to JSON: %s", err) } - expected := `{"id":1,"sender":"sender","contractor":"contractor2","contractorCandidates":["contractor1","contractor2"],"funder":"funder","paymentDenom":"denom","metadata":"metadata","status":"CREATED","expireAt":"2021-08-31T00:00:00Z","funderFeedback":"funderFeedback","contractorFeedback":"contractorFeedback","milestones":[{"id":1,"title":"title","desc":"desc","amount":100,"paid":0,"duration":2592000000000000,"link":"link","funded":false,"priority":"MS_PRIORITY_HIGH","status":"MS_OPEN"}],"pausedBy":"pausedBy","conflictHandler":"conflictHandler","handlerCandidate":"handlerCandidate","handlerSuggestor":"handlerSuggestor","createdAt":"2021-08-01T00:00:00Z","budget":1000,"funded":false,"rejectReason":"rejectReason","conflicts":[{"initiator":"initiator","createdAt":"2021-08-01T00:00:00Z","respondedAt":null,"resolvedAt":null,"initiatorMessage":"initiatorMessage","responseMessage":null,"resolutionMessage":null,"outcome":null}]}` - if output != expected { - t.Errorf("Expected output to be `%s`, got `%s`", expected, output) + expected := `{"id":"1","sender":"sender","contractor":"contractor2","contractorCandidates":["contractor1","contractor2"],"funder":"funder","paymentDenom":"denom","metadata":"metadata","status":"CREATED","expireAt":"2021-08-31T00:00:00Z","funderFeedback":"funderFeedback","contractorFeedback":"contractorFeedback","milestones":[{"id":"1","title":"title","desc":"desc","amount":"100","paid":"0","duration":2592000,"link":"link","funded":false,"priority":"MS_PRIORITY_HIGH","status":"MS_OPEN"}],"pausedBy":"pausedBy","conflictHandler":"conflictHandler","handlerCandidate":"handlerCandidate","handlerSuggestor":"handlerSuggestor","createdAt":"2021-08-01T00:00:00Z","budget":"1000","funded":false,"rejectReason":"rejectReason","conflicts":[{"initiator":"initiator","createdAt":"2021-08-01T00:00:00Z","initiatorMessage":"initiatorMessage"}]}` + if string(output) != expected { + t.Errorf("Expected output to be `%s`, got:\n`%s`", expected, string(output)) } } From 5e231370f1a31d8116acc6bf42e38e776201e85f Mon Sep 17 00:00:00 2001 From: Norman Meier Date: Fri, 3 May 2024 19:12:18 +0200 Subject: [PATCH 03/11] feat: render Signed-off-by: Norman Meier --- .../r/demo/teritori/projects_manager/gno.mod | 5 ++++- .../projects_manager/projects_manager.gno | 21 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/examples/gno.land/r/demo/teritori/projects_manager/gno.mod b/examples/gno.land/r/demo/teritori/projects_manager/gno.mod index c22db665640..da9d6b9f186 100644 --- a/examples/gno.land/r/demo/teritori/projects_manager/gno.mod +++ b/examples/gno.land/r/demo/teritori/projects_manager/gno.mod @@ -1,3 +1,6 @@ module gno.land/r/demo/teritori/projects_manager -require gno.land/p/demo/json v0.0.0-latest +require ( + gno.land/p/demo/json v0.0.0-latest + gno.land/p/demo/ui v0.0.0-latest +) diff --git a/examples/gno.land/r/demo/teritori/projects_manager/projects_manager.gno b/examples/gno.land/r/demo/teritori/projects_manager/projects_manager.gno index bbf3150f6e0..99cc30aa941 100644 --- a/examples/gno.land/r/demo/teritori/projects_manager/projects_manager.gno +++ b/examples/gno.land/r/demo/teritori/projects_manager/projects_manager.gno @@ -7,6 +7,7 @@ import ( "time" "gno.land/p/demo/json" + "gno.land/p/demo/ufmt" ) type ContractStatus uint32 @@ -836,3 +837,23 @@ func RenderContractsJSON(startAfter uint64, limit uint64, filterByFunder string, } return string(ret) } + +func Render(path string) string { + b := strings.Builder{} + b.WriteString("# Projects Manager\n") + b.WriteString("## Overview\n") + b.WriteString("This contract is a simple project manager that allows users to create projects and manage them.\n") + b.WriteString(ufmt.Sprintf("Contracts managed: %d\n", len(contracts))) + b.WriteString("## Latest projects\n") + numContracts := 3 + if len(contracts) < 3 { + numContracts = len(contracts) + } + for i := 0; i < numContracts; i++ { + b.WriteString("```json\n") + b.WriteString(RenderContractJSON(uint64(len(contracts) - (i + 1)))) + b.WriteString("\n") + b.WriteString("```\n") + } + return b.String() +} From 323a117084e5d2c8431235a9adfa9f7272b0ee47 Mon Sep 17 00:00:00 2001 From: Norman Meier Date: Fri, 3 May 2024 19:21:59 +0200 Subject: [PATCH 04/11] feat(gnoweb): prettify json code Signed-off-by: Norman Meier --- gno.land/pkg/gnoweb/static/css/app.css | 3 ++- .../pkg/gnoweb/static/js/highlight.min.js | 8 +++++++- gno.land/pkg/gnoweb/static/js/marked.min.js | 14 ++++++++++--- gno.land/pkg/gnoweb/static/js/renderer.js | 20 +++++++++++++++++-- gno.land/pkg/gnoweb/views/funcs.html | 1 + 5 files changed, 39 insertions(+), 7 deletions(-) diff --git a/gno.land/pkg/gnoweb/static/css/app.css b/gno.land/pkg/gnoweb/static/css/app.css index d02471e1d80..05fca0a5af9 100644 --- a/gno.land/pkg/gnoweb/static/css/app.css +++ b/gno.land/pkg/gnoweb/static/css/app.css @@ -283,6 +283,7 @@ label > img { code { white-space: pre-wrap; + overflow-wrap: anywhere; } /*** COMPOSITION ***/ .container { @@ -824,4 +825,4 @@ code.hljs { .hljs-strong { font-weight: bold; -} +} \ No newline at end of file diff --git a/gno.land/pkg/gnoweb/static/js/highlight.min.js b/gno.land/pkg/gnoweb/static/js/highlight.min.js index 222fa5fecf9..ff436887f3d 100644 --- a/gno.land/pkg/gnoweb/static/js/highlight.min.js +++ b/gno.land/pkg/gnoweb/static/js/highlight.min.js @@ -313,4 +313,10 @@ className:"number",variants:[{begin:e.C_NUMBER_RE+"[i]",relevance:1 },e.C_NUMBER_MODE]},{begin:/:=/},{className:"function",beginKeywords:"func", end:"\\s*(\\{|$)",excludeEnd:!0,contains:[e.TITLE_MODE,{className:"params", begin:/\(/,end:/\)/,endsParent:!0,keywords:n,illegal:/["']/}]}]}}})() -;hljs.registerLanguage("go",e)})(); \ No newline at end of file +;hljs.registerLanguage("go",e)})();/*! `json` grammar compiled for Highlight.js 11.5.1 */ +(()=>{var e=(()=>{"use strict";return e=>({name:"JSON",contains:[{ +className:"attr",begin:/"(\\.|[^\\"\r\n])*"(?=\s*:)/,relevance:1.01},{ +match:/[{}[\],:]/,className:"punctuation",relevance:0},e.QUOTE_STRING_MODE,{ +beginKeywords:"true false null" +},e.C_NUMBER_MODE,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE],illegal:"\\S"}) +})();hljs.registerLanguage("json",e)})(); \ No newline at end of file diff --git a/gno.land/pkg/gnoweb/static/js/marked.min.js b/gno.land/pkg/gnoweb/static/js/marked.min.js index 9b40903e997..3cc149db48e 100644 --- a/gno.land/pkg/gnoweb/static/js/marked.min.js +++ b/gno.land/pkg/gnoweb/static/js/marked.min.js @@ -1,6 +1,14 @@ /** - * marked - a markdown parser - * Copyright (c) 2011-2022, Christopher Jeffrey. (MIT Licensed) + * marked v12.0.2 - a markdown parser + * Copyright (c) 2011-2024, Christopher Jeffrey. (MIT Licensed) * https://github.com/markedjs/marked */ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).marked={})}(this,function(r){"use strict";function i(e,t){for(var u=0;ue.length)&&(t=e.length);for(var u=0,n=new Array(t);u=e.length?{done:!0}:{done:!1,value:e[u++]}};throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function e(){return{baseUrl:null,breaks:!1,extensions:null,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:null,sanitize:!1,sanitizer:null,silent:!1,smartLists:!1,smartypants:!1,tokenizer:null,walkTokens:null,xhtml:!1}}r.defaults=e();function u(e){return t[e]}var n=/[&<>"']/,l=/[&<>"']/g,a=/[<>"']|&(?!#?\w+;)/,o=/[<>"']|&(?!#?\w+;)/g,t={"&":"&","<":"<",">":">",'"':""","'":"'"};function D(e,t){if(t){if(n.test(e))return e.replace(l,u)}else if(a.test(e))return e.replace(o,u);return e}var c=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;function m(e){return e.replace(c,function(e,t){return"colon"===(t=t.toLowerCase())?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""})}var h=/(^|[^\[])\^/g;function p(u,e){u="string"==typeof u?u:u.source,e=e||"";var n={replace:function(e,t){return t=(t=t.source||t).replace(h,"$1"),u=u.replace(e,t),n},getRegex:function(){return new RegExp(u,e)}};return n}var f=/[^\w:]/g,Z=/^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;function g(e,t,u){if(e){try{n=decodeURIComponent(m(u)).replace(f,"").toLowerCase()}catch(e){return null}if(0===n.indexOf("javascript:")||0===n.indexOf("vbscript:")||0===n.indexOf("data:"))return null}var n;t&&!Z.test(u)&&(e=u,F[" "+(n=t)]||(O.test(n)?F[" "+n]=n+"/":F[" "+n]=k(n,"/",!0)),t=-1===(n=F[" "+n]).indexOf(":"),u="//"===e.substring(0,2)?t?e:n.replace(q,"$1")+e:"/"===e.charAt(0)?t?e:n.replace(L,"$1")+e:n+e);try{u=encodeURI(u).replace(/%25/g,"%")}catch(e){return null}return u}var F={},O=/^[^:]+:\/*[^/]*$/,q=/^([^:]+:)[\s\S]*$/,L=/^([^:]+:\/*[^/]*)[\s\S]*$/;var A={exec:function(){}};function d(e){for(var t,u,n=1;nt)u.splice(t);else for(;u.length>=1,e+=e;return u+e}function B(e,t,u,n){var r=t.href,t=t.title?D(t.title):null,i=e[1].replace(/\\([\[\]])/g,"$1");return"!"!==e[0].charAt(0)?(n.state.inLink=!0,e={type:"link",raw:u,href:r,title:t,text:i,tokens:n.inlineTokens(i,[])},n.state.inLink=!1,e):{type:"image",raw:u,href:r,title:t,text:D(i)}}var w=function(){function e(e){this.options=e||r.defaults}var t=e.prototype;return t.space=function(e){e=this.rules.block.newline.exec(e);if(e&&0=u.length?e.slice(u.length):e}).join("\n")}(t=e[0],e[3]||""),{type:"code",raw:t,lang:e[2]&&e[2].trim(),text:u}},t.heading=function(e){var t,u,e=this.rules.block.heading.exec(e);if(e)return t=e[2].trim(),/#$/.test(t)&&(u=k(t,"#"),!this.options.pedantic&&u&&!/ $/.test(u)||(t=u.trim())),u={type:"heading",raw:e[0],depth:e[1].length,text:t,tokens:[]},this.lexer.inline(u.text,u.tokens),u},t.hr=function(e){e=this.rules.block.hr.exec(e);if(e)return{type:"hr",raw:e[0]}},t.blockquote=function(e){var t,e=this.rules.block.blockquote.exec(e);if(e)return t=e[0].replace(/^ *>[ \t]?/gm,""),{type:"blockquote",raw:e[0],tokens:this.lexer.blockTokens(t,[]),text:t}},t.list=function(e){var t=this.rules.block.list.exec(e);if(t){var u,n,r,i,s,l,a,o,D,c,h,p=1<(g=t[1].trim()).length,f={type:"list",raw:"",ordered:p,start:p?+g.slice(0,-1):"",loose:!1,items:[]},g=p?"\\d{1,9}\\"+g.slice(-1):"\\"+g;this.options.pedantic&&(g=p?g:"[*+-]");for(var F=new RegExp("^( {0,3}"+g+")((?:[\t ][^\\n]*)?(?:\\n|$))");e&&(h=!1,t=F.exec(e))&&!this.rules.block.hr.test(e);){if(u=t[0],e=e.substring(u.length),a=t[2].split("\n",1)[0],o=e.split("\n",1)[0],this.options.pedantic?(i=2,c=a.trimLeft()):(i=t[2].search(/[^ ]/),c=a.slice(i=4=i||!a.trim())c+="\n"+a.slice(i);else{if(s)break;c+="\n"+a}s||a.trim()||(s=!0),u+=D+"\n",e=e.substring(D.length+1)}f.loose||(l?f.loose=!0:/\n *\n *$/.test(u)&&(l=!0)),this.options.gfm&&(n=/^\[[ xX]\] /.exec(c))&&(r="[ ] "!==n[0],c=c.replace(/^\[[ xX]\] +/,"")),f.items.push({type:"list_item",raw:u,task:!!n,checked:r,loose:!1,text:c}),f.raw+=u}f.items[f.items.length-1].raw=u.trimRight(),f.items[f.items.length-1].text=c.trimRight(),f.raw=f.raw.trimRight();for(var d=f.items.length,C=0;C/i.test(e[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(e[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(e[0])&&(this.lexer.state.inRawBlock=!1),{type:this.options.sanitize?"text":"html",raw:e[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,text:this.options.sanitize?this.options.sanitizer?this.options.sanitizer(e[0]):D(e[0]):e[0]}},t.link=function(e){e=this.rules.inline.link.exec(e);if(e){var t=e[2].trim();if(!this.options.pedantic&&/^$/.test(t))return;var u=k(t.slice(0,-1),"\\");if((t.length-u.length)%2==0)return}else{u=function(e,t){if(-1===e.indexOf(t[1]))return-1;for(var u=e.length,n=0,r=0;r$/.test(t)?u.slice(1):u.slice(1,-1):u)&&u.replace(this.rules.inline._escapes,"$1"),title:r&&r.replace(this.rules.inline._escapes,"$1")},e[0],this.lexer)}},t.reflink=function(e,t){var u;if((u=this.rules.inline.reflink.exec(e))||(u=this.rules.inline.nolink.exec(e)))return(e=t[(e=(u[2]||u[1]).replace(/\s+/g," ")).toLowerCase()])&&e.href?B(u,e,u[0],this.lexer):{type:"text",raw:t=u[0].charAt(0),text:t}},t.emStrong=function(e,t,u){void 0===u&&(u="");var n=this.rules.inline.emStrong.lDelim.exec(e);if(n&&(!n[3]||!u.match(/(?:[0-9A-Za-z\xAA\xB2\xB3\xB5\xB9\xBA\xBC-\xBE\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0560-\u0588\u05D0-\u05EA\u05EF-\u05F2\u0620-\u064A\u0660-\u0669\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07C0-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u0870-\u0887\u0889-\u088E\u08A0-\u08C9\u0904-\u0939\u093D\u0950\u0958-\u0961\u0966-\u096F\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09E6-\u09F1\u09F4-\u09F9\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A66-\u0A6F\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AE6-\u0AEF\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B66-\u0B6F\u0B71-\u0B77\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0BE6-\u0BF2\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C5D\u0C60\u0C61\u0C66-\u0C6F\u0C78-\u0C7E\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDD\u0CDE\u0CE0\u0CE1\u0CE6-\u0CEF\u0CF1\u0CF2\u0D04-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D58-\u0D61\u0D66-\u0D78\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DE6-\u0DEF\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F20-\u0F33\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F-\u1049\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u1090-\u1099\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1369-\u137C\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u1711\u171F-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u17E0-\u17E9\u17F0-\u17F9\u1810-\u1819\u1820-\u1878\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19DA\u1A00-\u1A16\u1A20-\u1A54\u1A80-\u1A89\u1A90-\u1A99\u1AA7\u1B05-\u1B33\u1B45-\u1B4C\u1B50-\u1B59\u1B83-\u1BA0\u1BAE-\u1BE5\u1C00-\u1C23\u1C40-\u1C49\u1C4D-\u1C7D\u1C80-\u1C88\u1C90-\u1CBA\u1CBD-\u1CBF\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5\u1CF6\u1CFA\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2070\u2071\u2074-\u2079\u207F-\u2089\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2150-\u2189\u2460-\u249B\u24EA-\u24FF\u2776-\u2793\u2C00-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2CFD\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u3192-\u3195\u31A0-\u31BF\u31F0-\u31FF\u3220-\u3229\u3248-\u324F\u3251-\u325F\u3280-\u3289\u32B1-\u32BF\u3400-\u4DBF\u4E00-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA7CA\uA7D0\uA7D1\uA7D3\uA7D5-\uA7D9\uA7F2-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA830-\uA835\uA840-\uA873\uA882-\uA8B3\uA8D0-\uA8D9\uA8F2-\uA8F7\uA8FB\uA8FD\uA8FE\uA900-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF-\uA9D9\uA9E0-\uA9E4\uA9E6-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA50-\uAA59\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB69\uAB70-\uABE2\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD07-\uDD33\uDD40-\uDD78\uDD8A\uDD8B\uDE80-\uDE9C\uDEA0-\uDED0\uDEE1-\uDEFB\uDF00-\uDF23\uDF2D-\uDF4A\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCA0-\uDCA9\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDD70-\uDD7A\uDD7C-\uDD8A\uDD8C-\uDD92\uDD94\uDD95\uDD97-\uDDA1\uDDA3-\uDDB1\uDDB3-\uDDB9\uDDBB\uDDBC\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67\uDF80-\uDF85\uDF87-\uDFB0\uDFB2-\uDFBA]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC58-\uDC76\uDC79-\uDC9E\uDCA7-\uDCAF\uDCE0-\uDCF2\uDCF4\uDCF5\uDCFB-\uDD1B\uDD20-\uDD39\uDD80-\uDDB7\uDDBC-\uDDCF\uDDD2-\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE35\uDE40-\uDE48\uDE60-\uDE7E\uDE80-\uDE9F\uDEC0-\uDEC7\uDEC9-\uDEE4\uDEEB-\uDEEF\uDF00-\uDF35\uDF40-\uDF55\uDF58-\uDF72\uDF78-\uDF91\uDFA9-\uDFAF]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2\uDCFA-\uDD23\uDD30-\uDD39\uDE60-\uDE7E\uDE80-\uDEA9\uDEB0\uDEB1\uDF00-\uDF27\uDF30-\uDF45\uDF51-\uDF54\uDF70-\uDF81\uDFB0-\uDFCB\uDFE0-\uDFF6]|\uD804[\uDC03-\uDC37\uDC52-\uDC6F\uDC71\uDC72\uDC75\uDC83-\uDCAF\uDCD0-\uDCE8\uDCF0-\uDCF9\uDD03-\uDD26\uDD36-\uDD3F\uDD44\uDD47\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDD0-\uDDDA\uDDDC\uDDE1-\uDDF4\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDEF0-\uDEF9\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC50-\uDC59\uDC5F-\uDC61\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDCD0-\uDCD9\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE50-\uDE59\uDE80-\uDEAA\uDEB8\uDEC0-\uDEC9\uDF00-\uDF1A\uDF30-\uDF3B\uDF40-\uDF46]|\uD806[\uDC00-\uDC2B\uDCA0-\uDCF2\uDCFF-\uDD06\uDD09\uDD0C-\uDD13\uDD15\uDD16\uDD18-\uDD2F\uDD3F\uDD41\uDD50-\uDD59\uDDA0-\uDDA7\uDDAA-\uDDD0\uDDE1\uDDE3\uDE00\uDE0B-\uDE32\uDE3A\uDE50\uDE5C-\uDE89\uDE9D\uDEB0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC50-\uDC6C\uDC72-\uDC8F\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD30\uDD46\uDD50-\uDD59\uDD60-\uDD65\uDD67\uDD68\uDD6A-\uDD89\uDD98\uDDA0-\uDDA9\uDEE0-\uDEF2\uDFB0\uDFC0-\uDFD4]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|\uD80B[\uDF90-\uDFF0]|[\uD80C\uD81C-\uD820\uD822\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879\uD880-\uD883][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE60-\uDE69\uDE70-\uDEBE\uDEC0-\uDEC9\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF50-\uDF59\uDF5B-\uDF61\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDE40-\uDE96\uDF00-\uDF4A\uDF50\uDF93-\uDF9F\uDFE0\uDFE1\uDFE3]|\uD821[\uDC00-\uDFF7]|\uD823[\uDC00-\uDCD5\uDD00-\uDD08]|\uD82B[\uDFF0-\uDFF3\uDFF5-\uDFFB\uDFFD\uDFFE]|\uD82C[\uDC00-\uDD22\uDD50-\uDD52\uDD64-\uDD67\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD834[\uDEE0-\uDEF3\uDF60-\uDF78]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB\uDFCE-\uDFFF]|\uD837[\uDF00-\uDF1E]|\uD838[\uDD00-\uDD2C\uDD37-\uDD3D\uDD40-\uDD49\uDD4E\uDE90-\uDEAD\uDEC0-\uDEEB\uDEF0-\uDEF9]|\uD839[\uDFE0-\uDFE6\uDFE8-\uDFEB\uDFED\uDFEE\uDFF0-\uDFFE]|\uD83A[\uDC00-\uDCC4\uDCC7-\uDCCF\uDD00-\uDD43\uDD4B\uDD50-\uDD59]|\uD83B[\uDC71-\uDCAB\uDCAD-\uDCAF\uDCB1-\uDCB4\uDD01-\uDD2D\uDD2F-\uDD3D\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD83C[\uDD00-\uDD0C]|\uD83E[\uDFF0-\uDFF9]|\uD869[\uDC00-\uDEDF\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF38\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]|\uD884[\uDC00-\uDF4A])/))){var r=n[1]||n[2]||"";if(!r||""===u||this.rules.inline.punctuation.exec(u)){var i=n[0].length-1,s=i,l=0,a="*"===n[0][0]?this.rules.inline.emStrong.rDelimAst:this.rules.inline.emStrong.rDelimUnd;for(a.lastIndex=0,t=t.slice(-1*e.length+i);null!=(n=a.exec(t));)if(o=n[1]||n[2]||n[3]||n[4]||n[5]||n[6])if(o=o.length,n[3]||n[4])s+=o;else if((n[5]||n[6])&&i%3&&!((i+o)%3))l+=o;else if(!(0<(s-=o))){var o=Math.min(o,o+s+l);if(Math.min(i,o)%2)return D=e.slice(1,i+n.index+o),{type:"em",raw:e.slice(0,i+n.index+o+1),text:D,tokens:this.lexer.inlineTokens(D,[])};var D=e.slice(2,i+n.index+o-1);return{type:"strong",raw:e.slice(0,i+n.index+o+1),text:D,tokens:this.lexer.inlineTokens(D,[])}}}}},t.codespan=function(e){var t,u,n,e=this.rules.inline.code.exec(e);if(e)return n=e[2].replace(/\n/g," "),t=/[^ ]/.test(n),u=/^ /.test(n)&&/ $/.test(n),n=D(n=t&&u?n.substring(1,n.length-1):n,!0),{type:"codespan",raw:e[0],text:n}},t.br=function(e){e=this.rules.inline.br.exec(e);if(e)return{type:"br",raw:e[0]}},t.del=function(e){e=this.rules.inline.del.exec(e);if(e)return{type:"del",raw:e[0],text:e[2],tokens:this.lexer.inlineTokens(e[2],[])}},t.autolink=function(e,t){var u,e=this.rules.inline.autolink.exec(e);if(e)return t="@"===e[2]?"mailto:"+(u=D(this.options.mangle?t(e[1]):e[1])):u=D(e[1]),{type:"link",raw:e[0],text:u,href:t,tokens:[{type:"text",raw:u,text:u}]}},t.url=function(e,t){var u,n,r,i;if(u=this.rules.inline.url.exec(e)){if("@"===u[2])r="mailto:"+(n=D(this.options.mangle?t(u[0]):u[0]));else{for(;i=u[0],u[0]=this.rules.inline._backpedal.exec(u[0])[0],i!==u[0];);n=D(u[0]),r="www."===u[1]?"http://"+n:n}return{type:"link",raw:u[0],text:n,href:r,tokens:[{type:"text",raw:n,text:n}]}}},t.inlineText=function(e,t){e=this.rules.inline.text.exec(e);if(e)return t=this.lexer.state.inRawBlock?this.options.sanitize?this.options.sanitizer?this.options.sanitizer(e[0]):D(e[0]):e[0]:D(this.options.smartypants?t(e[0]):e[0]),{type:"text",raw:e[0],text:t}},e}(),y={newline:/^(?: *(?:\n|$))+/,code:/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,fences:/^ {0,3}(`{3,}(?=[^`\n]*\n)|~{3,})([^\n]*)\n(?:|([\s\S]*?)\n)(?: {0,3}\1[~`]* *(?=\n|$)|$)/,hr:/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,blockquote:/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/,list:/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/,html:"^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$))",def:/^ {0,3}\[(label)\]: *(?:\n *)?]+)>?(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/,table:A,lheading:/^([^\n]+)\n {0,3}(=+|-+) *(?:\n+|$)/,_paragraph:/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,text:/^[^\n]+/,_label:/(?!\s*\])(?:\\.|[^\[\]\\])+/,_title:/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/},v=(y.def=p(y.def).replace("label",y._label).replace("title",y._title).getRegex(),y.bullet=/(?:[*+-]|\d{1,9}[.)])/,y.listItemStart=p(/^( *)(bull) */).replace("bull",y.bullet).getRegex(),y.list=p(y.list).replace(/bull/g,y.bullet).replace("hr","\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))").replace("def","\\n+(?="+y.def.source+")").getRegex(),y._tag="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",y._comment=/|$)/,y.html=p(y.html,"i").replace("comment",y._comment).replace("tag",y._tag).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),y.paragraph=p(y._paragraph).replace("hr",y.hr).replace("heading"," {0,3}#{1,6} ").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",y._tag).getRegex(),y.blockquote=p(y.blockquote).replace("paragraph",y.paragraph).getRegex(),y.normal=d({},y),y.gfm=d({},y.normal,{table:"^ *([^\\n ].*\\|.*)\\n {0,3}(?:\\| *)?(:?-+:? *(?:\\| *:?-+:? *)*)(?:\\| *)?(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)"}),y.gfm.table=p(y.gfm.table).replace("hr",y.hr).replace("heading"," {0,3}#{1,6} ").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",y._tag).getRegex(),y.gfm.paragraph=p(y._paragraph).replace("hr",y.hr).replace("heading"," {0,3}#{1,6} ").replace("|lheading","").replace("table",y.gfm.table).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",y._tag).getRegex(),y.pedantic=d({},y.normal,{html:p("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",y._comment).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:A,paragraph:p(y.normal._paragraph).replace("hr",y.hr).replace("heading"," *#{1,6} *[^\n]").replace("lheading",y.lheading).replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").getRegex()}),{escape:/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,autolink:/^<(scheme:[^\s\x00-\x1f<>]*|email)>/,url:A,tag:"^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^",link:/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/,reflink:/^!?\[(label)\]\[(ref)\]/,nolink:/^!?\[(ref)\](?:\[\])?/,reflinkSearch:"reflink|nolink(?!\\()",emStrong:{lDelim:/^(?:\*+(?:([punct_])|[^\s*]))|^_+(?:([punct*])|([^\s_]))/,rDelimAst:/^[^_*]*?\_\_[^_*]*?\*[^_*]*?(?=\_\_)|[^*]+(?=[^*])|[punct_](\*+)(?=[\s]|$)|[^punct*_\s](\*+)(?=[punct_\s]|$)|[punct_\s](\*+)(?=[^punct*_\s])|[\s](\*+)(?=[punct_])|[punct_](\*+)(?=[punct_])|[^punct*_\s](\*+)(?=[^punct*_\s])/,rDelimUnd:/^[^_*]*?\*\*[^_*]*?\_[^_*]*?(?=\*\*)|[^_]+(?=[^_])|[punct*](\_+)(?=[\s]|$)|[^punct*_\s](\_+)(?=[punct*\s]|$)|[punct*\s](\_+)(?=[^punct*_\s])|[\s](\_+)(?=[punct*])|[punct*](\_+)(?=[punct*])/},code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,br:/^( {2,}|\\)\n(?!\s*$)/,del:A,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\?@\\[\\]`^{|}~",v.punctuation=p(v.punctuation).replace(/punctuation/g,v._punctuation).getRegex(),v.blockSkip=/\[[^\]]*?\]\([^\)]*?\)|`[^`]*?`|<[^>]*?>/g,v.escapedEmSt=/\\\*|\\_/g,v._comment=p(y._comment).replace("(?:--\x3e|$)","--\x3e").getRegex(),v.emStrong.lDelim=p(v.emStrong.lDelim).replace(/punct/g,v._punctuation).getRegex(),v.emStrong.rDelimAst=p(v.emStrong.rDelimAst,"g").replace(/punct/g,v._punctuation).getRegex(),v.emStrong.rDelimUnd=p(v.emStrong.rDelimUnd,"g").replace(/punct/g,v._punctuation).getRegex(),v._escapes=/\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g,v._scheme=/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/,v._email=/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/,v.autolink=p(v.autolink).replace("scheme",v._scheme).replace("email",v._email).getRegex(),v._attribute=/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/,v.tag=p(v.tag).replace("comment",v._comment).replace("attribute",v._attribute).getRegex(),v._label=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,v._href=/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/,v._title=/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/,v.link=p(v.link).replace("label",v._label).replace("href",v._href).replace("title",v._title).getRegex(),v.reflink=p(v.reflink).replace("label",v._label).replace("ref",y._label).getRegex(),v.nolink=p(v.nolink).replace("ref",y._label).getRegex(),v.reflinkSearch=p(v.reflinkSearch,"g").replace("reflink",v.reflink).replace("nolink",v.nolink).getRegex(),v.normal=d({},v),v.pedantic=d({},v.normal,{strong:{start:/^__|\*\*/,middle:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,endAst:/\*\*(?!\*)/g,endUnd:/__(?!_)/g},em:{start:/^_|\*/,middle:/^()\*(?=\S)([\s\S]*?\S)\*(?!\*)|^_(?=\S)([\s\S]*?\S)_(?!_)/,endAst:/\*(?!\*)/g,endUnd:/_(?!_)/g},link:p(/^!?\[(label)\]\((.*?)\)/).replace("label",v._label).getRegex(),reflink:p(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",v._label).getRegex()}),v.gfm=d({},v.normal,{escape:p(v.escape).replace("])","~|])").getRegex(),_extended_email:/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/,url:/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,_backpedal:/(?:[^?!.,:;*_~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])([\s\S]*?[^\s~])\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\'+(u?e:D(e,!0))+"\n":"
"+(u?e:D(e,!0))+"
\n"},t.blockquote=function(e){return"
\n"+e+"
\n"},t.html=function(e){return e},t.heading=function(e,t,u,n){return this.options.headerIds?"'+e+"\n":""+e+"\n"},t.hr=function(){return this.options.xhtml?"
\n":"
\n"},t.list=function(e,t,u){var n=t?"ol":"ul";return"<"+n+(t&&1!==u?' start="'+u+'"':"")+">\n"+e+"\n"},t.listitem=function(e){return"
  • "+e+"
  • \n"},t.checkbox=function(e){return" "},t.paragraph=function(e){return"

    "+e+"

    \n"},t.table=function(e,t){return"\n\n"+e+"\n"+(t=t&&""+t+"")+"
    \n"},t.tablerow=function(e){return"\n"+e+"\n"},t.tablecell=function(e,t){var u=t.header?"th":"td";return(t.align?"<"+u+' align="'+t.align+'">':"<"+u+">")+e+"\n"},t.strong=function(e){return""+e+""},t.em=function(e){return""+e+""},t.codespan=function(e){return""+e+""},t.br=function(){return this.options.xhtml?"
    ":"
    "},t.del=function(e){return""+e+""},t.link=function(e,t,u){if(null===(e=g(this.options.sanitize,this.options.baseUrl,e)))return u;e='"},t.image=function(e,t,u){if(null===(e=g(this.options.sanitize,this.options.baseUrl,e)))return u;e=''+u+'":">"},t.text=function(e){return e},e}(),S=function(){function e(){}var t=e.prototype;return t.strong=function(e){return e},t.em=function(e){return e},t.codespan=function(e){return e},t.del=function(e){return e},t.html=function(e){return e},t.text=function(e){return e},t.link=function(e,t,u){return""+u},t.image=function(e,t,u){return""+u},t.br=function(){return""},e}(),T=function(){function e(){this.seen={}}var t=e.prototype;return t.serialize=function(e){return e.toLowerCase().trim().replace(/<[!\/a-z].*?>/gi,"").replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g,"").replace(/\s/g,"-")},t.getNextSafeSlug=function(e,t){var u=e,n=0;if(this.seen.hasOwnProperty(u))for(n=this.seen[e];u=e+"-"+ ++n,this.seen.hasOwnProperty(u););return t||(this.seen[e]=n,this.seen[u]=0),u},t.slug=function(e,t){void 0===t&&(t={});e=this.serialize(e);return this.getNextSafeSlug(e,t.dryrun)},e}(),R=function(){function u(e){this.options=e||r.defaults,this.options.renderer=this.options.renderer||new $,this.renderer=this.options.renderer,this.renderer.options=this.options,this.textRenderer=new S,this.slugger=new T}u.parse=function(e,t){return new u(t).parse(e)},u.parseInline=function(e,t){return new u(t).parseInline(e)};var e=u.prototype;return e.parse=function(e,t){void 0===t&&(t=!0);for(var u,n,r,i,s,l,a,o,D,c,h,p,f,g,F,A,d="",C=e.length,k=0;kAn error occurred:

    "+D(e.message+"",!0)+"
    ";throw e}}I.options=I.setOptions=function(e){return d(I.defaults,e),e=I.defaults,r.defaults=e,I},I.getDefaults=e,I.defaults=r.defaults,I.use=function(){for(var e=arguments.length,t=new Array(e),u=0;uAn error occurred:

    "+D(e.message+"",!0)+"
    ";throw e}},I.Parser=R,I.parser=R.parse,I.Renderer=$,I.TextRenderer=S,I.Lexer=z,I.lexer=z.lex,I.Tokenizer=w,I.Slugger=T;var A=(I.parse=I).options,P=I.setOptions,Q=I.use,U=I.walkTokens,M=I.parseInline,N=I,X=R.parse,G=z.lex;r.Lexer=z,r.Parser=R,r.Renderer=$,r.Slugger=T,r.TextRenderer=S,r.Tokenizer=w,r.getDefaults=e,r.lexer=G,r.marked=I,r.options=A,r.parse=N,r.parseInline=M,r.parser=X,r.setOptions=P,r.use=Q,r.walkTokens=U,Object.defineProperty(r,"__esModule",{value:!0})}); +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).marked={})}(this,(function(e){"use strict";function t(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}function n(t){e.defaults=t}e.defaults={async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null};const s=/[&<>"']/,r=new RegExp(s.source,"g"),i=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,l=new RegExp(i.source,"g"),o={"&":"&","<":"<",">":">",'"':""","'":"'"},a=e=>o[e];function c(e,t){if(t){if(s.test(e))return e.replace(r,a)}else if(i.test(e))return e.replace(l,a);return e}const h=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;function p(e){return e.replace(h,((e,t)=>"colon"===(t=t.toLowerCase())?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""))}const u=/(^|[^\[])\^/g;function k(e,t){let n="string"==typeof e?e:e.source;t=t||"";const s={replace:(e,t)=>{let r="string"==typeof t?t:t.source;return r=r.replace(u,"$1"),n=n.replace(e,r),s},getRegex:()=>new RegExp(n,t)};return s}function g(e){try{e=encodeURI(e).replace(/%25/g,"%")}catch(e){return null}return e}const f={exec:()=>null};function d(e,t){const n=e.replace(/\|/g,((e,t,n)=>{let s=!1,r=t;for(;--r>=0&&"\\"===n[r];)s=!s;return s?"|":" |"})).split(/ \|/);let s=0;if(n[0].trim()||n.shift(),n.length>0&&!n[n.length-1].trim()&&n.pop(),t)if(n.length>t)n.splice(t);else for(;n.length0)return{type:"space",raw:t[0]}}code(e){const t=this.rules.block.code.exec(e);if(t){const e=t[0].replace(/^ {1,4}/gm,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?e:x(e,"\n")}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const e=t[0],n=function(e,t){const n=e.match(/^(\s+)(?:```)/);if(null===n)return t;const s=n[1];return t.split("\n").map((e=>{const t=e.match(/^\s+/);if(null===t)return e;const[n]=t;return n.length>=s.length?e.slice(s.length):e})).join("\n")}(e,t[3]||"");return{type:"code",raw:e,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:n}}}heading(e){const t=this.rules.block.heading.exec(e);if(t){let e=t[2].trim();if(/#$/.test(e)){const t=x(e,"#");this.options.pedantic?e=t.trim():t&&!/ $/.test(t)||(e=t.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:e,tokens:this.lexer.inline(e)}}}hr(e){const t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:t[0]}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){let e=t[0].replace(/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,"\n $1");e=x(e.replace(/^ *>[ \t]?/gm,""),"\n");const n=this.lexer.state.top;this.lexer.state.top=!0;const s=this.lexer.blockTokens(e);return this.lexer.state.top=n,{type:"blockquote",raw:t[0],tokens:s,text:e}}}list(e){let t=this.rules.block.list.exec(e);if(t){let n=t[1].trim();const s=n.length>1,r={type:"list",raw:"",ordered:s,start:s?+n.slice(0,-1):"",loose:!1,items:[]};n=s?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=s?n:"[*+-]");const i=new RegExp(`^( {0,3}${n})((?:[\t ][^\\n]*)?(?:\\n|$))`);let l="",o="",a=!1;for(;e;){let n=!1;if(!(t=i.exec(e)))break;if(this.rules.block.hr.test(e))break;l=t[0],e=e.substring(l.length);let s=t[2].split("\n",1)[0].replace(/^\t+/,(e=>" ".repeat(3*e.length))),c=e.split("\n",1)[0],h=0;this.options.pedantic?(h=2,o=s.trimStart()):(h=t[2].search(/[^ ]/),h=h>4?1:h,o=s.slice(h),h+=t[1].length);let p=!1;if(!s&&/^ *$/.test(c)&&(l+=c+"\n",e=e.substring(c.length+1),n=!0),!n){const t=new RegExp(`^ {0,${Math.min(3,h-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`),n=new RegExp(`^ {0,${Math.min(3,h-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),r=new RegExp(`^ {0,${Math.min(3,h-1)}}(?:\`\`\`|~~~)`),i=new RegExp(`^ {0,${Math.min(3,h-1)}}#`);for(;e;){const a=e.split("\n",1)[0];if(c=a,this.options.pedantic&&(c=c.replace(/^ {1,4}(?=( {4})*[^ ])/g," ")),r.test(c))break;if(i.test(c))break;if(t.test(c))break;if(n.test(e))break;if(c.search(/[^ ]/)>=h||!c.trim())o+="\n"+c.slice(h);else{if(p)break;if(s.search(/[^ ]/)>=4)break;if(r.test(s))break;if(i.test(s))break;if(n.test(s))break;o+="\n"+c}p||c.trim()||(p=!0),l+=a+"\n",e=e.substring(a.length+1),s=c.slice(h)}}r.loose||(a?r.loose=!0:/\n *\n *$/.test(l)&&(a=!0));let u,k=null;this.options.gfm&&(k=/^\[[ xX]\] /.exec(o),k&&(u="[ ] "!==k[0],o=o.replace(/^\[[ xX]\] +/,""))),r.items.push({type:"list_item",raw:l,task:!!k,checked:u,loose:!1,text:o,tokens:[]}),r.raw+=l}r.items[r.items.length-1].raw=l.trimEnd(),r.items[r.items.length-1].text=o.trimEnd(),r.raw=r.raw.trimEnd();for(let e=0;e"space"===e.type)),n=t.length>0&&t.some((e=>/\n.*\n/.test(e.raw)));r.loose=n}if(r.loose)for(let e=0;e$/,"$1").replace(this.rules.inline.anyPunctuation,"$1"):"",s=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline.anyPunctuation,"$1"):t[3];return{type:"def",tag:e,raw:t[0],href:n,title:s}}}table(e){const t=this.rules.block.table.exec(e);if(!t)return;if(!/[:|]/.test(t[2]))return;const n=d(t[1]),s=t[2].replace(/^\||\| *$/g,"").split("|"),r=t[3]&&t[3].trim()?t[3].replace(/\n[ \t]*$/,"").split("\n"):[],i={type:"table",raw:t[0],header:[],align:[],rows:[]};if(n.length===s.length){for(const e of s)/^ *-+: *$/.test(e)?i.align.push("right"):/^ *:-+: *$/.test(e)?i.align.push("center"):/^ *:-+ *$/.test(e)?i.align.push("left"):i.align.push(null);for(const e of n)i.header.push({text:e,tokens:this.lexer.inline(e)});for(const e of r)i.rows.push(d(e,i.header.length).map((e=>({text:e,tokens:this.lexer.inline(e)}))));return i}}lheading(e){const t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:"="===t[2].charAt(0)?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){const t=this.rules.block.paragraph.exec(e);if(t){const e="\n"===t[1].charAt(t[1].length-1)?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:e,tokens:this.lexer.inline(e)}}}text(e){const t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){const t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:c(t[1])}}tag(e){const t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&/^
    /i.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){const t=this.rules.inline.link.exec(e);if(t){const e=t[2].trim();if(!this.options.pedantic&&/^$/.test(e))return;const t=x(e.slice(0,-1),"\\");if((e.length-t.length)%2==0)return}else{const e=function(e,t){if(-1===e.indexOf(t[1]))return-1;let n=0;for(let s=0;s-1){const n=(0===t[0].indexOf("!")?5:4)+t[1].length+e;t[2]=t[2].substring(0,e),t[0]=t[0].substring(0,n).trim(),t[3]=""}}let n=t[2],s="";if(this.options.pedantic){const e=/^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(n);e&&(n=e[1],s=e[3])}else s=t[3]?t[3].slice(1,-1):"";return n=n.trim(),/^$/.test(e)?n.slice(1):n.slice(1,-1)),b(t,{href:n?n.replace(this.rules.inline.anyPunctuation,"$1"):n,title:s?s.replace(this.rules.inline.anyPunctuation,"$1"):s},t[0],this.lexer)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){const e=t[(n[2]||n[1]).replace(/\s+/g," ").toLowerCase()];if(!e){const e=n[0].charAt(0);return{type:"text",raw:e,text:e}}return b(n,e,n[0],this.lexer)}}emStrong(e,t,n=""){let s=this.rules.inline.emStrongLDelim.exec(e);if(!s)return;if(s[3]&&n.match(/[\p{L}\p{N}]/u))return;if(!(s[1]||s[2]||"")||!n||this.rules.inline.punctuation.exec(n)){const n=[...s[0]].length-1;let r,i,l=n,o=0;const a="*"===s[0][0]?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(a.lastIndex=0,t=t.slice(-1*e.length+n);null!=(s=a.exec(t));){if(r=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!r)continue;if(i=[...r].length,s[3]||s[4]){l+=i;continue}if((s[5]||s[6])&&n%3&&!((n+i)%3)){o+=i;continue}if(l-=i,l>0)continue;i=Math.min(i,i+l+o);const t=[...s[0]][0].length,a=e.slice(0,n+s.index+t+i);if(Math.min(n,i)%2){const e=a.slice(1,-1);return{type:"em",raw:a,text:e,tokens:this.lexer.inlineTokens(e)}}const c=a.slice(2,-2);return{type:"strong",raw:a,text:c,tokens:this.lexer.inlineTokens(c)}}}}codespan(e){const t=this.rules.inline.code.exec(e);if(t){let e=t[2].replace(/\n/g," ");const n=/[^ ]/.test(e),s=/^ /.test(e)&&/ $/.test(e);return n&&s&&(e=e.substring(1,e.length-1)),e=c(e,!0),{type:"codespan",raw:t[0],text:e}}}br(e){const t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){const t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){const t=this.rules.inline.autolink.exec(e);if(t){let e,n;return"@"===t[2]?(e=c(t[1]),n="mailto:"+e):(e=c(t[1]),n=e),{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let e,n;if("@"===t[2])e=c(t[0]),n="mailto:"+e;else{let s;do{s=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??""}while(s!==t[0]);e=c(t[0]),n="www."===t[1]?"http://"+t[0]:t[0]}return{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}inlineText(e){const t=this.rules.inline.text.exec(e);if(t){let e;return e=this.lexer.state.inRawBlock?t[0]:c(t[0]),{type:"text",raw:t[0],text:e}}}}const m=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,y=/(?:[*+-]|\d{1,9}[.)])/,$=k(/^(?!bull |blockCode|fences|blockquote|heading|html)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html))+?)\n {0,3}(=+|-+) *(?:\n+|$)/).replace(/bull/g,y).replace(/blockCode/g,/ {4}/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).getRegex(),z=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,T=/(?!\s*\])(?:\\.|[^\[\]\\])+/,R=k(/^ {0,3}\[(label)\]: *(?:\n *)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/).replace("label",T).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),_=k(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,y).getRegex(),A="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",S=/|$))/,I=k("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$))","i").replace("comment",S).replace("tag",A).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),E=k(z).replace("hr",m).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",A).getRegex(),q={blockquote:k(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",E).getRegex(),code:/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,def:R,fences:/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,hr:m,html:I,lheading:$,list:_,newline:/^(?: *(?:\n|$))+/,paragraph:E,table:f,text:/^[^\n]+/},Z=k("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",m).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",A).getRegex(),L={...q,table:Z,paragraph:k(z).replace("hr",m).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",Z).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",A).getRegex()},P={...q,html:k("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",S).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:f,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:k(z).replace("hr",m).replace("heading"," *#{1,6} *[^\n]").replace("lheading",$).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},Q=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,v=/^( {2,}|\\)\n(?!\s*$)/,B="\\p{P}\\p{S}",C=k(/^((?![*_])[\spunctuation])/,"u").replace(/punctuation/g,B).getRegex(),M=k(/^(?:\*+(?:((?!\*)[punct])|[^\s*]))|^_+(?:((?!_)[punct])|([^\s_]))/,"u").replace(/punct/g,B).getRegex(),O=k("^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)[punct](\\*+)(?=[\\s]|$)|[^punct\\s](\\*+)(?!\\*)(?=[punct\\s]|$)|(?!\\*)[punct\\s](\\*+)(?=[^punct\\s])|[\\s](\\*+)(?!\\*)(?=[punct])|(?!\\*)[punct](\\*+)(?!\\*)(?=[punct])|[^punct\\s](\\*+)(?=[^punct\\s])","gu").replace(/punct/g,B).getRegex(),D=k("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)[punct](_+)(?=[\\s]|$)|[^punct\\s](_+)(?!_)(?=[punct\\s]|$)|(?!_)[punct\\s](_+)(?=[^punct\\s])|[\\s](_+)(?!_)(?=[punct])|(?!_)[punct](_+)(?!_)(?=[punct])","gu").replace(/punct/g,B).getRegex(),j=k(/\\([punct])/,"gu").replace(/punct/g,B).getRegex(),H=k(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),U=k(S).replace("(?:--\x3e|$)","--\x3e").getRegex(),X=k("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",U).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),F=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,N=k(/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/).replace("label",F).replace("href",/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),G=k(/^!?\[(label)\]\[(ref)\]/).replace("label",F).replace("ref",T).getRegex(),J=k(/^!?\[(ref)\](?:\[\])?/).replace("ref",T).getRegex(),K={_backpedal:f,anyPunctuation:j,autolink:H,blockSkip:/\[[^[\]]*?\]\([^\(\)]*?\)|`[^`]*?`|<[^<>]*?>/g,br:v,code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,del:f,emStrongLDelim:M,emStrongRDelimAst:O,emStrongRDelimUnd:D,escape:Q,link:N,nolink:J,punctuation:C,reflink:G,reflinkSearch:k("reflink|nolink(?!\\()","g").replace("reflink",G).replace("nolink",J).getRegex(),tag:X,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\t+" ".repeat(n.length)));e;)if(!(this.options.extensions&&this.options.extensions.block&&this.options.extensions.block.some((s=>!!(n=s.call({lexer:this},e,t))&&(e=e.substring(n.raw.length),t.push(n),!0)))))if(n=this.tokenizer.space(e))e=e.substring(n.raw.length),1===n.raw.length&&t.length>0?t[t.length-1].raw+="\n":t.push(n);else if(n=this.tokenizer.code(e))e=e.substring(n.raw.length),s=t[t.length-1],!s||"paragraph"!==s.type&&"text"!==s.type?t.push(n):(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue[this.inlineQueue.length-1].src=s.text);else if(n=this.tokenizer.fences(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.heading(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.hr(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.blockquote(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.list(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.html(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.def(e))e=e.substring(n.raw.length),s=t[t.length-1],!s||"paragraph"!==s.type&&"text"!==s.type?this.tokens.links[n.tag]||(this.tokens.links[n.tag]={href:n.href,title:n.title}):(s.raw+="\n"+n.raw,s.text+="\n"+n.raw,this.inlineQueue[this.inlineQueue.length-1].src=s.text);else if(n=this.tokenizer.table(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.lheading(e))e=e.substring(n.raw.length),t.push(n);else{if(r=e,this.options.extensions&&this.options.extensions.startBlock){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startBlock.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(r=e.substring(0,t+1))}if(this.state.top&&(n=this.tokenizer.paragraph(r)))s=t[t.length-1],i&&"paragraph"===s.type?(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=s.text):t.push(n),i=r.length!==e.length,e=e.substring(n.raw.length);else if(n=this.tokenizer.text(e))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===s.type?(s.raw+="\n"+n.raw,s.text+="\n"+n.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=s.text):t.push(n);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n,s,r,i,l,o,a=e;if(this.tokens.links){const e=Object.keys(this.tokens.links);if(e.length>0)for(;null!=(i=this.tokenizer.rules.inline.reflinkSearch.exec(a));)e.includes(i[0].slice(i[0].lastIndexOf("[")+1,-1))&&(a=a.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;null!=(i=this.tokenizer.rules.inline.blockSkip.exec(a));)a=a.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);for(;null!=(i=this.tokenizer.rules.inline.anyPunctuation.exec(a));)a=a.slice(0,i.index)+"++"+a.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;e;)if(l||(o=""),l=!1,!(this.options.extensions&&this.options.extensions.inline&&this.options.extensions.inline.some((s=>!!(n=s.call({lexer:this},e,t))&&(e=e.substring(n.raw.length),t.push(n),!0)))))if(n=this.tokenizer.escape(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.tag(e))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.link(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.reflink(e,this.tokens.links))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.emStrong(e,a,o))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.codespan(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.br(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.del(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.autolink(e))e=e.substring(n.raw.length),t.push(n);else if(this.state.inLink||!(n=this.tokenizer.url(e))){if(r=e,this.options.extensions&&this.options.extensions.startInline){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startInline.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(r=e.substring(0,t+1))}if(n=this.tokenizer.inlineText(r))e=e.substring(n.raw.length),"_"!==n.raw.slice(-1)&&(o=n.raw.slice(-1)),l=!0,s=t[t.length-1],s&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}else e=e.substring(n.raw.length),t.push(n);return t}}class se{options;constructor(t){this.options=t||e.defaults}code(e,t,n){const s=(t||"").match(/^\S*/)?.[0];return e=e.replace(/\n$/,"")+"\n",s?'
    '+(n?e:c(e,!0))+"
    \n":"
    "+(n?e:c(e,!0))+"
    \n"}blockquote(e){return`
    \n${e}
    \n`}html(e,t){return e}heading(e,t,n){return`${e}\n`}hr(){return"
    \n"}list(e,t,n){const s=t?"ol":"ul";return"<"+s+(t&&1!==n?' start="'+n+'"':"")+">\n"+e+"\n"}listitem(e,t,n){return`
  • ${e}
  • \n`}checkbox(e){return"'}paragraph(e){return`

    ${e}

    \n`}table(e,t){return t&&(t=`${t}`),"\n\n"+e+"\n"+t+"
    \n"}tablerow(e){return`\n${e}\n`}tablecell(e,t){const n=t.header?"th":"td";return(t.align?`<${n} align="${t.align}">`:`<${n}>`)+e+`\n`}strong(e){return`${e}`}em(e){return`${e}`}codespan(e){return`${e}`}br(){return"
    "}del(e){return`${e}`}link(e,t,n){const s=g(e);if(null===s)return n;let r='
    ",r}image(e,t,n){const s=g(e);if(null===s)return n;let r=`${n}0&&"paragraph"===n.tokens[0].type?(n.tokens[0].text=e+" "+n.tokens[0].text,n.tokens[0].tokens&&n.tokens[0].tokens.length>0&&"text"===n.tokens[0].tokens[0].type&&(n.tokens[0].tokens[0].text=e+" "+n.tokens[0].tokens[0].text)):n.tokens.unshift({type:"text",text:e+" "}):o+=e+" "}o+=this.parse(n.tokens,i),l+=this.renderer.listitem(o,r,!!s)}n+=this.renderer.list(l,t,s);continue}case"html":{const e=r;n+=this.renderer.html(e.text,e.block);continue}case"paragraph":{const e=r;n+=this.renderer.paragraph(this.parseInline(e.tokens));continue}case"text":{let i=r,l=i.tokens?this.parseInline(i.tokens):i.text;for(;s+1{const r=e[s].flat(1/0);n=n.concat(this.walkTokens(r,t))})):e.tokens&&(n=n.concat(this.walkTokens(e.tokens,t)))}}return n}use(...e){const t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach((e=>{const n={...e};if(n.async=this.defaults.async||n.async||!1,e.extensions&&(e.extensions.forEach((e=>{if(!e.name)throw new Error("extension name required");if("renderer"in e){const n=t.renderers[e.name];t.renderers[e.name]=n?function(...t){let s=e.renderer.apply(this,t);return!1===s&&(s=n.apply(this,t)),s}:e.renderer}if("tokenizer"in e){if(!e.level||"block"!==e.level&&"inline"!==e.level)throw new Error("extension level must be 'block' or 'inline'");const n=t[e.level];n?n.unshift(e.tokenizer):t[e.level]=[e.tokenizer],e.start&&("block"===e.level?t.startBlock?t.startBlock.push(e.start):t.startBlock=[e.start]:"inline"===e.level&&(t.startInline?t.startInline.push(e.start):t.startInline=[e.start]))}"childTokens"in e&&e.childTokens&&(t.childTokens[e.name]=e.childTokens)})),n.extensions=t),e.renderer){const t=this.defaults.renderer||new se(this.defaults);for(const n in e.renderer){if(!(n in t))throw new Error(`renderer '${n}' does not exist`);if("options"===n)continue;const s=n,r=e.renderer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n||""}}n.renderer=t}if(e.tokenizer){const t=this.defaults.tokenizer||new w(this.defaults);for(const n in e.tokenizer){if(!(n in t))throw new Error(`tokenizer '${n}' does not exist`);if(["options","rules","lexer"].includes(n))continue;const s=n,r=e.tokenizer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.tokenizer=t}if(e.hooks){const t=this.defaults.hooks||new le;for(const n in e.hooks){if(!(n in t))throw new Error(`hook '${n}' does not exist`);if("options"===n)continue;const s=n,r=e.hooks[s],i=t[s];le.passThroughHooks.has(n)?t[s]=e=>{if(this.defaults.async)return Promise.resolve(r.call(t,e)).then((e=>i.call(t,e)));const n=r.call(t,e);return i.call(t,n)}:t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.hooks=t}if(e.walkTokens){const t=this.defaults.walkTokens,s=e.walkTokens;n.walkTokens=function(e){let n=[];return n.push(s.call(this,e)),t&&(n=n.concat(t.call(this,e))),n}}this.defaults={...this.defaults,...n}})),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return ne.lex(e,t??this.defaults)}parser(e,t){return ie.parse(e,t??this.defaults)}#e(e,t){return(n,s)=>{const r={...s},i={...this.defaults,...r};!0===this.defaults.async&&!1===r.async&&(i.silent||console.warn("marked(): The async option was set to true by an extension. The async: false option sent to parse will be ignored."),i.async=!0);const l=this.#t(!!i.silent,!!i.async);if(null==n)return l(new Error("marked(): input parameter is undefined or null"));if("string"!=typeof n)return l(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(n)+", string expected"));if(i.hooks&&(i.hooks.options=i),i.async)return Promise.resolve(i.hooks?i.hooks.preprocess(n):n).then((t=>e(t,i))).then((e=>i.hooks?i.hooks.processAllTokens(e):e)).then((e=>i.walkTokens?Promise.all(this.walkTokens(e,i.walkTokens)).then((()=>e)):e)).then((e=>t(e,i))).then((e=>i.hooks?i.hooks.postprocess(e):e)).catch(l);try{i.hooks&&(n=i.hooks.preprocess(n));let s=e(n,i);i.hooks&&(s=i.hooks.processAllTokens(s)),i.walkTokens&&this.walkTokens(s,i.walkTokens);let r=t(s,i);return i.hooks&&(r=i.hooks.postprocess(r)),r}catch(e){return l(e)}}}#t(e,t){return n=>{if(n.message+="\nPlease report this to https://github.com/markedjs/marked.",e){const e="

    An error occurred:

    "+c(n.message+"",!0)+"
    ";return t?Promise.resolve(e):e}if(t)return Promise.reject(n);throw n}}}const ae=new oe;function ce(e,t){return ae.parse(e,t)}ce.options=ce.setOptions=function(e){return ae.setOptions(e),ce.defaults=ae.defaults,n(ce.defaults),ce},ce.getDefaults=t,ce.defaults=e.defaults,ce.use=function(...e){return ae.use(...e),ce.defaults=ae.defaults,n(ce.defaults),ce},ce.walkTokens=function(e,t){return ae.walkTokens(e,t)},ce.parseInline=ae.parseInline,ce.Parser=ie,ce.parser=ie.parse,ce.Renderer=se,ce.TextRenderer=re,ce.Lexer=ne,ce.lexer=ne.lex,ce.Tokenizer=w,ce.Hooks=le,ce.parse=ce;const he=ce.options,pe=ce.setOptions,ue=ce.use,ke=ce.walkTokens,ge=ce.parseInline,fe=ce,de=ie.parse,xe=ne.lex;e.Hooks=le,e.Lexer=ne,e.Marked=oe,e.Parser=ie,e.Renderer=se,e.TextRenderer=re,e.Tokenizer=w,e.getDefaults=t,e.lexer=xe,e.marked=ce,e.options=he,e.parse=fe,e.parseInline=ge,e.parser=de,e.setOptions=pe,e.use=ue,e.walkTokens=ke})); +/** + * Minified by jsDelivr using Terser v5.19.2. + * Original file: /npm/marked-highlight@2.1.1/lib/index.umd.js + * + * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files + */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).markedHighlight={})}(this,(function(e){"use strict";function t(e){return(e||"").match(/\S*/)[0]}function n(e){return t=>{"string"==typeof t&&t!==e.text&&(e.escaped=!0,e.text=t)}}const i=/[&<>"']/,o=new RegExp(i.source,"g"),r=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,g=new RegExp(r.source,"g"),h={"&":"&","<":"<",">":">",'"':""","'":"'"},s=e=>h[e];function c(e,t){if(t){if(i.test(e))return e.replace(o,s)}else if(r.test(e))return e.replace(g,s);return e}e.markedHighlight=function(e){if("function"==typeof e&&(e={highlight:e}),!e||"function"!=typeof e.highlight)throw new Error("Must provide highlight function");return"string"!=typeof e.langPrefix&&(e.langPrefix="language-"),{async:!!e.async,walkTokens(i){if("code"!==i.type)return;const o=t(i.lang);if(e.async)return Promise.resolve(e.highlight(i.text,o,i.lang||"")).then(n(i));const r=e.highlight(i.text,o,i.lang||"");if(r instanceof Promise)throw new Error("markedHighlight is not set to async but the highlight function is async. Set the async option to true on markedHighlight to await the async highlight function.");n(i)(r)},renderer:{code(n,i,o){const r=t(i),g=r?` class="${e.langPrefix}${c(r)}"`:"";return n=n.replace(/\n$/,""),`
    ${o?n:c(n,!0)}\n
    `}}}}})); +//# sourceMappingURL=/sm/3bfb625a4ed441ddc1f215743851a4b727156eef53b458bd31c51a627ce891c9.map \ No newline at end of file diff --git a/gno.land/pkg/gnoweb/static/js/renderer.js b/gno.land/pkg/gnoweb/static/js/renderer.js index 174a89c7a64..4937b5a5691 100644 --- a/gno.land/pkg/gnoweb/static/js/renderer.js +++ b/gno.land/pkg/gnoweb/static/js/renderer.js @@ -8,10 +8,26 @@ function renderUsernames(raw) { } function parseContent(source) { - marked.setOptions({ gfm: true }); + const { markedHighlight } = globalThis.markedHighlight; + const { Marked } = globalThis.marked; + const markedInstance = new Marked( + markedHighlight({ + langPrefix: 'language-', + highlight(code, lang, info) { + if (lang === "json") { + try { + code = JSON.stringify(JSON.parse(code), null, 2); + } catch {} + } + const language = hljs.getLanguage(lang) ? lang : 'plaintext'; + return hljs.highlight(code, { language }).value; + } + }) + ); + markedInstance.setOptions({ gfm: true }); const doc = new DOMParser().parseFromString(source, "text/html"); const contents = doc.documentElement.textContent; - return marked.parse(contents); + return markedInstance.parse(contents); } /* diff --git a/gno.land/pkg/gnoweb/views/funcs.html b/gno.land/pkg/gnoweb/views/funcs.html index 683e2d28328..74952eac52d 100644 --- a/gno.land/pkg/gnoweb/views/funcs.html +++ b/gno.land/pkg/gnoweb/views/funcs.html @@ -130,6 +130,7 @@ {{- end -}} {{- define "js" -}} + From e5fe773ae3c7b0f820610e61743335032ed15dd3 Mon Sep 17 00:00:00 2001 From: Norman Meier Date: Fri, 3 May 2024 20:14:09 +0200 Subject: [PATCH 05/11] chore: mod tidy Signed-off-by: Norman Meier --- examples/gno.land/r/demo/teritori/projects_manager/gno.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/gno.land/r/demo/teritori/projects_manager/gno.mod b/examples/gno.land/r/demo/teritori/projects_manager/gno.mod index da9d6b9f186..3159ab96fcf 100644 --- a/examples/gno.land/r/demo/teritori/projects_manager/gno.mod +++ b/examples/gno.land/r/demo/teritori/projects_manager/gno.mod @@ -2,5 +2,5 @@ module gno.land/r/demo/teritori/projects_manager require ( gno.land/p/demo/json v0.0.0-latest - gno.land/p/demo/ui v0.0.0-latest + gno.land/p/demo/ufmt v0.0.0-latest ) From 87683d215426a0ad20ea48f2c888d3a7029b2bef Mon Sep 17 00:00:00 2001 From: Norman Meier Date: Mon, 6 May 2024 16:47:55 +0200 Subject: [PATCH 06/11] chore: unit comment Signed-off-by: Norman Meier --- .../r/demo/teritori/projects_manager/projects_manager.gno | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/gno.land/r/demo/teritori/projects_manager/projects_manager.gno b/examples/gno.land/r/demo/teritori/projects_manager/projects_manager.gno index 99cc30aa941..db9ee9ecc9f 100644 --- a/examples/gno.land/r/demo/teritori/projects_manager/projects_manager.gno +++ b/examples/gno.land/r/demo/teritori/projects_manager/projects_manager.gno @@ -130,8 +130,8 @@ type Milestone struct { desc string amount int64 paid int64 - duration time.Duration - link string // milestone reference link + duration time.Duration // marshal as seconds + link string // milestone reference link funded bool priority MilestonePriority status MilestoneStatus From c2478ce0071ab0a663d230283743a966718a971a Mon Sep 17 00:00:00 2001 From: Norman Meier Date: Tue, 7 May 2024 13:38:35 +0200 Subject: [PATCH 07/11] chore: add test injector Signed-off-by: Norman Meier --- .../r/demo/teritori/inject_project/gno.mod | 3 ++ .../inject_project/inject_project.gno | 38 +++++++++++++++++++ .../projects_manager/projects_manager.gno | 2 +- 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 examples/gno.land/r/demo/teritori/inject_project/gno.mod create mode 100644 examples/gno.land/r/demo/teritori/inject_project/inject_project.gno diff --git a/examples/gno.land/r/demo/teritori/inject_project/gno.mod b/examples/gno.land/r/demo/teritori/inject_project/gno.mod new file mode 100644 index 00000000000..1c8ada4e2bc --- /dev/null +++ b/examples/gno.land/r/demo/teritori/inject_project/gno.mod @@ -0,0 +1,3 @@ +module gno.land/r/demo/teritori/inject_project + +require gno.land/r/demo/teritori/projects_manager v0.0.0-latest diff --git a/examples/gno.land/r/demo/teritori/inject_project/inject_project.gno b/examples/gno.land/r/demo/teritori/inject_project/inject_project.gno new file mode 100644 index 00000000000..1a5a9a07198 --- /dev/null +++ b/examples/gno.land/r/demo/teritori/inject_project/inject_project.gno @@ -0,0 +1,38 @@ +package inject_project + +import ( + "std" + + "gno.land/r/demo/teritori/projects_manager" +) + +func CreateDemoProject() { + projects_manager.CreateContract( + std.Address(""), + std.CurrentRealm().Addr(), + "ugnot", + `{ + "shortDescData": { + "name": "My Awesome Blog", + "desc": "A blog about my awesome life", + "tags": "frontend,ui,ux", + "coverImg": "ipfs://bafybeihlfopxuwgrjjgkxn46j73dse4g3v5qcpv6z7ycaujpnu2qzpdaiq" + }, + "teamAndLinkData": { + "websiteLink": "https://website.com", + "twitterProfile": "https://twitter.com", + "discordLink": "https://discord.com", + "githubLink": "https://github.com", + "teamDesc": "This is long team description" + } + }`, + 200000, + "Bootstrap repository,Create Home page", + "Create a GitHub repository with\n- These\n- Tech\n- Requirements,Create the Home page with\n- These\n- UI/UX\n- Requirements", + "42,420", + "3600,36000", + ",", + "MS_PRIORITY_HIGH,MS_PRIORITY_MEDIUM", + "g108cszmcvs4r3k67k7h5zuhm4el3qhlrxzhshtv", + ) +} diff --git a/examples/gno.land/r/demo/teritori/projects_manager/projects_manager.gno b/examples/gno.land/r/demo/teritori/projects_manager/projects_manager.gno index db9ee9ecc9f..5f7543619ac 100644 --- a/examples/gno.land/r/demo/teritori/projects_manager/projects_manager.gno +++ b/examples/gno.land/r/demo/teritori/projects_manager/projects_manager.gno @@ -374,7 +374,7 @@ func CreateContract( sent := std.GetOrigSend() amount := sent.AmountOf(paymentDenom) if amount != projectBudget { - panic("funder should send all the needed funds at instantiation") + panic(ufmt.Sprintf("funder `%s` should send `%d%s`, got `%d%s`", caller, amount, paymentDenom, sent, paymentDenom)) } funded = true } From bf1d8eda4fc3404156c77a819042af0556d2bb62 Mon Sep 17 00:00:00 2001 From: Norman Meier Date: Fri, 17 May 2024 15:17:43 +0200 Subject: [PATCH 08/11] feat: stokey Signed-off-by: Norman Meier --- .../p/demo/dao_maker/jsonutil/gno.mod | 7 + .../p/demo/dao_maker/jsonutil/jsonutil.gno | 135 ++++++++++++++++++ examples/gno.land/p/demo/stokey/gno.mod | 3 + examples/gno.land/p/demo/stokey/storekey.gno | 95 ++++++++++++ examples/gno.land/p/demo/stokey/utils.gno | 3 + 5 files changed, 243 insertions(+) create mode 100644 examples/gno.land/p/demo/dao_maker/jsonutil/gno.mod create mode 100644 examples/gno.land/p/demo/dao_maker/jsonutil/jsonutil.gno create mode 100644 examples/gno.land/p/demo/stokey/gno.mod create mode 100644 examples/gno.land/p/demo/stokey/storekey.gno create mode 100644 examples/gno.land/p/demo/stokey/utils.gno diff --git a/examples/gno.land/p/demo/dao_maker/jsonutil/gno.mod b/examples/gno.land/p/demo/dao_maker/jsonutil/gno.mod new file mode 100644 index 00000000000..86765ddeb64 --- /dev/null +++ b/examples/gno.land/p/demo/dao_maker/jsonutil/gno.mod @@ -0,0 +1,7 @@ +module gno.land/p/demo/dao_maker/jsonutil + +require ( + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/json v0.0.0-latest + gno.land/p/demo/users v0.0.0-latest +) diff --git a/examples/gno.land/p/demo/dao_maker/jsonutil/jsonutil.gno b/examples/gno.land/p/demo/dao_maker/jsonutil/jsonutil.gno new file mode 100644 index 00000000000..e3fbe0b835a --- /dev/null +++ b/examples/gno.land/p/demo/dao_maker/jsonutil/jsonutil.gno @@ -0,0 +1,135 @@ +package jsonutil + +import ( + "std" + "strconv" + "time" + + "gno.land/p/demo/avl" + "gno.land/p/demo/json" + "gno.land/p/demo/users" +) + +func UnionNode(variant string, value *json.Node) *json.Node { + return json.ObjectNode("", map[string]*json.Node{ + variant: value, + }) +} + +func MustUnion(value *json.Node) (string, *json.Node) { + obj := value.MustObject() + for key, value := range obj { + return key, value + } + + panic("no variant in union") +} + +func TimeNode(value time.Time) *json.Node { + j, err := value.MarshalJSON() + if err != nil { + panic(err) + } + + return json.StringNode("", string(j[1:len(j)-1])) +} + +func MustTime(value *json.Node) time.Time { + t := time.Time{} + err := t.UnmarshalJSON([]byte(value.String())) + if err != nil { + panic(err) + } + + return t +} + +func DurationNode(value time.Duration) *json.Node { + return Int64Node(value.Nanoseconds()) +} + +func MustDurationSeconds(value *json.Node) time.Duration { + return time.Duration(MustInt64(value)) * time.Second +} + +func EmptyObjectNode() *json.Node { + return json.ObjectNode("", nil) +} + +// int is always 64 bits in gno so we need a string to represent it without loss of precision in a lot of javascript environment, I wish bigint in json was more widely supported +func IntNode(value int) *json.Node { + return json.StringNode("", strconv.Itoa(value)) +} + +func MustInt(value *json.Node) int { + i, err := strconv.Atoi(value.MustString()) + if err != nil { + panic(err) + } + + return i +} + +func Uint32Node(value uint32) *json.Node { + return json.StringNode("", strconv.FormatUint(uint64(value), 10)) +} + +func MustUint32(value *json.Node) uint32 { + return uint32(MustInt(value)) +} + +func Int64Node(value int64) *json.Node { + return json.StringNode("", strconv.FormatInt(value, 10)) +} + +func MustInt64(value *json.Node) int64 { + return int64(MustInt(value)) +} + +func Uint64Node(value uint64) *json.Node { + return json.StringNode("", strconv.FormatUint(value, 10)) +} + +func MustUint64(value *json.Node) uint64 { + return uint64(MustInt(value)) // FIXME: full uint64 range support (currently limited to [-2^63, 2^63-1]) +} + +func AVLTreeNode(root *avl.Tree, transform func(elem interface{}) *json.Node) *json.Node { + if root == nil { + return EmptyObjectNode() + } + + fields := make(map[string]*json.Node) + root.Iterate("", "", func(key string, val interface{}) bool { + fields[key] = transform(val) + return false + }) + + return json.ObjectNode("", fields) +} + +func AddressNode(addr std.Address) *json.Node { + return json.StringNode("", addr.String()) +} + +func MustAddress(value *json.Node) std.Address { + addr := std.Address(value.MustString()) + if !addr.IsValid() { + panic("invalid address") + } + + return addr +} + +func AddressOrNameNode(aon users.AddressOrName) *json.Node { + return json.StringNode("", string(aon)) +} + +func MustAddressOrName(value *json.Node) users.AddressOrName { + aon := users.AddressOrName(value.MustString()) + if !aon.IsValid() { + panic("invalid address or name") + } + + return aon +} diff --git a/examples/gno.land/p/demo/stokey/gno.mod b/examples/gno.land/p/demo/stokey/gno.mod new file mode 100644 index 00000000000..45145f2d71d --- /dev/null +++ b/examples/gno.land/p/demo/stokey/gno.mod @@ -0,0 +1,3 @@ +module gno.land/p/demo/stokey + +require gno.land/p/demo/seqid v0.0.0-latest diff --git a/examples/gno.land/p/demo/stokey/storekey.gno b/examples/gno.land/p/demo/stokey/storekey.gno new file mode 100644 index 00000000000..e5f5a81da86 --- /dev/null +++ b/examples/gno.land/p/demo/stokey/storekey.gno @@ -0,0 +1,95 @@ +package stokey + +import ( + "std" + "strings" + + "gno.land/p/demo/seqid" +) + +type SubKey interface { + KeyString() string +} + +type Key []SubKey + +func NewKey(subKeys ...SubKey) Key { + return Key(subKeys) +} + +func (k Key) String() string { + b := strings.Builder{} + for _, subKey := range k { + b.WriteString(subKey.KeyString()) + } + return b.String() +} + +// std.Address + +type addrSubKey std.Address + +func Address(addr std.Address) SubKey { + return addrSubKey(addr) +} + +func (a addrSubKey) KeyString() string { + _, b, ok := std.DecodeBech32(std.Address(a)) + if !ok { + panic("invalid address") + } + return string(b[:]) +} + +type noAddrSubKey struct{} + +func NoAddress() SubKey { + return noAddrSubKey{} +} + +func (na noAddrSubKey) KeyString() string { + return string(make([]byte, 20)) +} + +type nextAddrSubKey std.Address + +func NextAddress(addr std.Address) SubKey { + return nextAddrSubKey(addr) +} + +func (na nextAddrSubKey) KeyString() string { + _, b, ok := std.DecodeBech32(std.Address(na)) + if !ok { + panic("invalid address") + } + for i := len(b) - 1; i >= 0; i-- { + if b[i] == 255 { + if i == 0 { + panic("overflow") + } + b[i] = 0 + } else { + b[i]++ + break + } + } + return string(b[:]) +} + +// uint64 + +type uint64SubKey uint64 + +func Uint64(u uint64) SubKey { + return uint64SubKey(u) +} + +func (u uint64SubKey) KeyString() string { + return seqid.ID(u).String() +} + +// uint32 + +func Uint32(u uint32) SubKey { + return uint64SubKey(u) +} diff --git a/examples/gno.land/p/demo/stokey/utils.gno b/examples/gno.land/p/demo/stokey/utils.gno new file mode 100644 index 00000000000..008cfcccc09 --- /dev/null +++ b/examples/gno.land/p/demo/stokey/utils.gno @@ -0,0 +1,3 @@ +package stokey + +// TODO: add a KeySchema type to explicitely define keys shape and allow more qol utils From 96142a9f750ffd9b55a6e3b931bca8516339c169 Mon Sep 17 00:00:00 2001 From: Norman Meier Date: Fri, 17 May 2024 15:17:50 +0200 Subject: [PATCH 09/11] tmp Signed-off-by: Norman Meier --- .../p/demo/dao_maker/jsonutil/jsonutil.gno | 4 - .../r/demo/teritori/inject_project/gno.mod | 3 - .../inject_project/inject_project.gno | 38 --- .../r/demo/teritori/projects_manager/gno.mod | 3 + .../projects_manager/projects_manager.gno | 245 +++++++++++------- gno.land/pkg/sdk/vm/convert.go | 2 +- 6 files changed, 151 insertions(+), 144 deletions(-) delete mode 100644 examples/gno.land/r/demo/teritori/inject_project/gno.mod delete mode 100644 examples/gno.land/r/demo/teritori/inject_project/inject_project.gno diff --git a/examples/gno.land/p/demo/dao_maker/jsonutil/jsonutil.gno b/examples/gno.land/p/demo/dao_maker/jsonutil/jsonutil.gno index e3fbe0b835a..8bc5c05e687 100644 --- a/examples/gno.land/p/demo/dao_maker/jsonutil/jsonutil.gno +++ b/examples/gno.land/p/demo/dao_maker/jsonutil/jsonutil.gno @@ -127,9 +127,5 @@ func AddressOrNameNode(aon users.AddressOrName) *json.Node { func MustAddressOrName(value *json.Node) users.AddressOrName { aon := users.AddressOrName(value.MustString()) - if !aon.IsValid() { - panic("invalid address or name") - } - return aon } diff --git a/examples/gno.land/r/demo/teritori/inject_project/gno.mod b/examples/gno.land/r/demo/teritori/inject_project/gno.mod deleted file mode 100644 index 1c8ada4e2bc..00000000000 --- a/examples/gno.land/r/demo/teritori/inject_project/gno.mod +++ /dev/null @@ -1,3 +0,0 @@ -module gno.land/r/demo/teritori/inject_project - -require gno.land/r/demo/teritori/projects_manager v0.0.0-latest diff --git a/examples/gno.land/r/demo/teritori/inject_project/inject_project.gno b/examples/gno.land/r/demo/teritori/inject_project/inject_project.gno deleted file mode 100644 index 1a5a9a07198..00000000000 --- a/examples/gno.land/r/demo/teritori/inject_project/inject_project.gno +++ /dev/null @@ -1,38 +0,0 @@ -package inject_project - -import ( - "std" - - "gno.land/r/demo/teritori/projects_manager" -) - -func CreateDemoProject() { - projects_manager.CreateContract( - std.Address(""), - std.CurrentRealm().Addr(), - "ugnot", - `{ - "shortDescData": { - "name": "My Awesome Blog", - "desc": "A blog about my awesome life", - "tags": "frontend,ui,ux", - "coverImg": "ipfs://bafybeihlfopxuwgrjjgkxn46j73dse4g3v5qcpv6z7ycaujpnu2qzpdaiq" - }, - "teamAndLinkData": { - "websiteLink": "https://website.com", - "twitterProfile": "https://twitter.com", - "discordLink": "https://discord.com", - "githubLink": "https://github.com", - "teamDesc": "This is long team description" - } - }`, - 200000, - "Bootstrap repository,Create Home page", - "Create a GitHub repository with\n- These\n- Tech\n- Requirements,Create the Home page with\n- These\n- UI/UX\n- Requirements", - "42,420", - "3600,36000", - ",", - "MS_PRIORITY_HIGH,MS_PRIORITY_MEDIUM", - "g108cszmcvs4r3k67k7h5zuhm4el3qhlrxzhshtv", - ) -} diff --git a/examples/gno.land/r/demo/teritori/projects_manager/gno.mod b/examples/gno.land/r/demo/teritori/projects_manager/gno.mod index 3159ab96fcf..d828758f043 100644 --- a/examples/gno.land/r/demo/teritori/projects_manager/gno.mod +++ b/examples/gno.land/r/demo/teritori/projects_manager/gno.mod @@ -1,6 +1,9 @@ module gno.land/r/demo/teritori/projects_manager require ( + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/dao_maker/jsonutil v0.0.0-latest gno.land/p/demo/json v0.0.0-latest + gno.land/p/demo/seqid v0.0.0-latest gno.land/p/demo/ufmt v0.0.0-latest ) diff --git a/examples/gno.land/r/demo/teritori/projects_manager/projects_manager.gno b/examples/gno.land/r/demo/teritori/projects_manager/projects_manager.gno index 5f7543619ac..756def95f21 100644 --- a/examples/gno.land/r/demo/teritori/projects_manager/projects_manager.gno +++ b/examples/gno.land/r/demo/teritori/projects_manager/projects_manager.gno @@ -6,7 +6,10 @@ import ( "strings" "time" + "gno.land/p/demo/avl" + "gno.land/p/demo/dao_maker/jsonutil" "gno.land/p/demo/json" + "gno.land/p/demo/seqid" "gno.land/p/demo/ufmt" ) @@ -120,6 +123,18 @@ func (x MilestonePriority) String() string { return "UNKNOWN" } +func MilestonePriorityFromString(s string) MilestonePriority { + switch s { + case "MS_PRIORITY_HIGH": + return MS_PRIORITY_HIGH + case "MS_PRIORITY_MEDIUM": + return MS_PRIORITY_MEDIUM + case "MS_PRIORITY_LOW": + return MS_PRIORITY_LOW + } + panic("invalid MilestonePriority") +} + func (x MilestonePriority) ToJSON() *json.Node { return json.StringNode("", x.String()) } @@ -254,25 +269,72 @@ func (c Contract) ToJSON() *json.Node { }) } -// Escrow State -var contracts []Contract +// State +var ( + contracts []*Contract + contractsByFunder = avl.NewTree() // std.Address(funder) => contractID => *Contract + contractsByContractor = avl.NewTree() // std.Address(contractor) => contractID => *Contract + contractsByFunderAndContractor = avl.NewTree() // std.Address(funder) + std.Address(contractor) => contractID => *Contract +) + +func setIndices(contract *Contract) { + if contract == nil { + panic("contract is nil") + } + + if contract.contractor != "" { + contractorKey := std.Address(contract.contractor).String() + byIDTree, ok := contractsByContractor.Get(contractorKey) + if !ok { + byIDTree = avl.NewTree() + contractsByContractor.Set(contractorKey, byIDTree) + } + + byIDTree.(*avl.Tree).Set(seqid.ID(contract.id).String(), contract) + } + + if contract.funder != "" { + funderKey := std.Address(contract.funder).String() + byIDTree, ok := contractsByFunder.Get(funderKey) + if !ok { + byIDTree = avl.NewTree() + contractsByFunder.Set(funderKey, byIDTree) + } + + byIDTree.(*avl.Tree).Set(seqid.ID(contract.id).String(), contract) + } + + if contract.contractor != "" && contract.funder != "" { + byIDTree, ok := contractsByFunderAndContractor.Get(std.Address(contract.funder).String() + std.Address(contract.contractor).String()) + if !ok { + byIDTree = avl.NewTree() + contractsByFunderAndContractor.Set(std.Address(contract.funder).String()+std.Address(contract.contractor).String(), byIDTree) + } + + byIDTree.(*avl.Tree).Set(seqid.ID(contract.id).String(), contract) + } +} func CurrentRealm() string { return std.CurrentRealm().Addr().String() } +type MilestoneDefinition struct { + Title string + Desc string + Amount int64 + Duration time.Duration + Link string + Priority MilestonePriority +} + func CreateContract( contractor std.Address, funder std.Address, paymentDenom string, metadata string, expiryDurationSeconds uint64, - milestoneTitles string, - milestoneDescs string, - milestoneAmounts string, - milestoneDurationsSeconds string, - milestoneLinks string, - milestonePriorities string, + milestones []MilestoneDefinition, conflictHandler string, ) { if contractor != "" && !contractor.IsValid() { @@ -300,72 +362,25 @@ func CreateContract( panic("caller should be one of contractor or funder") } - milestoneTitleArr := strings.Split(milestoneTitles, ",") - milestoneDescArr := strings.Split(milestoneDescs, ",") - milestoneAmountArr := strings.Split(milestoneAmounts, ",") - milestoneDurationArr := strings.Split(milestoneDurationsSeconds, ",") - milestoneLinkArr := strings.Split(milestoneLinks, ",") - milestonePrioritiesArr := strings.Split(milestonePriorities, ",") - - if len(milestoneTitleArr) == 0 { - panic("no milestone titles provided") - } - - if len(milestoneTitleArr) != len(milestoneAmountArr) || - len(milestoneTitleArr) != len(milestoneDurationArr) || - len(milestoneTitleArr) != len(milestonePrioritiesArr) || - len(milestoneTitleArr) != len(milestoneDescArr) || - len(milestoneTitleArr) != len(milestoneLinkArr) { - panic("mismatch on milestones title, description, amount, duration, priority and link") + if len(milestones) == 0 { + panic("milestones should not be empty") } - milestones := []Milestone{} + mss := make([]Milestone, 0, len(milestones)) projectBudget := int64(0) - for i, title := range milestoneTitleArr { - amount, err := strconv.Atoi(milestoneAmountArr[i]) - if err != nil { - panic(err) - } - if amount < 0 { - panic("milestone amount should be a positive number") - } - - durationSeconds, err := strconv.Atoi(milestoneDurationArr[i]) - if err != nil { - panic(err) - } - if durationSeconds <= 0 { - panic("milestone duration should be greater than 0") - } - - var prio MilestonePriority - - switch milestonePrioritiesArr[i] { - case "MS_PRIORITY_HIGH": - prio = MS_PRIORITY_HIGH - case "MS_PRIORITY_MEDIUM": - prio = MS_PRIORITY_MEDIUM - case "MS_PRIORITY_LOW": - prio = MS_PRIORITY_LOW - default: - panic("priority is not valid") - } - - duration := time.Duration(durationSeconds) * time.Second - - milestones = append(milestones, Milestone{ - id: uint64(i), - title: title, - desc: milestoneDescArr[i], - amount: int64(amount), + for _, ms := range milestones { + projectBudget += ms.Amount + mss = append(mss, Milestone{ + id: uint64(len(mss)), + title: ms.Title, + desc: ms.Desc, + amount: ms.Amount, paid: 0, - duration: duration, - link: milestoneLinkArr[i], - priority: prio, - funded: false, + duration: ms.Duration, + link: ms.Link, + priority: ms.Priority, status: MS_OPEN, }) - projectBudget += int64(amount) } // If contract creator is funder then he needs to send all the needed fund to contract @@ -374,7 +389,7 @@ func CreateContract( sent := std.GetOrigSend() amount := sent.AmountOf(paymentDenom) if amount != projectBudget { - panic(ufmt.Sprintf("funder `%s` should send `%d%s`, got `%d%s`", caller, amount, paymentDenom, sent, paymentDenom)) + panic(ufmt.Sprintf("funder `%s` should send `%d%s`, got `%d%s`", caller, projectBudget, paymentDenom, amount, paymentDenom)) } funded = true } @@ -383,7 +398,7 @@ func CreateContract( now := time.Now() contractId := uint64(len(contracts)) - contracts = append(contracts, Contract{ + contracts = append(contracts, &Contract{ id: contractId, sender: caller, contractor: contractor, @@ -392,12 +407,43 @@ func CreateContract( metadata: metadata, status: CREATED, expireAt: now.Add(expiryDuration), - milestones: milestones, + milestones: mss, conflictHandler: conflictHandler, budget: projectBudget, createdAt: now, funded: funded, }) + setIndices(contracts[contractId]) +} + +func CreateContractJSON( + contractor std.Address, + funder std.Address, + paymentDenom string, + metadata string, + expiryDurationSeconds uint64, + milestonesJSON string, + conflictHandler string, +) { + ast, err := json.Unmarshal([]byte(milestonesJSON)) + if err != nil { + panic(err) + } + vals := ast.MustArray() + milestones := make([]MilestoneDefinition, 0, len(vals)) + for _, val := range vals { + obj := val.MustObject() + milestone := MilestoneDefinition{ + Title: obj["title"].MustString(), + Desc: obj["desc"].MustString(), + Amount: jsonutil.MustInt64(obj["amount"]), + Duration: jsonutil.MustDurationSeconds(obj["duration"]), + Link: obj["link"].MustString(), + Priority: MilestonePriorityFromString(obj["priority"].MustString()), + } + milestones = append(milestones, milestone) + } + CreateContract(contractor, funder, paymentDenom, metadata, expiryDurationSeconds, milestones, conflictHandler) } func CancelContract(contractId uint64) { @@ -776,37 +822,40 @@ func GetContractorCandidatesJSON(contractId uint64) string { return string(ret) } -func GetContracts(startAfter, limit uint64, filterByFunder string, filterByContractor string) []Contract { - max := uint64(len(contracts)) - if startAfter+limit < max { - max = startAfter + limit +func GetContracts(offset, limit int, filterByFunder string, filterByContractor string) []*Contract { + if offset < 0 { + offset = 0 } - var results []Contract - i := uint64(0) - - for _, c := range contracts { - if filterByFunder != "ALL" && c.funder.String() != filterByFunder { - continue - } - - if filterByContractor != "ALL" && c.contractor.String() != filterByContractor { - continue - } - - if i < startAfter { - i++ - continue - } + if limit <= 0 || offset >= len(contracts) { + return nil + } - if i > max { - break + var tree interface{} + if filterByFunder != "" && filterByContractor != "" { + tree, _ = contractsByFunderAndContractor.Get(filterByFunder + filterByContractor) + } else if filterByFunder != "" { + tree, _ = contractsByFunder.Get(filterByFunder) + } else if filterByContractor != "" { + tree, _ = contractsByContractor.Get(filterByContractor) + } else { + end := offset + limit + if end > len(contracts) { + end = len(contracts) } + return contracts[offset:end] + } - results = append(results, c) - i++ + if tree == nil { + return nil } + var results []*Contract + tree.(*avl.Tree).IterateByOffset(offset, limit, func(key string, value interface{}) bool { + results = append(results, value.(*Contract)) + return false + }) + return results } @@ -824,8 +873,8 @@ func RenderContractJSON(contractId uint64) string { return string(ret) } -func RenderContractsJSON(startAfter uint64, limit uint64, filterByFunder string, filterByContractor string) string { - contracts := GetContracts(startAfter, limit, filterByFunder, filterByContractor) +func RenderContractsJSON(offset, limit int, filterByFunder string, filterByContractor string) string { + contracts := GetContracts(offset, limit, filterByFunder, filterByContractor) contractsJSON := make([]*json.Node, len(contracts)) for i, c := range contracts { contractsJSON[i] = c.ToJSON() @@ -852,7 +901,7 @@ func Render(path string) string { for i := 0; i < numContracts; i++ { b.WriteString("```json\n") b.WriteString(RenderContractJSON(uint64(len(contracts) - (i + 1)))) - b.WriteString("\n") + b.WriteRune('\n') b.WriteString("```\n") } return b.String() diff --git a/gno.land/pkg/sdk/vm/convert.go b/gno.land/pkg/sdk/vm/convert.go index f70f99403a8..a8b6148f82c 100644 --- a/gno.land/pkg/sdk/vm/convert.go +++ b/gno.land/pkg/sdk/vm/convert.go @@ -184,7 +184,7 @@ func convertArgToGno(arg string, argT gno.Type) (tv gno.TypedValue) { } return } else { - panic("unexpected slice type in contract arg") + panic(fmt.Sprintf("unexpected slice type in contract arg %q", arg)) } default: panic(fmt.Sprintf("unexpected type in contract arg: %v", argT)) From cbd14a70dd2f852cb94a58d8ccb7f08717af719c Mon Sep 17 00:00:00 2001 From: Norman Meier Date: Mon, 27 May 2024 19:17:43 +0200 Subject: [PATCH 10/11] feat: allow to reset gnodev with endpoint Signed-off-by: Norman Meier --- contribs/gnodev/cmd/gnodev/main.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/contribs/gnodev/cmd/gnodev/main.go b/contribs/gnodev/cmd/gnodev/main.go index 9b769321c83..34f6d7a9552 100644 --- a/contribs/gnodev/cmd/gnodev/main.go +++ b/contribs/gnodev/cmd/gnodev/main.go @@ -258,6 +258,13 @@ func execDev(cfg *devCfg, args []string, io commands.IO) (err error) { // Setup gnoweb webhandler := setupGnoWebServer(logger.WithGroup(WebLogName), cfg, devNode) + mux.HandleFunc("/reset", func(res http.ResponseWriter, req *http.Request) { + if err := devNode.Reset(req.Context()); err != nil { + logger.Error("failed to reset", slog.Any("err", err)) + res.WriteHeader(http.StatusInternalServerError) + } + }) + // Setup HotReload if needed if !cfg.noWatch { evtstarget := fmt.Sprintf("%s/_events", server.Addr) From f1904238dbb487c1fa843219ca611d301b3fb473 Mon Sep 17 00:00:00 2001 From: Norman Meier Date: Mon, 27 May 2024 19:17:50 +0200 Subject: [PATCH 11/11] feat: add filter object Signed-off-by: Norman Meier --- .../demo/teritori/projects_manager/filter.gno | 81 +++++++++++++++++++ .../projects_manager/projects_manager.gno | 59 +++++++++++--- 2 files changed, 127 insertions(+), 13 deletions(-) create mode 100644 examples/gno.land/r/demo/teritori/projects_manager/filter.gno diff --git a/examples/gno.land/r/demo/teritori/projects_manager/filter.gno b/examples/gno.land/r/demo/teritori/projects_manager/filter.gno new file mode 100644 index 00000000000..0d727f3353d --- /dev/null +++ b/examples/gno.land/r/demo/teritori/projects_manager/filter.gno @@ -0,0 +1,81 @@ +package projects_manager + +import ( + "std" + + "gno.land/p/demo/dao_maker/jsonutil" + "gno.land/p/demo/json" + "gno.land/p/demo/ufmt" +) + +type Filter interface { + FromJSON(ast *json.Node) +} + +func FilterFromJSON(ast *json.Node) Filter { + if ast.IsNull() { + return nil + } + var filter Filter + key, member := jsonutil.MustUnion(ast) + switch key { + case "byCandidatesForFunder": + filter = &FilterByCandidatesForFunder{} + case "byFunder": + filter = &FilterByFunder{} + case "byContractor": + filter = &FilterByContractor{} + case "byContractorAndFunder": + filter = &FilterByContractorAndFunder{} + default: + panic(ufmt.Sprintf("invalid filter kind `%s`", key)) + } + filter.FromJSON(member) + return filter +} + +type FilterByCandidatesForFunder struct { + Funder std.Address +} + +func (f *FilterByCandidatesForFunder) FromJSON(ast *json.Node) { + obj := ast.MustObject() + f.Funder = jsonutil.MustAddress(obj["funder"]) +} + +var _ Filter = &FilterByCandidatesForFunder{} + +type FilterByFunder struct { + Funder std.Address +} + +func (f *FilterByFunder) FromJSON(ast *json.Node) { + obj := ast.MustObject() + f.Funder = jsonutil.MustAddress(obj["funder"]) +} + +var _ Filter = &FilterByFunder{} + +type FilterByContractor struct { + Contractor std.Address +} + +func (f *FilterByContractor) FromJSON(ast *json.Node) { + obj := ast.MustObject() + f.Contractor = jsonutil.MustAddress(obj["contractor"]) +} + +var _ Filter = &FilterByContractor{} + +type FilterByContractorAndFunder struct { + Contractor std.Address + Funder std.Address +} + +func (f *FilterByContractorAndFunder) FromJSON(ast *json.Node) { + obj := ast.MustObject() + f.Contractor = jsonutil.MustAddress(obj["contractor"]) + f.Funder = jsonutil.MustAddress(obj["funder"]) +} + +var _ Filter = &FilterByContractorAndFunder{} diff --git a/examples/gno.land/r/demo/teritori/projects_manager/projects_manager.gno b/examples/gno.land/r/demo/teritori/projects_manager/projects_manager.gno index 756def95f21..ff3da646d6f 100644 --- a/examples/gno.land/r/demo/teritori/projects_manager/projects_manager.gno +++ b/examples/gno.land/r/demo/teritori/projects_manager/projects_manager.gno @@ -275,6 +275,7 @@ var ( contractsByFunder = avl.NewTree() // std.Address(funder) => contractID => *Contract contractsByContractor = avl.NewTree() // std.Address(contractor) => contractID => *Contract contractsByFunderAndContractor = avl.NewTree() // std.Address(funder) + std.Address(contractor) => contractID => *Contract + contractsWithCandidates = avl.NewTree() // std.Address(funder) => contractID => *Contract ) func setIndices(contract *Contract) { @@ -578,6 +579,18 @@ func AcceptContractor(contractId uint64, contractor std.Address) { } contracts[contractId].contractor = contractor + + funderKey := contract.funder.String() + byIDTreeIface, ok := contractsWithCandidates.Get(funderKey) + if !ok { + byIDTreeIface = avl.NewTree() + contractsWithCandidates.Set(funderKey, byIDTreeIface) + } + byIDTree := byIDTreeIface.(*avl.Tree) + byIDTree.Remove(seqid.ID(contract.id).String()) + if byIDTree.Size() == 0 { + contractsWithCandidates.Remove(funderKey) + } } func SubmitContractorCandidate(contractId uint64) { @@ -608,6 +621,14 @@ func SubmitContractorCandidate(contractId uint64) { } contracts[contractId].contractorCandidates = append(candidates, caller) + + funderKey := contract.funder.String() + byIDTree, ok := contractsWithCandidates.Get(funderKey) + if !ok { + byIDTree = avl.NewTree() + contractsWithCandidates.Set(funderKey, byIDTree) + } + byIDTree.(*avl.Tree).Set(seqid.ID(contract.id).String(), contract) } // Complete any milestone in review status and pay the needed amount @@ -822,7 +843,7 @@ func GetContractorCandidatesJSON(contractId uint64) string { return string(ret) } -func GetContracts(offset, limit int, filterByFunder string, filterByContractor string) []*Contract { +func GetContracts(offset, limit int, filter Filter) []*Contract { if offset < 0 { offset = 0 } @@ -831,14 +852,7 @@ func GetContracts(offset, limit int, filterByFunder string, filterByContractor s return nil } - var tree interface{} - if filterByFunder != "" && filterByContractor != "" { - tree, _ = contractsByFunderAndContractor.Get(filterByFunder + filterByContractor) - } else if filterByFunder != "" { - tree, _ = contractsByFunder.Get(filterByFunder) - } else if filterByContractor != "" { - tree, _ = contractsByContractor.Get(filterByContractor) - } else { + if filter == nil { end := offset + limit if end > len(contracts) { end = len(contracts) @@ -846,6 +860,20 @@ func GetContracts(offset, limit int, filterByFunder string, filterByContractor s return contracts[offset:end] } + var tree interface{} + switch f := filter.(type) { + case *FilterByCandidatesForFunder: + tree, _ = contractsWithCandidates.Get(f.Funder.String()) + case *FilterByContractorAndFunder: + tree, _ = contractsByFunderAndContractor.Get(f.Funder.String() + f.Contractor.String()) + case *FilterByContractor: + tree, _ = contractsByContractor.Get(f.Contractor.String()) + case *FilterByFunder: + tree, _ = contractsByFunder.Get(f.Funder.String()) + default: + panic("unknown filter") + } + if tree == nil { return nil } @@ -873,10 +901,15 @@ func RenderContractJSON(contractId uint64) string { return string(ret) } -func RenderContractsJSON(offset, limit int, filterByFunder string, filterByContractor string) string { - contracts := GetContracts(offset, limit, filterByFunder, filterByContractor) - contractsJSON := make([]*json.Node, len(contracts)) - for i, c := range contracts { +func RenderContractsJSON(offset, limit int, filterJSON string) string { + filter := FilterFromJSON(json.Must(json.Unmarshal([]byte(filterJSON)))) + contractsRes := GetContracts(offset, limit, filter) + return renderContractsJSON(contractsRes) +} + +func renderContractsJSON(contractsRes []*Contract) string { + contractsJSON := make([]*json.Node, len(contractsRes)) + for i, c := range contractsRes { contractsJSON[i] = c.ToJSON() }