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]*?(?:\\1>[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|?(tag)(?: +|\\n|/?>)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|(?!script|pre|style|textarea)[a-z][\\w-]*\\s*>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$))",def:/^ {0,3}\[(label)\]: *(?:\n *)?([^\s>]+)>?(?:(?: +(?:\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","?(?:tag)(?: +|\\n|/?>)|<(?: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","?(?:tag)(?: +|\\n|/?>)|<(?: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","?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",y._tag).getRegex(),y.pedantic=d({},y.normal,{html:p("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+?\\1> *(?:\\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:/^ *\[([^\]]+)\]: *([^\s>]+)>?(?: +(["(][^\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:-]*\\s*>|^<[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?"\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+">\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+""+u+">\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='"+u+" "},t.image=function(e,t,u){if(null===(e=g(this.options.sanitize,this.options.baseUrl,e)))return u;e=' ":">"},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]*?(?:\\1>[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|?(tag)(?: +|\\n|/?>)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|(?!script|pre|style|textarea)[a-z][\\w-]*\\s*>(?=[ \\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","?(?:tag)(?: +|\\n|/?>)|<(?: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","?(?:tag)(?: +|\\n|/?>)|<(?: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","?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",A).getRegex()},P={...q,html:k("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+?\\1> *(?:\\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:/^ *\[([^\]]+)\]: *([^\s>]+)>?(?: +(["(][^\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:-]*\\s*>|^<[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+""+s+">\n"}listitem(e,t,n){return`${e} \n`}checkbox(e){return" '}paragraph(e){return`${e}
\n`}table(e,t){return t&&(t=`${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}>\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='"+n+" ",r}image(e,t,n){const s=g(e);if(null===s)return n;let r=` ",r}text(e){return e}}class re{strong(e){return e}em(e){return e}codespan(e){return e}del(e){return e}html(e){return e}text(e){return e}link(e,t,n){return""+n}image(e,t,n){return""+n}br(){return""}}class ie{options;renderer;textRenderer;constructor(t){this.options=t||e.defaults,this.options.renderer=this.options.renderer||new se,this.renderer=this.options.renderer,this.renderer.options=this.options,this.textRenderer=new re}static parse(e,t){return new ie(t).parse(e)}static parseInline(e,t){return new ie(t).parseInline(e)}parse(e,t=!0){let n="";for(let s=0;s0&&"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()
}