You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
142 lines
4.4 KiB
142 lines
4.4 KiB
/** |
|
* `XMR: 4abc…` / `BTC: bc1…` lines in kind 0 `about` (catalog-driven labels). |
|
*/ |
|
|
|
import paytoTypesCatalog from '@/data/payto-types.json' |
|
import { buildPaytoUri } from '@/lib/payto' |
|
import { getCanonicalPaytoType, getPaytoEditorTypeLabel, isKnownPaytoType } from '@/lib/payto-registry' |
|
import type { Kind0ImportedPaymentMethod } from '@/lib/payto-kind0-import' |
|
|
|
type PaytoAboutCoinCatalog = { |
|
kind0CryptocurrencyAddresses?: Record<string, string> |
|
aliases?: Record<string, string> |
|
} |
|
|
|
const catalog = paytoTypesCatalog as PaytoAboutCoinCatalog |
|
|
|
function escapeRegExp(s: string): string { |
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') |
|
} |
|
|
|
function mapCoinLabelToPaytoType(label: string): string | null { |
|
const k = label.trim().toLowerCase() |
|
if (!k) return null |
|
const fromCrypto = catalog.kind0CryptocurrencyAddresses?.[k] |
|
if (fromCrypto) return getCanonicalPaytoType(fromCrypto) |
|
const canonical = getCanonicalPaytoType(k) |
|
return isKnownPaytoType(canonical) ? canonical : null |
|
} |
|
|
|
function buildAboutCoinLabelAlternation(): string { |
|
const labels = new Set<string>() |
|
const crypto = catalog.kind0CryptocurrencyAddresses ?? {} |
|
for (const key of Object.keys(crypto)) { |
|
labels.add(key) |
|
labels.add(key.toUpperCase()) |
|
} |
|
for (const [alias, canonical] of Object.entries(catalog.aliases ?? {})) { |
|
if (crypto[alias] || Object.values(crypto).includes(canonical)) { |
|
labels.add(alias) |
|
labels.add(alias.toUpperCase()) |
|
} |
|
} |
|
return [...labels] |
|
.sort((a, b) => b.length - a.length) |
|
.map(escapeRegExp) |
|
.join('|') |
|
} |
|
|
|
let aboutCoinLineRegex: RegExp | null = null |
|
|
|
function getAboutCoinLineRegex(): RegExp | null { |
|
if (aboutCoinLineRegex) return aboutCoinLineRegex |
|
const alternation = buildAboutCoinLabelAlternation() |
|
if (!alternation) return null |
|
aboutCoinLineRegex = new RegExp( |
|
`(?:^|[\\n\\r])\\s*(${alternation})\\s*:\\s*([^\\s\\n]+)`, |
|
'gi' |
|
) |
|
return aboutCoinLineRegex |
|
} |
|
|
|
export type AboutCoinLineMatch = { |
|
coinLabel: string |
|
authority: string |
|
paytoType: string |
|
payto: string |
|
displayType: string |
|
/** Full matched segment including label and address (for content replacement). */ |
|
raw: string |
|
} |
|
|
|
export function parseAboutCoinLabelPaymentLines(about: string): AboutCoinLineMatch[] { |
|
const text = about?.trim() |
|
if (!text) return [] |
|
const regex = getAboutCoinLineRegex() |
|
if (!regex) return [] |
|
|
|
const seen = new Set<string>() |
|
const out: AboutCoinLineMatch[] = [] |
|
regex.lastIndex = 0 |
|
|
|
for (const match of text.matchAll(regex)) { |
|
const coinLabel = match[1] ?? '' |
|
const authority = (match[2] ?? '').trim() |
|
if (!authority) continue |
|
const paytoType = mapCoinLabelToPaytoType(coinLabel) |
|
if (!paytoType) continue |
|
const dedupe = `${paytoType}:${authority.toLowerCase()}` |
|
if (seen.has(dedupe)) continue |
|
seen.add(dedupe) |
|
out.push({ |
|
coinLabel, |
|
authority, |
|
paytoType, |
|
payto: buildPaytoUri(paytoType, authority), |
|
displayType: getPaytoEditorTypeLabel(paytoType), |
|
raw: match[0] |
|
}) |
|
} |
|
return out |
|
} |
|
|
|
export function extractAboutCoinPaymentMethods(about: string): Kind0ImportedPaymentMethod[] { |
|
return parseAboutCoinLabelPaymentLines(about).map((m) => ({ |
|
type: m.paytoType, |
|
authority: m.authority, |
|
payto: m.payto, |
|
displayType: m.displayType |
|
})) |
|
} |
|
|
|
/** Split profile about text into plain segments and payto URIs for {@link parseContent}. */ |
|
export function parseAboutContentWithCoinPayto(content: string): import('@/lib/content-parser').TEmbeddedNode[] { |
|
const text = content |
|
if (!text) return [{ type: 'text', data: '' }] |
|
|
|
const regex = getAboutCoinLineRegex() |
|
if (!regex) return [{ type: 'text', data: text }] |
|
|
|
const result: import('@/lib/content-parser').TEmbeddedNode[] = [] |
|
let lastIndex = 0 |
|
regex.lastIndex = 0 |
|
|
|
for (const match of text.matchAll(regex)) { |
|
const matchStart = match.index ?? 0 |
|
const coinLabel = match[1] ?? '' |
|
const authority = (match[2] ?? '').trim() |
|
const paytoType = mapCoinLabelToPaytoType(coinLabel) |
|
if (!authority || !paytoType) continue |
|
|
|
if (matchStart > lastIndex) { |
|
result.push({ type: 'text', data: text.slice(lastIndex, matchStart) }) |
|
} |
|
result.push({ type: 'payto', data: buildPaytoUri(paytoType, authority) }) |
|
lastIndex = matchStart + match[0].length |
|
} |
|
|
|
if (lastIndex < text.length) { |
|
result.push({ type: 'text', data: text.slice(lastIndex) }) |
|
} |
|
return result.length > 0 ? result : [{ type: 'text', data: text }] |
|
}
|
|
|