@konemono/nostr-content-parser
Version:
Parse Nostr content into tokens
263 lines (224 loc) • 7.6 kB
text/typescript
// patterns.ts
// 既存の TokenType 定義に加えて型も作成
export const TokenType = {
TEXT: "text",
// NPUB: "npub",
// NPROFILE: "nprofile",
// NOTE: "note",
// NEVENT: "nevent",
// NADDR: "naddr",
// NSEC: "nsec",
NIP19: "nip19", // 統合されたNIP19タイプ
RELAY: "relay",
URL: "url",
CUSTOM_EMOJI: "custom_emoji",
HASHTAG: "hashtag",
LN_ADDRESS: "ln_address",
LN_URL: "ln_url",
LNBC: "lnbc",
EMAIL: "email",
CASHU_TOKEN: "cashu_token",
BITCOIN_ADDRESS: "bitcoin_address",
NIP_IDENTIFIER: "nip_identifier",
LEGACY_REFERENCE: "legacy_reference", // 旧タイプ引用 #[3]
} as const;
export type TokenType = (typeof TokenType)[keyof typeof TokenType];
// NIP19のサブタイプ定義
export const NIP19SubType = {
NPUB: "npub",
NPROFILE: "nprofile",
NOTE: "note",
NEVENT: "nevent",
NADDR: "naddr",
NSEC: "nsec",
} as const;
export type NIP19SubType = (typeof NIP19SubType)[keyof typeof NIP19SubType];
// NIP19サブタイプマッピング
export const NIP19_TYPE_MAP: Record<string, NIP19SubType> = {
[NIP19SubType.NPUB]: NIP19SubType.NPUB,
[NIP19SubType.NPROFILE]: NIP19SubType.NPROFILE,
[NIP19SubType.NOTE]: NIP19SubType.NOTE,
[NIP19SubType.NEVENT]: NIP19SubType.NEVENT,
[NIP19SubType.NADDR]: NIP19SubType.NADDR,
[NIP19SubType.NSEC]: NIP19SubType.NSEC,
};
export interface Token {
type: TokenType;
content: string;
start: number;
end: number;
metadata?: Record<string, unknown>;
}
// 必要なパターンをエクスポート
export const LN_ADDRESS_PATTERN =
/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
// ヘルパー関数もエクスポート
export function isLightningAddress(emailLike: string): boolean {
const commonEmailDomains = [
"gmail.com",
"yahoo.com",
"hotmail.com",
"outlook.com",
"icloud.com",
"protonmail.com",
"aol.com",
"live.com",
];
const domain = emailLike.split("@")[1]?.toLowerCase();
return !!domain && !commonEmailDomains.includes(domain);
}
export function findCustomEmojiMetadata(
emojiName: string,
tags: string[][]
): { url: string } | null {
if (!tags) return null;
const emojiTag = tags.find(
(tag) => tag[0] === "emoji" && tag[1] === emojiName
);
return emojiTag ? { url: emojiTag[2] } : null;
}
export const NIP19_PATTERNS = {
npub: /nostr:npub1[023456789acdefghjklmnpqrstuvwxyz]{58}/g,
nprofile: /nostr:nprofile1[023456789acdefghjklmnpqrstuvwxyz]+/g,
note: /nostr:note1[023456789acdefghjklmnpqrstuvwxyz]{58}/g,
nevent: /nostr:nevent1[023456789acdefghjklmnpqrstuvwxyz]+/g,
naddr: /nostr:naddr1[023456789acdefghjklmnpqrstuvwxyz]+/g,
nsec: /nostr:nsec1[023456789acdefghjklmnpqrstuvwxyz]{58}/g,
} as const;
export const NIP19_PLAIN_PATTERNS = {
npub: /(? = {
")": "(",
")": "(",
"]": "[",
"」": "「",
"}": "{",
"}": "{",
">": "<",
"〉": "〈",
"』": "『",
"》": "《",
};
const trailingChars = /[..,,;;::!!??→←]/;
// 文字をエスケープするヘルパー関数
const escapeRegExp = (string: string): string => {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
};
export const cleanUrlEnd = (url: string): string => {
let cleanedUrl = url;
// 末尾の文字を繰り返しチェック
while (cleanedUrl.length > 0) {
const lastChar = cleanedUrl.slice(-1);
// 句読点の場合は即座に除去
if (trailingChars.test(lastChar)) {
cleanedUrl = cleanedUrl.slice(0, -1);
continue;
}
// 括弧の場合はペアリングをチェック
if (Object.keys(brackets).includes(lastChar)) {
const openChar = brackets[lastChar];
const escapedOpenChar = escapeRegExp(openChar);
const escapedLastChar = escapeRegExp(lastChar);
const openCount = (
cleanedUrl.match(new RegExp(escapedOpenChar, "g")) || []
).length;
const closeCount = (
cleanedUrl.match(new RegExp(escapedLastChar, "g")) || []
).length;
// 開き括弧の数が閉じ括弧の数以上であれば、URLの一部とみなして除去を終了
if (openCount >= closeCount) {
break;
}
cleanedUrl = cleanedUrl.slice(0, -1);
continue;
}
// その他の文字の場合は処理を終了
break;
}
return cleanedUrl;
};
//---------------
// 旧タイプ引用のメタデータを取得する関数
export function findLegacyReferenceMetadata(
referenceMatch: string,
tags: string[][]
): {
tagIndex: number;
tagType?: string;
referenceId?: string;
referenceType?: "npub" | "note" | "naddr" | "unknown";
} | null {
// #[3] から数字部分を抽出
const indexMatch = referenceMatch.match(/#\[(\d+)\]/);
if (!indexMatch) return null;
const tagIndex = parseInt(indexMatch[1], 10);
if (!tags || tagIndex >= tags.length) {
return { tagIndex };
}
const tag = tags[tagIndex];
if (!tag || tag.length < 2) {
return { tagIndex };
}
const tagType = tag[0];
const referenceId = tag[1];
// tagTypeに基づいて参照タイプを判定
let referenceType: "npub" | "note" | "naddr" | "unknown" = "unknown";
if (tagType === "p") {
referenceType = "npub";
} else if (tagType === "e") {
referenceType = "note";
} else if (tagType === "a") {
referenceType = "naddr";
}
return {
tagIndex,
tagType,
referenceId,
referenceType,
};
}