@akita-protocol/algomd-core 1.0.0-alpha.0 → 1.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/guild.ts ADDED
@@ -0,0 +1,260 @@
1
+ import algosdk from 'algosdk'
2
+ import { fromHex } from './chain/bytes'
3
+ import type { TealKeyValue } from './chain/indexer-types'
4
+ import { stateFromTeal } from './chain/state'
5
+ import { boxName, decodeBoxValue, decodeGlobalState, type Decoded } from './contracts/decode'
6
+ import { requestJson, type FetchLike } from './http'
7
+ import { readBox } from './akita'
8
+ import type { Endpoint } from './types'
9
+
10
+ /*
11
+ * Guild proposals. A proposal lives in the Guild app's `proposals` box; once
12
+ * voting opens, its ballot lives in the paired consensus app's `ballots` box.
13
+ * The ref id is `<guildAppId>/<proposalId>`, since proposal ids count per Guild.
14
+ */
15
+
16
+ export type GuildProposalStatus = 'draft' | 'invalid' | 'voting' | 'rejected' | 'approved' | 'executed'
17
+
18
+ const STATUS: Record<string, GuildProposalStatus> = {
19
+ '0': 'draft',
20
+ '10': 'invalid',
21
+ '20': 'voting',
22
+ '30': 'rejected',
23
+ '40': 'approved',
24
+ '50': 'executed',
25
+ }
26
+
27
+ export interface GuildProposalAction {
28
+ /** Guild action type (akita-sc guilds/core/constants.ts). */
29
+ type: number
30
+ label: string
31
+ /** Plugin application, for plugin actions. */
32
+ pluginAppId?: string
33
+ /** Delegated caller, for add and remove plugin actions. The zero address means members by vote. */
34
+ caller?: string
35
+ }
36
+
37
+ export interface GuildBallotData {
38
+ resolved: boolean
39
+ approved: boolean
40
+ /** Unix seconds. */
41
+ votingEnd: string
42
+ eligiblePower: string
43
+ voteCount: string
44
+ /** Fixed point, 100000 = 100%. */
45
+ requirements: { participation: string; approval: string; minimumApprovals: string }
46
+ votes: { approvals: string; rejections: string; abstains: string }
47
+ }
48
+
49
+ export interface GuildProposalData {
50
+ guildAppId: string
51
+ proposalId: string
52
+ consensusAppId?: string
53
+ walletAppId?: string
54
+ status: GuildProposalStatus
55
+ creator?: string
56
+ /** Unix seconds. */
57
+ created: string
58
+ votingTs: string
59
+ actions: GuildProposalAction[]
60
+ ballot?: GuildBallotData
61
+ }
62
+
63
+ export const GUILD_ZERO_ADDRESS = algosdk.ALGORAND_ZERO_ADDRESS_STRING
64
+
65
+ /** Splits a `guild-proposal` id into its Guild app id and proposal id. */
66
+ export function splitGuildProposalId(id: string): { guildAppId: string; proposalId: string } | undefined {
67
+ const match = /^(\d+)\/(\d+)$/.exec(id.trim())
68
+ return match ? { guildAppId: match[1]!, proposalId: match[2]! } : undefined
69
+ }
70
+
71
+ // ---------------------------------------------------------------- actions
72
+
73
+ const ACTION_LABELS: Record<number, string> = {
74
+ 10: 'Upgrade the guild',
75
+ 20: 'Install a plugin',
76
+ 21: 'Install a named plugin',
77
+ 22: 'Change plugin vote settings',
78
+ 30: 'Run a plugin',
79
+ 31: 'Revoke a plugin run',
80
+ 40: 'Remove a plugin',
81
+ 41: 'Remove a named plugin',
82
+ 50: 'Add spending allowances',
83
+ 60: 'Remove spending allowances',
84
+ 70: 'Create an escrow',
85
+ 71: 'Lock or unlock an escrow',
86
+ 72: 'Reclaim from an escrow',
87
+ 80: 'Change proposal rules',
88
+ 81: 'Change the action limit',
89
+ 82: 'Create a wallet',
90
+ 83: 'Change membership',
91
+ 84: 'Change membership vote settings',
92
+ 86: 'Set the chat steward',
93
+ 90: 'Update the wallet',
94
+ 91: 'Hand the wallet to a new admin',
95
+ 100: 'Finish setup',
96
+ 110: 'Lock the plugin set',
97
+ }
98
+
99
+ function readUint64(data: Uint8Array, offset: number): bigint | undefined {
100
+ if (data.length < offset + 8) return undefined
101
+ let value = 0n
102
+ for (let i = 0; i < 8; i += 1) value = (value << 8n) | BigInt(data[offset + i]!)
103
+ return value
104
+ }
105
+
106
+ /**
107
+ * Labels a proposal's on-chain actions. Plugin actions decode their static head
108
+ * `(uint64 plugin, address caller, ...)` so a view can say which agent a plugin is for.
109
+ */
110
+ export function summarizeGuildActions(actions: ReadonlyArray<readonly [number, Uint8Array]>): GuildProposalAction[] {
111
+ return actions.map(([type, data]) => {
112
+ const label = ACTION_LABELS[type] ?? `Action ${type}`
113
+ if (type !== 20 && type !== 22 && type !== 30 && type !== 40) return { type, label }
114
+ const plugin = readUint64(data, 0)
115
+ const action: GuildProposalAction = { type, label, ...(plugin !== undefined && { pluginAppId: plugin.toString() }) }
116
+ if ((type === 20 || type === 40) && data.length >= 40) action.caller = algosdk.encodeAddress(data.slice(8, 40))
117
+ return action
118
+ })
119
+ }
120
+
121
+ // ---------------------------------------------------------------- chain read
122
+
123
+ const str = (v: Decoded | undefined): string => (typeof v === 'string' ? v : v == null ? '' : String(v))
124
+ const field = (v: Decoded | undefined, k: string): Decoded | undefined =>
125
+ v && typeof v === 'object' && !Array.isArray(v) ? (v as Record<string, Decoded>)[k] : undefined
126
+ const positive = (v: Decoded | undefined) => (str(v) && str(v) !== '0' ? str(v) : undefined)
127
+
128
+ function decodedActions(value: Decoded | undefined): Array<[number, Uint8Array]> {
129
+ if (!Array.isArray(value)) return []
130
+ return value.flatMap((entry) => {
131
+ if (!Array.isArray(entry) || entry.length < 2) return []
132
+ const type = Number(str(entry[0]))
133
+ const raw = entry[1]
134
+ const data =
135
+ typeof raw === 'string' ? fromHex(raw) : Array.isArray(raw) ? Uint8Array.from(raw, (b) => Number(str(b))) : new Uint8Array()
136
+ return Number.isSafeInteger(type) ? [[type, data] as [number, Uint8Array]] : []
137
+ })
138
+ }
139
+
140
+ function ballotFrom(b: Decoded): GuildBallotData {
141
+ const req = field(b, 'requirements')
142
+ const votes = field(b, 'votes')
143
+ return {
144
+ resolved: field(b, 'resolved') === true,
145
+ approved: field(b, 'approved') === true,
146
+ votingEnd: str(field(b, 'votingEnd')),
147
+ eligiblePower: str(field(b, 'eligiblePower')),
148
+ voteCount: str(field(b, 'voteCount')),
149
+ requirements: {
150
+ participation: str(field(req, 'participation')) || '0',
151
+ approval: str(field(req, 'approval')) || '0',
152
+ minimumApprovals: str(field(req, 'minimumApprovals')) || '0',
153
+ },
154
+ votes: {
155
+ approvals: str(field(votes, 'approvals')) || '0',
156
+ rejections: str(field(votes, 'rejections')) || '0',
157
+ abstains: str(field(votes, 'abstains')) || '0',
158
+ },
159
+ }
160
+ }
161
+
162
+ /** Reads a proposal from its Guild's boxes, plus its ballot once voting has opened. */
163
+ export async function readGuildProposal(fetchFn: FetchLike, algod: Endpoint, id: string): Promise<GuildProposalData | undefined> {
164
+ const parts = splitGuildProposalId(id)
165
+ if (!parts) return undefined
166
+ const { guildAppId, proposalId } = parts
167
+ const [app, value] = await Promise.all([
168
+ requestJson<{ params: { 'global-state'?: TealKeyValue[] } }>(fetchFn, algod, `/v2/applications/${guildAppId}`),
169
+ readBox(fetchFn, algod, guildAppId, boxName('guild', 'proposals', BigInt(proposalId))),
170
+ ])
171
+ if (!app || !value) return undefined
172
+ const guild = decodeGlobalState('guild', stateFromTeal(app.params['global-state']))
173
+ const p = decodeBoxValue('guild', 'proposals', value)
174
+ const status = STATUS[str(field(p, 'status'))] ?? 'invalid'
175
+ const consensusAppId = positive(guild.consensus)
176
+ const creator = str(field(p, 'creator'))
177
+ const data: GuildProposalData = {
178
+ guildAppId,
179
+ proposalId,
180
+ consensusAppId,
181
+ walletAppId: positive(guild.wallet),
182
+ status,
183
+ creator: creator && creator !== GUILD_ZERO_ADDRESS ? creator : undefined,
184
+ created: str(field(p, 'created')),
185
+ votingTs: str(field(p, 'votingTs')),
186
+ actions: summarizeGuildActions(decodedActions(field(p, 'actions'))),
187
+ }
188
+ if (consensusAppId && status !== 'draft' && status !== 'invalid') {
189
+ const ballot = await readBox(fetchFn, algod, consensusAppId, boxName('guildConsensus', 'ballots', BigInt(proposalId))).catch(() => undefined)
190
+ if (ballot) data.ballot = ballotFrom(decodeBoxValue('guildConsensus', 'ballots', ballot))
191
+ }
192
+ return data
193
+ }
194
+
195
+ // ---------------------------------------------------------------- outcome
196
+
197
+ const HUNDRED = 100_000n
198
+
199
+ function meets(b: GuildBallotData, approvals: bigint, rejections: bigint, turnout: bigint): boolean {
200
+ const participation = BigInt(b.requirements.participation)
201
+ const approval = BigInt(b.requirements.approval)
202
+ const minimum = BigInt(b.requirements.minimumApprovals)
203
+ const eligible = BigInt(b.eligiblePower || '0')
204
+ const decisive = approvals + rejections
205
+ return (
206
+ approvals > 0n &&
207
+ (participation === 0n || turnout * HUNDRED >= eligible * participation) &&
208
+ (minimum === 0n || approvals >= minimum) &&
209
+ (approval === 0n || (decisive > 0n && approvals * HUNDRED >= decisive * approval))
210
+ )
211
+ }
212
+
213
+ /**
214
+ * How an open ballot resolves if it is closed now (`now` in Unix seconds),
215
+ * mirroring the consensus contract: after `votingEnd` by the votes cast; before
216
+ * it only once the members who have not voted cannot change the result.
217
+ * `undefined` while the result is still open.
218
+ */
219
+ export function guildBallotOutcome(b: GuildBallotData, now: number): 'approved' | 'rejected' | undefined {
220
+ if (b.resolved) return undefined
221
+ const approvals = BigInt(b.votes.approvals)
222
+ const rejections = BigInt(b.votes.rejections)
223
+ const turnout = approvals + rejections + BigInt(b.votes.abstains)
224
+ const passes = meets(b, approvals, rejections, turnout)
225
+ if (now > Number(b.votingEnd)) return passes ? 'approved' : 'rejected'
226
+ const eligible = BigInt(b.eligiblePower || '0')
227
+ if (eligible === 0n) return undefined
228
+ const remaining = eligible > turnout ? eligible - turnout : 0n
229
+ if (passes) return meets(b, approvals, rejections + remaining, turnout) ? 'approved' : undefined
230
+ return meets(b, approvals + remaining, rejections, turnout + remaining) ? undefined : 'rejected'
231
+ }
232
+
233
+ /** A fixed point percentage (100000 = 100%), rounded up so a requirement never reads lower than it is. */
234
+ export function formatGuildPercent(value: string): string {
235
+ const tenths = Number((BigInt(value || '0') * 1_000n + HUNDRED - 1n) / HUNDRED)
236
+ return `${tenths % 10 === 0 ? tenths / 10 : (tenths / 10).toFixed(1)}%`
237
+ }
238
+
239
+ export function guildProposalStatusLabel(status: GuildProposalStatus): string {
240
+ switch (status) {
241
+ case 'draft': return 'Draft'
242
+ case 'invalid': return 'Invalid'
243
+ case 'voting': return 'Voting'
244
+ case 'rejected': return 'Rejected'
245
+ case 'approved': return 'Passed'
246
+ case 'executed': return 'Done'
247
+ }
248
+ }
249
+
250
+ /** The ballot's requirements in a few words, e.g. "Needs 50% yes, at least 1 approval". */
251
+ export function guildRequirementsText(b: GuildBallotData): string {
252
+ const { approval, minimumApprovals, participation } = b.requirements
253
+ return [
254
+ approval !== '0' ? `Needs ${formatGuildPercent(approval)} yes` : null,
255
+ minimumApprovals !== '0' ? `at least ${minimumApprovals} approval${minimumApprovals === '1' ? '' : 's'}` : null,
256
+ participation !== '0' ? `${formatGuildPercent(participation)} turnout` : null,
257
+ ]
258
+ .filter(Boolean)
259
+ .join(', ')
260
+ }
package/src/index.ts CHANGED
@@ -78,6 +78,20 @@ export {
78
78
  type DualStakeData,
79
79
  type PrivateContentData,
80
80
  } from './akita'
81
+ export {
82
+ GUILD_ZERO_ADDRESS,
83
+ formatGuildPercent,
84
+ guildBallotOutcome,
85
+ guildProposalStatusLabel,
86
+ guildRequirementsText,
87
+ readGuildProposal,
88
+ splitGuildProposalId,
89
+ summarizeGuildActions,
90
+ type GuildBallotData,
91
+ type GuildProposalAction,
92
+ type GuildProposalData,
93
+ type GuildProposalStatus,
94
+ } from './guild'
81
95
  export {
82
96
  FAMILIES,
83
97
  reduceListing,
package/src/kinds.ts CHANGED
@@ -93,6 +93,21 @@ export const BUILT_IN_KINDS: KindDef[] = [
93
93
  return i < 0 ? { id, params: {} } : { id: id.slice(i + 1), params: { owner: id.slice(0, i) } }
94
94
  },
95
95
  },
96
+ // A Guild proposal. Proposal ids count per Guild, so the cache id is `<guildAppId>/<proposalId>`.
97
+ {
98
+ kind: 'guild-proposal',
99
+ tags: ['GuildProposal'],
100
+ idProps: ['proposalId', 'id'],
101
+ params: ['appId'],
102
+ policy: 'live',
103
+ sources: ['algod'],
104
+ normalizeId: (id) => (/^(\d+\/)?\d+$/.test(id.trim()) ? id.trim() : undefined),
105
+ composeId: (id, params) => (params.appId && !id.includes('/') ? `${params.appId}/${id}` : id),
106
+ decomposeId: (id): { id: string; params: Record<string, string> } => {
107
+ const i = id.indexOf('/')
108
+ return i < 0 ? { id, params: {} } : { id: id.slice(i + 1), params: { appId: id.slice(0, i) } }
109
+ },
110
+ },
96
111
  // Title is the author's own label in their post, not chain data.
97
112
  hosted('private-content', 'PrivateContent', ['id'], 'slow', { params: ['title'] }),
98
113
  // A page inside the same post. The host supplies its preview and navigation.
@@ -6,6 +6,7 @@ import { extractRefs } from '../grammar'
6
6
  import type { IdentityData } from '../identity'
7
7
  import type { AccountData, ApplicationData, AssetData, CollectionData } from '../sources/chain-data'
8
8
  import type { NfdData } from '../sources/nfd'
9
+ import type { GuildProposalData } from '../guild'
9
10
  import type { DualStakeData, PrivateContentData, RewardData, StakingPoolData, SubscriptionData, TradeData } from '../akita'
10
11
  import type { AssetMediaData } from '../media'
11
12
  import type { Entity, Ref } from '../types'
@@ -92,6 +93,8 @@ export const useSubscription = (id?: string, o?: UseEntityOptions) => useEntity<
92
93
  export const useTrade = (offerId?: string, o?: UseEntityOptions) => useEntity<TradeData>('trade', offerId, o)
93
94
  export const useDualStake = (appId?: string, o?: UseEntityOptions) => useEntity<DualStakeData>('dual-stake', appId, o)
94
95
  export const usePrivateContent = (id?: string, o?: UseEntityOptions) => useEntity<PrivateContentData>('private-content', id, o)
96
+ /** A Guild proposal; `id` is `<guildAppId>/<proposalId>`. */
97
+ export const useGuildProposal = (id?: string, o?: UseEntityOptions) => useEntity<GuildProposalData>('guild-proposal', id, o)
95
98
  export const usePrizeBox = (appId?: string, o?: UseEntityOptions) => useEntity<PrizeBoxData>('prize-box', appId, o)
96
99
 
97
100
  /**
@@ -5,6 +5,7 @@ import { requestJson } from '../http'
5
5
  import { refKey, type Endpoint, type Entity, type Ref } from '../types'
6
6
  import { accountFromJson, assetFromJson, type ApplicationData } from './chain-data'
7
7
  import { readReward, readStakingPool, readSubscription, readTrade, type SingletonApps } from '../akita'
8
+ import { readGuildProposal } from '../guild'
8
9
  import type { Source, SourceContext } from './types'
9
10
 
10
11
  interface AlgodApp {
@@ -12,7 +13,7 @@ interface AlgodApp {
12
13
  params: { creator?: string; 'global-state'?: TealKeyValue[] }
13
14
  }
14
15
 
15
- const KINDS = new Set(['account', 'asset', 'application'])
16
+ const KINDS = new Set(['account', 'asset', 'application', 'guild-proposal'])
16
17
 
17
18
  /**
18
19
  * Live state from an algod node: accounts, assets, apps, contract global state
@@ -41,6 +42,11 @@ export function createAlgodSource(endpoint: Endpoint, singletons: SingletonApps
41
42
  if (a) out.set(refKey(ref), { ...base, data: accountFromJson(a), provenance: { ...provenance, round: Number(a.round ?? 0) || undefined } })
42
43
  return out
43
44
  }
45
+ if (ref.kind === 'guild-proposal') {
46
+ const data = await readGuildProposal(ctx.fetch, endpoint, ref.id)
47
+ if (data) out.set(refKey(ref), { ...base, data, provenance })
48
+ return out
49
+ }
44
50
  const singleton = singletonKinds[ref.kind]
45
51
  if (singleton) {
46
52
  const data =
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/kinds.ts","../src/grammar.ts"],"sourcesContent":["import type { PolicyName } from './policy'\n\nexport interface KindDef {\n kind: string\n /** Tag names that store this kind in content. The first one is used when serializing. */\n tags: string[]\n /** Props that carry the id, in priority order. The first is used when serializing. */\n idProps: string[]\n /** Extra identifying props kept on the ref (never display data). */\n params?: string[]\n policy: PolicyName\n /** Source names to try, in order. Sources not configured on the client are skipped. */\n sources: string[]\n /** Contract family for chain sources (live state and archive reconstruction). */\n family?: string\n normalizeId?: (id: string) => string | undefined\n /** Builds the cache id from the tag id and params, for kinds keyed by more than one value. */\n composeId?: (id: string, params: Record<string, string>) => string\n /** Inverse of composeId, used when serializing a ref back to a tag. */\n decomposeId?: (id: string) => { id: string; params: Record<string, string> }\n}\n\nconst uint = (id: string) => (/^\\d+$/.test(id.trim()) ? String(BigInt(id.trim())) : undefined)\nconst address = (id: string) => (/^[A-Z2-7]{58}$/.test(id.trim()) ? id.trim() : undefined)\nconst nfdName = (id: string) => {\n const n = id.trim().toLowerCase()\n if (!n) return undefined\n return n.endsWith('.algo') ? n : `${n}.algo`\n}\nconst txid = (id: string) => (/^[A-Z2-7]{52}$/.test(id.trim()) ? id.trim() : undefined)\nconst any = (id: string) => (id.trim() ? id.trim() : undefined)\n\nconst contract = (kind: string, tag: string, family: string): KindDef => ({\n kind,\n tags: [tag],\n idProps: ['appId', 'id'],\n policy: 'live',\n sources: ['resolver', 'algod', 'indexer'],\n family,\n normalizeId: uint,\n})\n\nconst hosted = (kind: string, tag: string, idProps: string[], policy: PolicyName, extra: Partial<KindDef> = {}): KindDef => ({\n kind,\n tags: [tag],\n idProps,\n policy,\n sources: ['resolver'],\n normalizeId: any,\n ...extra,\n})\n\nexport const BUILT_IN_KINDS: KindDef[] = [\n { kind: 'account', tags: ['Account'], idProps: ['address', 'id'], policy: 'live', sources: ['algod', 'indexer'], normalizeId: address },\n { kind: 'identity', tags: [], idProps: ['address'], policy: 'slow', sources: ['identity'], normalizeId: address },\n { kind: 'asset', tags: ['ASA', 'Asset'], idProps: ['id', 'assetId'], policy: 'slow', sources: ['resolver', 'algod', 'indexer'], normalizeId: uint },\n { kind: 'application', tags: ['Application'], idProps: ['id', 'appId'], policy: 'slow', sources: ['resolver', 'indexer', 'algod'], normalizeId: uint },\n // A contract's ARC-56 interface: its `ui` actions and cards. Keyed by app id.\n { kind: 'interface', tags: [], idProps: ['appId', 'id'], policy: 'slow', sources: ['interface'], normalizeId: uint },\n { kind: 'asset-media', tags: [], idProps: ['id'], policy: 'slow', sources: ['media'], normalizeId: uint },\n { kind: 'transaction', tags: ['Transaction'], idProps: ['id', 'txId', 'txid'], policy: 'immutable', sources: ['indexer'], normalizeId: txid },\n { kind: 'nfd', tags: ['NFDProfile'], idProps: ['name', 'id'], policy: 'slow', sources: ['nfd'], normalizeId: nfdName },\n { kind: 'nfd-address', tags: [], idProps: ['address'], policy: 'slow', sources: ['nfd'], normalizeId: address },\n contract('listing', 'NFTListing', 'listing'),\n contract('auction', 'AuctionListing', 'auction'),\n contract('raffle', 'RaffleListing', 'raffle'),\n contract('poll', 'Poll', 'poll'),\n contract('prize-box', 'PrizeBox', 'prize-box'),\n hosted('collection', 'Collection', ['id', 'collectionId'], 'slow'),\n contract('staking-pool', 'StakingPool', 'staking-pool'),\n // HyperSwap is a singleton, so an offer id is enough. Older app tags also carry appId.\n // Chain first while the box exists; the resolver keeps closed offers and their participants.\n { kind: 'trade', tags: ['TradeOffer'], idProps: ['offerId', 'id'], params: ['appId'], policy: 'live', sources: ['algod', 'resolver'], normalizeId: uint },\n hosted('dual-stake', 'DualStakePool', ['appId', 'id'], 'live', { params: ['rewardAssetId'], normalizeId: uint }),\n // A disbursement id in the Rewards singleton.\n { kind: 'reward', tags: ['Reward'], idProps: ['id'], policy: 'live', sources: ['algod', 'resolver'], normalizeId: uint },\n // Services are numbered per owner, so the cache id is `<owner>/<serviceId>` when the owner is known.\n {\n kind: 'subscription',\n tags: ['Subscription'],\n idProps: ['id', 'serviceId'],\n params: ['owner', 'creator', 'address'],\n policy: 'slow',\n // Without an owner only the resolver can find the service.\n sources: ['algod', 'resolver'],\n normalizeId: (id) => (/^([A-Z2-7]{58}\\/)?\\d+$/.test(id.trim()) ? id.trim() : undefined),\n composeId: (id, params) => {\n const owner = params.owner ?? params.creator ?? params.address\n return owner && !id.includes('/') ? `${owner}/${id}` : id\n },\n decomposeId: (id): { id: string; params: Record<string, string> } => {\n const i = id.indexOf('/')\n return i < 0 ? { id, params: {} } : { id: id.slice(i + 1), params: { owner: id.slice(0, i) } }\n },\n },\n // Title is the author's own label in their post, not chain data.\n hosted('private-content', 'PrivateContent', ['id'], 'slow', { params: ['title'] }),\n // A page inside the same post. The host supplies its preview and navigation.\n { kind: 'page-card', tags: ['PageCard'], idProps: ['page', 'next', 'href', 'id'], params: ['title'], policy: 'immutable', sources: [], normalizeId: any },\n]\n\nexport class KindRegistry {\n private byKind = new Map<string, KindDef>()\n private byTag = new Map<string, KindDef>()\n\n constructor(defs: KindDef[] = BUILT_IN_KINDS) {\n defs.forEach((d) => this.register(d))\n }\n\n register(def: KindDef): void {\n this.byKind.set(def.kind, def)\n for (const tag of def.tags) this.byTag.set(tag, def)\n }\n\n get(kind: string): KindDef | undefined {\n return this.byKind.get(kind)\n }\n\n require(kind: string): KindDef {\n const def = this.byKind.get(kind)\n if (!def) throw new Error(`Unknown AlgoMD kind: ${kind}`)\n return def\n }\n\n forTag(tag: string): KindDef | undefined {\n return this.byTag.get(tag)\n }\n\n tags(): string[] {\n return [...this.byTag.keys()]\n }\n\n all(): KindDef[] {\n return [...this.byKind.values()]\n }\n}\n\nexport const defaultRegistry = new KindRegistry()\n","import { defaultRegistry, type KindRegistry } from './kinds'\nimport type { Ref, Size } from './types'\n\n/**\n * The AlgoMD content grammar. Stored content uses self-closing tags such as\n * `<ASA id=\"31566704\" />`. Existing posts are a compatibility boundary: tag\n * names, id props and legacy size props keep working.\n *\n * Only identifiers survive parsing. Any other prop is dropped, so content can\n * never supply the data that gets displayed.\n */\n\nexport interface RefSegment {\n type: 'ref'\n ref: Ref & { size?: Size }\n tag: string\n raw: string\n}\n\nexport interface TextSegment {\n type: 'text'\n content: string\n}\n\nexport type Segment = TextSegment | RefSegment\n\nconst PROPS = `((?:\"[^\"]*\"|'[^']*'|“[^”]*”|‘[^’]*’|\\\\{[^}]*\\\\}|[^'\"“”‘’>])*)`\nconst PROP_RE = /([A-Za-z_][\\w-]*)(?:\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|\\{([^}]*)\\}|“([^”]*)”|‘([^’]*)’))?/g\nconst FENCE_RE = /```[\\s\\S]*?```/g\nconst ENTITIES: Record<string, string> = { amp: '&', quot: '\"', apos: \"'\", lt: '<', gt: '>' }\n\nconst tagRegexCache = new WeakMap<KindRegistry, { source: string; tags: string }>()\n\nfunction tagRegex(registry: KindRegistry): RegExp {\n const tags = registry.tags().sort((a, b) => b.length - a.length).join('|')\n let cached = tagRegexCache.get(registry)\n if (!cached || cached.tags !== tags) {\n cached = { tags, source: `<(${tags})\\\\b${PROPS}\\\\/>` }\n tagRegexCache.set(registry, cached)\n }\n return new RegExp(cached.source, 'g')\n}\n\nfunction decodeEntities(value: string): string {\n return value.replace(/&(amp|quot|apos|lt|gt);/g, (_, e: string) => ENTITIES[e] ?? _)\n}\n\nexport function parseProps(raw: string): Record<string, string> {\n const props: Record<string, string> = {}\n for (const m of raw.matchAll(PROP_RE)) {\n const name = m[1]!\n props[name] = decodeEntities(m[2] ?? m[3] ?? m[4] ?? m[5] ?? m[6] ?? 'true').trim()\n }\n return props\n}\n\n/** Maps current and legacy size props to a size. */\nexport function sizeFromProps(props: Record<string, string>): Size | undefined {\n const size = props.size?.toLowerCase()\n switch (size) {\n case 'inline':\n case 'small':\n case 'large':\n return size\n case 'xs':\n case 'sm':\n return 'inline'\n // Legacy `full` meant a full-width card. The full-screen size is stored as `fullscreen`.\n case 'md':\n case 'lg':\n case 'xl':\n case 'full':\n return 'large'\n case 'fullscreen':\n return 'full'\n }\n if (props.inline === 'true' || props.compact === 'true') return 'inline'\n return undefined\n}\n\n/** Builds a ref from a tag name and its props, or undefined if the id is missing or malformed. */\nexport function refFromTag(tag: string, props: Record<string, string>, registry: KindRegistry = defaultRegistry): (Ref & { size?: Size }) | undefined {\n const def = registry.forTag(tag)\n if (!def) return undefined\n const rawId = def.idProps.map((p) => props[p]).find((v) => v && v !== 'true')\n if (!rawId) return undefined\n const params = Object.fromEntries((def.params ?? []).filter((p) => props[p]).map((p) => [p, props[p]!]))\n const composed = def.composeId ? def.composeId(rawId, params) : rawId\n const id = def.normalizeId ? def.normalizeId(composed) : composed\n if (!id) return undefined\n const ref: Ref & { size?: Size } = { kind: def.kind, id }\n if (Object.keys(params).length) ref.params = params\n const size = sizeFromProps(props)\n if (size) ref.size = size\n return ref\n}\n\n/** Splits content into text and reference segments. Tags inside fenced code stay text. */\nexport function parseContent(content: string, registry: KindRegistry = defaultRegistry): Segment[] {\n if (!content) return []\n const fences = [...content.matchAll(FENCE_RE)].map((m) => [m.index!, m.index! + m[0].length] as const)\n const inFence = (i: number) => fences.some(([s, e]) => i >= s && i < e)\n\n const out: Segment[] = []\n let last = 0\n for (const m of content.matchAll(tagRegex(registry))) {\n const at = m.index!\n if (inFence(at)) continue\n const ref = refFromTag(m[1]!, parseProps(m[2] ?? ''), registry)\n if (!ref) continue\n if (at > last) out.push({ type: 'text', content: content.slice(last, at) })\n out.push({ type: 'ref', ref, tag: m[1]!, raw: m[0] })\n last = at + m[0].length\n }\n if (last < content.length) out.push({ type: 'text', content: content.slice(last) })\n return out\n}\n\n/** Every distinct reference in the content, in order of first appearance. */\nexport function extractRefs(content: string, registry: KindRegistry = defaultRegistry): Ref[] {\n const seen = new Map<string, Ref>()\n for (const s of parseContent(content, registry)) {\n if (s.type !== 'ref') continue\n const key = `${s.ref.kind}:${s.ref.id}`\n if (!seen.has(key)) seen.set(key, { kind: s.ref.kind, id: s.ref.id, ...(s.ref.params ? { params: s.ref.params } : {}) })\n }\n return [...seen.values()]\n}\n\nexport function hasRefs(content: string, registry: KindRegistry = defaultRegistry): boolean {\n return parseContent(content, registry).some((s) => s.type === 'ref')\n}\n\nconst attr = (v: string) => v.replace(/&/g, '&amp;').replace(/\"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;')\n\n/**\n * Serializes a reference as a tag. Sizes are written in a form that older\n * clients also understand: inline as `compact`, full as `size=\"fullscreen\"`.\n */\nexport function serializeRef(ref: Ref & { size?: Size }, registry: KindRegistry = defaultRegistry): string {\n const def = registry.require(ref.kind)\n const tag = def.tags[0]\n if (!tag) throw new Error(`Kind ${ref.kind} has no tag and cannot be stored in content`)\n const split = def.decomposeId ? def.decomposeId(ref.id) : { id: ref.id, params: {} as Record<string, string> }\n const params = { ...split.params, ...ref.params }\n const parts = [`${def.idProps[0]}=\"${attr(split.id)}\"`]\n for (const p of def.params ?? []) if (params[p]) parts.push(`${p}=\"${attr(params[p])}\"`)\n if (ref.size === 'inline') parts.push('compact')\n else if (ref.size === 'small') parts.push('size=\"small\"')\n else if (ref.size === 'full') parts.push('size=\"fullscreen\"')\n return `<${tag} ${parts.join(' ')} />`\n}\n"],"mappings":"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","names":[]}