@qntx/nostr 0.3.1 → 0.4.0
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/dist/nips/nip27.d.mts +28 -15
- package/dist/nips/nip27.mjs +192 -159
- package/dist/nips/nip27.mjs.map +1 -1
- package/package.json +1 -1
package/dist/nips/nip27.d.mts
CHANGED
|
@@ -6,36 +6,49 @@ export type ContentBlock = {
|
|
|
6
6
|
text: string;
|
|
7
7
|
} | {
|
|
8
8
|
type: "reference";
|
|
9
|
-
pointer: ProfilePointer |
|
|
9
|
+
pointer: ProfilePointer | EventPointer | AddressPointer;
|
|
10
|
+
/**
|
|
11
|
+
* `true` when the reference had no `nostr:` prefix (see
|
|
12
|
+
* {@link ParseContentOptions.legacyBech32}).
|
|
13
|
+
*/
|
|
14
|
+
bare: boolean;
|
|
10
15
|
} | {
|
|
11
16
|
type: "url";
|
|
12
17
|
url: string;
|
|
13
18
|
} | {
|
|
14
|
-
type: "
|
|
15
|
-
url: string;
|
|
16
|
-
} | {
|
|
17
|
-
type: "image";
|
|
19
|
+
type: "image" | "video" | "audio";
|
|
18
20
|
url: string;
|
|
19
21
|
} | {
|
|
20
|
-
type: "
|
|
22
|
+
type: "relay";
|
|
21
23
|
url: string;
|
|
22
24
|
} | {
|
|
23
|
-
type: "
|
|
24
|
-
|
|
25
|
+
type: "hashtag";
|
|
26
|
+
value: string;
|
|
25
27
|
} | {
|
|
26
28
|
type: "emoji";
|
|
27
29
|
shortcode: string;
|
|
28
30
|
url: string;
|
|
29
31
|
} | {
|
|
30
|
-
type: "
|
|
31
|
-
|
|
32
|
+
type: "invoice";
|
|
33
|
+
bolt11: string;
|
|
34
|
+
};
|
|
35
|
+
export type ParseContentOptions = {
|
|
36
|
+
/**
|
|
37
|
+
* Recognize bare `npub`, `nprofile`, `note`, `nevent`, `naddr` without the `nostr:` prefix.
|
|
38
|
+
* Default `false`.
|
|
39
|
+
*/
|
|
40
|
+
legacyBech32?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* URL → MIME type (the `m` field of NIP-92 `imeta` tags); takes precedence over extension
|
|
43
|
+
* classification.
|
|
44
|
+
*/
|
|
45
|
+
imeta?: ReadonlyMap<string, string>;
|
|
32
46
|
};
|
|
33
47
|
/**
|
|
34
|
-
* Parse note content (or a full event, to pick up emoji tags) into blocks.
|
|
35
|
-
*
|
|
48
|
+
* Parse note content (or a full event, to pick up `emoji` tags) into ordered blocks. One
|
|
49
|
+
* left-to-right scan; at every position the rules fire in the alternation order above. Adjacent
|
|
50
|
+
* text merges and no empty text blocks are emitted.
|
|
36
51
|
*/
|
|
37
|
-
export declare function parseContent(content: string | Pick<Event, "content" | "tags"
|
|
38
|
-
/** Collect all content blocks into an array. */
|
|
39
|
-
export declare function parseContentBlocks(content: string | Pick<Event, "content" | "tags">): ContentBlock[];
|
|
52
|
+
export declare function parseContent(content: string | Pick<Event, "content" | "tags">, opts?: ParseContentOptions): ContentBlock[];
|
|
40
53
|
//#endregion
|
|
41
54
|
//# sourceMappingURL=nip27.d.mts.map
|
package/dist/nips/nip27.mjs
CHANGED
|
@@ -1,176 +1,209 @@
|
|
|
1
1
|
import { decode } from "./nip19.mjs";
|
|
2
2
|
//#region src/nips/nip27.ts
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const
|
|
3
|
+
const BECH32_CHARS = "02-9ac-hj-np-z";
|
|
4
|
+
const NIP19_PREFIX = "(?:npub|nprofile|note|nevent|naddr|nsec)1";
|
|
5
|
+
const NIP19_BARE_PREFIX = "(?:npub|nprofile|note|nevent|naddr)1";
|
|
6
|
+
const URL_RUN = `[^\\s<>"\\u3000-\\u303f\\uff00-\\uffef]+`;
|
|
7
|
+
const SCAN_BODY = [
|
|
8
|
+
`(?<nostr>nostr:${NIP19_PREFIX}[${BECH32_CHARS}]+)`,
|
|
9
|
+
`(?<url>https?://${URL_RUN})`,
|
|
10
|
+
`(?<relay>wss?://${URL_RUN})`,
|
|
11
|
+
`(?<invoice>(?:lightning:)?(?:lnbc|lntbs|lntb|lnbcrt)1[${BECH32_CHARS}]+)`,
|
|
12
|
+
`(?<hashtag>#[\\p{L}\\p{M}\\p{N}_]{1,42})`,
|
|
13
|
+
`(?<emoji>:[A-Za-z0-9_]+:)`
|
|
14
|
+
].join("|");
|
|
15
|
+
const SCAN = new RegExp(SCAN_BODY, "giu");
|
|
16
|
+
const SCAN_LEGACY = new RegExp(`${SCAN_BODY}|(?<bare>${NIP19_BARE_PREFIX}[${BECH32_CHARS}]+)`, "giu");
|
|
17
|
+
const TAG_CHAR = /[\p{L}\p{M}\p{N}_]/u;
|
|
18
|
+
const WORD_CHAR = /[\p{L}\p{N}]/u;
|
|
19
|
+
const TRAILING_URL_CHARS = /* @__PURE__ */ new Set([
|
|
20
|
+
".",
|
|
21
|
+
",",
|
|
22
|
+
";",
|
|
23
|
+
":",
|
|
24
|
+
"!",
|
|
25
|
+
"?",
|
|
26
|
+
"'"
|
|
27
|
+
]);
|
|
28
|
+
const IMAGE_EXT = /* @__PURE__ */ new Set([
|
|
29
|
+
"jpg",
|
|
30
|
+
"jpeg",
|
|
31
|
+
"png",
|
|
32
|
+
"gif",
|
|
33
|
+
"webp",
|
|
34
|
+
"avif",
|
|
35
|
+
"svg"
|
|
36
|
+
]);
|
|
37
|
+
const VIDEO_EXT = /* @__PURE__ */ new Set([
|
|
38
|
+
"mp4",
|
|
39
|
+
"webm",
|
|
40
|
+
"mov",
|
|
41
|
+
"m4v",
|
|
42
|
+
"m3u8"
|
|
43
|
+
]);
|
|
44
|
+
const AUDIO_EXT = /* @__PURE__ */ new Set([
|
|
45
|
+
"mp3",
|
|
46
|
+
"m4a",
|
|
47
|
+
"ogg",
|
|
48
|
+
"oga",
|
|
49
|
+
"opus",
|
|
50
|
+
"wav",
|
|
51
|
+
"flac",
|
|
52
|
+
"aac"
|
|
53
|
+
]);
|
|
54
|
+
function isChar(cp, re) {
|
|
55
|
+
return cp !== void 0 && re.test(String.fromCodePoint(cp));
|
|
56
|
+
}
|
|
57
|
+
/** Drop trailing sentence punctuation and closers that have no opener inside the URL. */
|
|
58
|
+
function trimUrlTail(raw) {
|
|
59
|
+
let url = raw;
|
|
60
|
+
for (;;) {
|
|
61
|
+
const last = url.at(-1);
|
|
62
|
+
if (last !== void 0 && TRAILING_URL_CHARS.has(last)) {
|
|
63
|
+
url = url.slice(0, -1);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (last === ")" || last === "]") {
|
|
67
|
+
const open = last === ")" ? "(" : "[";
|
|
68
|
+
let opens = 0;
|
|
69
|
+
let closes = 0;
|
|
70
|
+
for (const c of url) if (c === open) opens++;
|
|
71
|
+
else if (c === last) closes++;
|
|
72
|
+
if (closes > opens) {
|
|
73
|
+
url = url.slice(0, -1);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return url;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function mimeMedia(mime) {
|
|
81
|
+
if (mime === void 0) return;
|
|
82
|
+
const slash = mime.indexOf("/");
|
|
83
|
+
const base = slash === -1 ? mime : mime.slice(0, slash);
|
|
84
|
+
return base === "image" || base === "video" || base === "audio" ? base : void 0;
|
|
85
|
+
}
|
|
86
|
+
function extMedia(pathname) {
|
|
87
|
+
const dot = pathname.lastIndexOf(".");
|
|
88
|
+
if (dot === -1) return;
|
|
89
|
+
const ext = pathname.slice(dot + 1).toLowerCase();
|
|
90
|
+
if (IMAGE_EXT.has(ext)) return "image";
|
|
91
|
+
if (VIDEO_EXT.has(ext)) return "video";
|
|
92
|
+
return AUDIO_EXT.has(ext) ? "audio" : void 0;
|
|
93
|
+
}
|
|
94
|
+
function urlBlock(raw, relay, imeta) {
|
|
95
|
+
const url = trimUrlTail(raw);
|
|
96
|
+
let parsed;
|
|
97
|
+
try {
|
|
98
|
+
parsed = new URL(url);
|
|
99
|
+
} catch {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (!parsed.hostname.includes(".")) return;
|
|
103
|
+
if (relay) return {
|
|
104
|
+
type: "relay",
|
|
105
|
+
url
|
|
106
|
+
};
|
|
107
|
+
const media = mimeMedia(imeta?.get(url)) ?? extMedia(parsed.pathname);
|
|
108
|
+
return media === void 0 ? {
|
|
109
|
+
type: "url",
|
|
110
|
+
url
|
|
111
|
+
} : {
|
|
112
|
+
type: media,
|
|
113
|
+
url
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function referenceFor(code, bare) {
|
|
117
|
+
let pointer;
|
|
118
|
+
try {
|
|
119
|
+
const decoded = decode(code);
|
|
120
|
+
switch (decoded.type) {
|
|
121
|
+
case "npub":
|
|
122
|
+
pointer = { pubkey: decoded.data };
|
|
123
|
+
break;
|
|
124
|
+
case "note":
|
|
125
|
+
pointer = { id: decoded.data };
|
|
126
|
+
break;
|
|
127
|
+
case "nprofile":
|
|
128
|
+
case "nevent":
|
|
129
|
+
case "naddr":
|
|
130
|
+
pointer = decoded.data;
|
|
131
|
+
break;
|
|
132
|
+
case "nsec": return;
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
type: "reference",
|
|
139
|
+
pointer,
|
|
140
|
+
bare
|
|
141
|
+
};
|
|
142
|
+
}
|
|
6
143
|
/**
|
|
7
|
-
* Parse note content (or a full event, to pick up emoji tags) into blocks.
|
|
8
|
-
*
|
|
144
|
+
* Parse note content (or a full event, to pick up `emoji` tags) into ordered blocks. One
|
|
145
|
+
* left-to-right scan; at every position the rules fire in the alternation order above. Adjacent
|
|
146
|
+
* text merges and no empty text blocks are emitted.
|
|
9
147
|
*/
|
|
10
|
-
function
|
|
11
|
-
const emojis =
|
|
148
|
+
function parseContent(content, opts) {
|
|
149
|
+
const emojis = /* @__PURE__ */ new Map();
|
|
12
150
|
let text;
|
|
13
151
|
if (typeof content === "string") text = content;
|
|
14
152
|
else {
|
|
15
|
-
for (const tag of content.tags)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
153
|
+
for (const tag of content.tags) if (tag[0] === "emoji" && tag[1] !== void 0 && tag[1] !== "" && tag[2] !== void 0 && tag[2] !== "") emojis.set(tag[1], tag[2]);
|
|
154
|
+
text = content.content;
|
|
155
|
+
}
|
|
156
|
+
const scan = opts?.legacyBech32 === true ? SCAN_LEGACY : SCAN;
|
|
157
|
+
scan.lastIndex = 0;
|
|
158
|
+
const blocks = [];
|
|
159
|
+
let textStart = 0;
|
|
160
|
+
const flushText = (end) => {
|
|
161
|
+
if (end > textStart) blocks.push({
|
|
162
|
+
type: "text",
|
|
163
|
+
text: text.slice(textStart, end)
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
for (let m = scan.exec(text); m !== null; m = scan.exec(text)) {
|
|
167
|
+
const start = m.index;
|
|
168
|
+
const [token] = m;
|
|
169
|
+
const end = start + token.length;
|
|
170
|
+
const g = m.groups;
|
|
171
|
+
let block;
|
|
172
|
+
if (g?.["nostr"] !== void 0) block = referenceFor(token.slice(6), false);
|
|
173
|
+
else if (g?.["url"] !== void 0) block = urlBlock(token, false, opts?.imeta);
|
|
174
|
+
else if (g?.["relay"] !== void 0) block = urlBlock(token, true, void 0);
|
|
175
|
+
else if (g?.["invoice"] !== void 0) {
|
|
176
|
+
if (!isChar(start === 0 ? void 0 : text.codePointAt(start - 1), WORD_CHAR) && !isChar(text.codePointAt(end), WORD_CHAR)) block = {
|
|
177
|
+
type: "invoice",
|
|
178
|
+
bolt11: (token.toLowerCase().startsWith("lightning:") ? token.slice(10) : token).toLowerCase()
|
|
179
|
+
};
|
|
180
|
+
} else if (g?.["hashtag"] !== void 0) {
|
|
181
|
+
if (!isChar(start === 0 ? void 0 : text.codePointAt(start - 1), TAG_CHAR)) block = {
|
|
182
|
+
type: "hashtag",
|
|
183
|
+
value: token.slice(1)
|
|
184
|
+
};
|
|
185
|
+
} else if (g?.["emoji"] !== void 0) {
|
|
186
|
+
const url = emojis.get(token.slice(1, -1));
|
|
187
|
+
if (url !== void 0) block = {
|
|
19
188
|
type: "emoji",
|
|
20
|
-
shortcode,
|
|
189
|
+
shortcode: token.slice(1, -1),
|
|
21
190
|
url
|
|
22
|
-
}
|
|
191
|
+
};
|
|
192
|
+
} else if (g?.["bare"] !== void 0) {
|
|
193
|
+
if (!isChar(start === 0 ? void 0 : text.codePointAt(start - 1), WORD_CHAR) && !isChar(text.codePointAt(end), WORD_CHAR)) block = referenceFor(token, true);
|
|
23
194
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const max = text.length;
|
|
27
|
-
let prevIndex = 0;
|
|
28
|
-
let index = 0;
|
|
29
|
-
mainloop: while (index < max) {
|
|
30
|
-
const u = text.indexOf(":", index);
|
|
31
|
-
const h = text.indexOf("#", index);
|
|
32
|
-
if (u === -1 && h === -1) break;
|
|
33
|
-
if (u === -1 || h !== -1 && h < u) {
|
|
34
|
-
if (h === 0 || h > 0 && noCharacter.exec(text.at(h - 1) ?? "") !== null) {
|
|
35
|
-
const m = noCharacter.exec(text.slice(h + 1, h + MAX_HASHTAG_LENGTH));
|
|
36
|
-
const end = m ? h + 1 + (m.index ?? 0) : max;
|
|
37
|
-
if (prevIndex !== h) yield {
|
|
38
|
-
type: "text",
|
|
39
|
-
text: text.slice(prevIndex, h)
|
|
40
|
-
};
|
|
41
|
-
yield {
|
|
42
|
-
type: "hashtag",
|
|
43
|
-
value: text.slice(h + 1, end)
|
|
44
|
-
};
|
|
45
|
-
index = end;
|
|
46
|
-
prevIndex = index;
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
index = h + 1;
|
|
195
|
+
if (block === void 0) {
|
|
196
|
+
scan.lastIndex = start + 1;
|
|
50
197
|
continue;
|
|
51
198
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
try {
|
|
56
|
-
const { data, type } = decode(text.slice(u + 1, end));
|
|
57
|
-
let pointer;
|
|
58
|
-
switch (type) {
|
|
59
|
-
case "npub":
|
|
60
|
-
pointer = { pubkey: data };
|
|
61
|
-
break;
|
|
62
|
-
case "note":
|
|
63
|
-
pointer = { id: data };
|
|
64
|
-
break;
|
|
65
|
-
case "nprofile":
|
|
66
|
-
case "nevent":
|
|
67
|
-
case "naddr":
|
|
68
|
-
pointer = data;
|
|
69
|
-
break;
|
|
70
|
-
case "nsec":
|
|
71
|
-
index = end + 1;
|
|
72
|
-
continue mainloop;
|
|
73
|
-
}
|
|
74
|
-
if (prevIndex !== u - 5) yield {
|
|
75
|
-
type: "text",
|
|
76
|
-
text: text.slice(prevIndex, u - 5)
|
|
77
|
-
};
|
|
78
|
-
yield {
|
|
79
|
-
type: "reference",
|
|
80
|
-
pointer
|
|
81
|
-
};
|
|
82
|
-
index = end;
|
|
83
|
-
prevIndex = index;
|
|
84
|
-
continue;
|
|
85
|
-
} catch {
|
|
86
|
-
index = u + 1;
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
if (u >= 5 && text.slice(u - 5, u) === "https" || u >= 4 && text.slice(u - 4, u) === "http") {
|
|
91
|
-
const m = noURLCharacter.exec(text.slice(u + 4));
|
|
92
|
-
const end = m ? u + 4 + (m.index ?? 0) : max;
|
|
93
|
-
const prefixLen = text[u - 1] === "s" ? 5 : 4;
|
|
94
|
-
try {
|
|
95
|
-
const url = new URL(text.slice(u - prefixLen, end));
|
|
96
|
-
if (!url.hostname.includes(".")) throw new Error("invalid url");
|
|
97
|
-
if (prevIndex !== u - prefixLen) yield {
|
|
98
|
-
type: "text",
|
|
99
|
-
text: text.slice(prevIndex, u - prefixLen)
|
|
100
|
-
};
|
|
101
|
-
const path = url.pathname;
|
|
102
|
-
if (/\.(png|jpe?g|gif|webp|heic|svg)$/i.test(path)) yield {
|
|
103
|
-
type: "image",
|
|
104
|
-
url: url.toString()
|
|
105
|
-
};
|
|
106
|
-
else if (/\.(mp4|avi|webm|mkv|mov)$/i.test(path)) yield {
|
|
107
|
-
type: "video",
|
|
108
|
-
url: url.toString()
|
|
109
|
-
};
|
|
110
|
-
else if (/\.(mp3|aac|ogg|opus|wav|flac)$/i.test(path)) yield {
|
|
111
|
-
type: "audio",
|
|
112
|
-
url: url.toString()
|
|
113
|
-
};
|
|
114
|
-
else yield {
|
|
115
|
-
type: "url",
|
|
116
|
-
url: url.toString()
|
|
117
|
-
};
|
|
118
|
-
index = end;
|
|
119
|
-
prevIndex = index;
|
|
120
|
-
continue;
|
|
121
|
-
} catch {
|
|
122
|
-
index = end + 1;
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
if (u >= 3 && text.slice(u - 3, u) === "wss" || u >= 2 && text.slice(u - 2, u) === "ws") {
|
|
127
|
-
const m = noURLCharacter.exec(text.slice(u + 4));
|
|
128
|
-
const end = m ? u + 4 + (m.index ?? 0) : max;
|
|
129
|
-
const prefixLen = text[u - 1] === "s" ? 3 : 2;
|
|
130
|
-
try {
|
|
131
|
-
const url = new URL(text.slice(u - prefixLen, end));
|
|
132
|
-
if (!url.hostname.includes(".")) throw new Error("invalid ws url");
|
|
133
|
-
if (prevIndex !== u - prefixLen) yield {
|
|
134
|
-
type: "text",
|
|
135
|
-
text: text.slice(prevIndex, u - prefixLen)
|
|
136
|
-
};
|
|
137
|
-
yield {
|
|
138
|
-
type: "relay",
|
|
139
|
-
url: url.toString()
|
|
140
|
-
};
|
|
141
|
-
index = end;
|
|
142
|
-
prevIndex = index;
|
|
143
|
-
continue;
|
|
144
|
-
} catch {
|
|
145
|
-
index = end + 1;
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
for (const emoji of emojis) {
|
|
150
|
-
const endColon = u + emoji.shortcode.length + 1;
|
|
151
|
-
if (text[endColon] === ":" && text.slice(u + 1, endColon) === emoji.shortcode) {
|
|
152
|
-
if (prevIndex !== u) yield {
|
|
153
|
-
type: "text",
|
|
154
|
-
text: text.slice(prevIndex, u)
|
|
155
|
-
};
|
|
156
|
-
yield emoji;
|
|
157
|
-
index = endColon + 1;
|
|
158
|
-
prevIndex = index;
|
|
159
|
-
continue mainloop;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
index = u + 1;
|
|
199
|
+
flushText(start);
|
|
200
|
+
blocks.push(block);
|
|
201
|
+
textStart = block.type === "url" || block.type === "relay" || block.type === "image" || block.type === "video" || block.type === "audio" ? start + block.url.length : end;
|
|
163
202
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
text: text.slice(prevIndex)
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
/** Collect all content blocks into an array. */
|
|
170
|
-
function parseContentBlocks(content) {
|
|
171
|
-
return [...parseContent(content)];
|
|
203
|
+
flushText(text.length);
|
|
204
|
+
return blocks;
|
|
172
205
|
}
|
|
173
206
|
//#endregion
|
|
174
|
-
export { parseContent
|
|
207
|
+
export { parseContent };
|
|
175
208
|
|
|
176
209
|
//# sourceMappingURL=nip27.mjs.map
|
package/dist/nips/nip27.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nip27.mjs","names":[],"sources":["../../src/nips/nip27.ts"],"sourcesContent":["/**\n * NIP-27: Text Note References Tokenizes note content into text, nostr: references, URLs, hashtags,\n * media, relays, emoji.\n *\n * @see https://github.com/nostr-protocol/nips/blob/master/27.md\n */\nimport type { Event } from \"../core/event.ts\";\nimport { decode } from \"./nip19.ts\";\nimport type { AddressPointer, EventPointer, ProfilePointer } from \"./nip19.ts\";\n\nexport type ContentBlock =\n | { type: \"text\"; text: string }\n | { type: \"reference\"; pointer: ProfilePointer | AddressPointer | EventPointer }\n | { type: \"url\"; url: string }\n | { type: \"relay\"; url: string }\n | { type: \"image\"; url: string }\n | { type: \"video\"; url: string }\n | { type: \"audio\"; url: string }\n | { type: \"emoji\"; shortcode: string; url: string }\n | { type: \"hashtag\"; value: string };\n\nconst noCharacter = /\\W/m;\nconst noURLCharacter = /[^\\w/] |[^\\w/]$|$|,| /m;\nconst MAX_HASHTAG_LENGTH = 42;\n\n/**\n * Parse note content (or a full event, to pick up emoji tags) into blocks. Generator — consume with\n * `for…of` or `[...parseContent(s)]`.\n */\nexport function* parseContent(\n content: string | Pick<Event, \"content\" | \"tags\">,\n): Iterable<ContentBlock> {\n const emojis: Array<{ type: \"emoji\"; shortcode: string; url: string }> = [];\n let text: string;\n\n if (typeof content === \"string\") {\n text = content;\n } else {\n for (const tag of content.tags) {\n const shortcode = tag.at(1);\n const url = tag.at(2);\n if (\n tag[0] === \"emoji\" &&\n shortcode !== undefined &&\n shortcode !== \"\" &&\n url !== undefined &&\n url !== \"\"\n ) {\n emojis.push({ type: \"emoji\", shortcode, url });\n }\n }\n text = content.content;\n }\n\n const max = text.length;\n let prevIndex = 0;\n let index = 0;\n\n mainloop: while (index < max) {\n const u = text.indexOf(\":\", index);\n const h = text.indexOf(\"#\", index);\n if (u === -1 && h === -1) {\n break;\n }\n\n if (u === -1 || (h !== -1 && h < u)) {\n // hashtag\n if (h === 0 || (h > 0 && noCharacter.exec(text.at(h - 1) ?? \"\") !== null)) {\n const m = noCharacter.exec(text.slice(h + 1, h + MAX_HASHTAG_LENGTH));\n const end = m ? h + 1 + (m.index ?? 0) : max;\n if (prevIndex !== h) {\n yield { type: \"text\", text: text.slice(prevIndex, h) };\n }\n yield { type: \"hashtag\", value: text.slice(h + 1, end) };\n index = end;\n prevIndex = index;\n continue;\n }\n index = h + 1;\n continue;\n }\n\n // nostr: references\n if (u >= 5 && text.slice(u - 5, u) === \"nostr\") {\n const m = noCharacter.exec(text.slice(u + 60));\n const end = m ? u + 60 + (m.index ?? 0) : max;\n try {\n const { data, type } = decode(text.slice(u + 1, end));\n let pointer: ProfilePointer | AddressPointer | EventPointer | undefined;\n switch (type) {\n case \"npub\":\n pointer = { pubkey: data };\n break;\n case \"note\":\n pointer = { id: data };\n break;\n case \"nprofile\":\n case \"nevent\":\n case \"naddr\":\n pointer = data;\n break;\n case \"nsec\":\n index = end + 1;\n continue mainloop;\n }\n if (prevIndex !== u - 5) {\n yield { type: \"text\", text: text.slice(prevIndex, u - 5) };\n }\n yield { type: \"reference\", pointer };\n index = end;\n prevIndex = index;\n continue;\n } catch {\n index = u + 1;\n continue;\n }\n }\n\n // http(s) URLs\n if (\n (u >= 5 && text.slice(u - 5, u) === \"https\") ||\n (u >= 4 && text.slice(u - 4, u) === \"http\")\n ) {\n const m = noURLCharacter.exec(text.slice(u + 4));\n const end = m ? u + 4 + (m.index ?? 0) : max;\n const prefixLen = text[u - 1] === \"s\" ? 5 : 4;\n try {\n const url = new URL(text.slice(u - prefixLen, end));\n if (!url.hostname.includes(\".\")) {\n throw new Error(\"invalid url\");\n }\n if (prevIndex !== u - prefixLen) {\n yield { type: \"text\", text: text.slice(prevIndex, u - prefixLen) };\n }\n const path = url.pathname;\n if (/\\.(png|jpe?g|gif|webp|heic|svg)$/i.test(path)) {\n yield { type: \"image\", url: url.toString() };\n } else if (/\\.(mp4|avi|webm|mkv|mov)$/i.test(path)) {\n yield { type: \"video\", url: url.toString() };\n } else if (/\\.(mp3|aac|ogg|opus|wav|flac)$/i.test(path)) {\n yield { type: \"audio\", url: url.toString() };\n } else {\n yield { type: \"url\", url: url.toString() };\n }\n index = end;\n prevIndex = index;\n continue;\n } catch {\n index = end + 1;\n continue;\n }\n }\n\n // ws(s) relays\n if ((u >= 3 && text.slice(u - 3, u) === \"wss\") || (u >= 2 && text.slice(u - 2, u) === \"ws\")) {\n const m = noURLCharacter.exec(text.slice(u + 4));\n const end = m ? u + 4 + (m.index ?? 0) : max;\n const prefixLen = text[u - 1] === \"s\" ? 3 : 2;\n try {\n const url = new URL(text.slice(u - prefixLen, end));\n if (!url.hostname.includes(\".\")) {\n throw new Error(\"invalid ws url\");\n }\n if (prevIndex !== u - prefixLen) {\n yield { type: \"text\", text: text.slice(prevIndex, u - prefixLen) };\n }\n yield { type: \"relay\", url: url.toString() };\n index = end;\n prevIndex = index;\n continue;\n } catch {\n index = end + 1;\n continue;\n }\n }\n\n // custom emoji shortcodes :code:\n for (const emoji of emojis) {\n const endColon = u + emoji.shortcode.length + 1;\n if (text[endColon] === \":\" && text.slice(u + 1, endColon) === emoji.shortcode) {\n if (prevIndex !== u) {\n yield { type: \"text\", text: text.slice(prevIndex, u) };\n }\n yield emoji;\n index = endColon + 1;\n prevIndex = index;\n continue mainloop;\n }\n }\n\n index = u + 1;\n }\n\n if (prevIndex !== max) {\n yield { type: \"text\", text: text.slice(prevIndex) };\n }\n}\n\n/** Collect all content blocks into an array. */\nexport function parseContentBlocks(\n content: string | Pick<Event, \"content\" | \"tags\">,\n): ContentBlock[] {\n return [...parseContent(content)];\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"nip27.mjs","names":[],"sources":["../../src/nips/nip27.ts"],"sourcesContent":["/**\n * NIP-27: Text Note References. Tokenizes note content into text, `nostr:` references, URLs, media,\n * relays, hashtags, custom emoji, and Lightning invoices.\n *\n * @see https://github.com/nostr-protocol/nips/blob/master/27.md\n */\nimport type { Event } from \"../core/event.ts\";\nimport { decode } from \"./nip19.ts\";\nimport type { AddressPointer, EventPointer, ProfilePointer } from \"./nip19.ts\";\n\nexport type ContentBlock =\n | { type: \"text\"; text: string }\n | {\n type: \"reference\";\n pointer: ProfilePointer | EventPointer | AddressPointer;\n /**\n * `true` when the reference had no `nostr:` prefix (see\n * {@link ParseContentOptions.legacyBech32}).\n */\n bare: boolean;\n }\n | { type: \"url\"; url: string }\n | { type: \"image\" | \"video\" | \"audio\"; url: string }\n | { type: \"relay\"; url: string }\n | { type: \"hashtag\"; value: string }\n | { type: \"emoji\"; shortcode: string; url: string }\n | { type: \"invoice\"; bolt11: string };\n\nexport type ParseContentOptions = {\n /**\n * Recognize bare `npub`, `nprofile`, `note`, `nevent`, `naddr` without the `nostr:` prefix.\n * Default `false`.\n */\n legacyBech32?: boolean;\n /**\n * URL → MIME type (the `m` field of NIP-92 `imeta` tags); takes precedence over extension\n * classification.\n */\n imeta?: ReadonlyMap<string, string>;\n};\n\n// Bech32 data charset ([02-9ac-hj-np-z], case-insensitive) and the NIP-19 entity prefixes.\nconst BECH32_CHARS = \"02-9ac-hj-np-z\";\nconst NIP19_PREFIX = \"(?:npub|nprofile|note|nevent|naddr|nsec)1\";\nconst NIP19_BARE_PREFIX = \"(?:npub|nprofile|note|nevent|naddr)1\";\n\n// A URL run ends at whitespace, angle brackets, double quotes, CJK punctuation\n// (U+3000–U+303F), or full-width forms (U+FF00–U+FFEF).\nconst URL_RUN = `[^\\\\s<>\"\\\\u3000-\\\\u303f\\\\uff00-\\\\uffef]+`;\n\n// Rule order matters: at every position the first matching alternative wins.\nconst SCAN_BODY = [\n // 1. nostr: URIs carry a NIP-19 entity.\n `(?<nostr>nostr:${NIP19_PREFIX}[${BECH32_CHARS}]+)`,\n // 2./3. http(s) and ws(s) URLs share the same boundary rules.\n `(?<url>https?://${URL_RUN})`,\n `(?<relay>wss?://${URL_RUN})`,\n // 4. BOLT11 invoices, with an optional lightning: URI prefix.\n `(?<invoice>(?:lightning:)?(?:lnbc|lntbs|lntb|lnbcrt)1[${BECH32_CHARS}]+)`,\n // 5. Hashtags: Unicode letters/marks/numbers and underscore, at most 42 code points.\n `(?<hashtag>#[\\\\p{L}\\\\p{M}\\\\p{N}_]{1,42})`,\n // 6. Custom emoji shortcodes (validated against the event's emoji tags).\n `(?<emoji>:[A-Za-z0-9_]+:)`,\n].join(\"|\");\n\nconst SCAN = new RegExp(SCAN_BODY, \"giu\");\n// 7. Bare NIP-19 entities — legacy rendering support, opt-in only.\nconst SCAN_LEGACY = new RegExp(\n `${SCAN_BODY}|(?<bare>${NIP19_BARE_PREFIX}[${BECH32_CHARS}]+)`,\n \"giu\",\n);\n\n// The character before `#` must be absent or a non-tag character.\nconst TAG_CHAR = /[\\p{L}\\p{M}\\p{N}_]/u;\n// Invoice and bare-bech32 tokens must not be glued to a letter or digit.\nconst WORD_CHAR = /[\\p{L}\\p{N}]/u;\n\nconst TRAILING_URL_CHARS = new Set([\".\", \",\", \";\", \":\", \"!\", \"?\", \"'\"]);\n\nconst IMAGE_EXT = new Set([\"jpg\", \"jpeg\", \"png\", \"gif\", \"webp\", \"avif\", \"svg\"]);\nconst VIDEO_EXT = new Set([\"mp4\", \"webm\", \"mov\", \"m4v\", \"m3u8\"]);\nconst AUDIO_EXT = new Set([\"mp3\", \"m4a\", \"ogg\", \"oga\", \"opus\", \"wav\", \"flac\", \"aac\"]);\n\nfunction isChar(cp: number | undefined, re: RegExp): boolean {\n return cp !== undefined && re.test(String.fromCodePoint(cp));\n}\n\n/** Drop trailing sentence punctuation and closers that have no opener inside the URL. */\nfunction trimUrlTail(raw: string): string {\n let url = raw;\n for (;;) {\n const last = url.at(-1);\n if (last !== undefined && TRAILING_URL_CHARS.has(last)) {\n url = url.slice(0, -1);\n continue;\n }\n if (last === \")\" || last === \"]\") {\n const open = last === \")\" ? \"(\" : \"[\";\n let opens = 0;\n let closes = 0;\n for (const c of url) {\n if (c === open) {\n opens++;\n } else if (c === last) {\n closes++;\n }\n }\n if (closes > opens) {\n url = url.slice(0, -1);\n continue;\n }\n }\n return url;\n }\n}\n\ntype MediaType = \"image\" | \"video\" | \"audio\";\n\nfunction mimeMedia(mime: string | undefined): MediaType | undefined {\n if (mime === undefined) {\n return undefined;\n }\n const slash = mime.indexOf(\"/\");\n const base = slash === -1 ? mime : mime.slice(0, slash);\n return base === \"image\" || base === \"video\" || base === \"audio\" ? base : undefined;\n}\n\nfunction extMedia(pathname: string): MediaType | undefined {\n const dot = pathname.lastIndexOf(\".\");\n if (dot === -1) {\n return undefined;\n }\n const ext = pathname.slice(dot + 1).toLowerCase();\n if (IMAGE_EXT.has(ext)) {\n return \"image\";\n }\n if (VIDEO_EXT.has(ext)) {\n return \"video\";\n }\n return AUDIO_EXT.has(ext) ? \"audio\" : undefined;\n}\n\nfunction urlBlock(\n raw: string,\n relay: boolean,\n imeta: ReadonlyMap<string, string> | undefined,\n): ContentBlock | undefined {\n const url = trimUrlTail(raw);\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n return undefined;\n }\n if (!parsed.hostname.includes(\".\")) {\n return undefined;\n }\n if (relay) {\n return { type: \"relay\", url };\n }\n const media = mimeMedia(imeta?.get(url)) ?? extMedia(parsed.pathname);\n return media === undefined ? { type: \"url\", url } : { type: media, url };\n}\n\nfunction referenceFor(code: string, bare: boolean): ContentBlock | undefined {\n let pointer: ProfilePointer | EventPointer | AddressPointer;\n try {\n const decoded = decode(code);\n switch (decoded.type) {\n case \"npub\":\n pointer = { pubkey: decoded.data };\n break;\n case \"note\":\n pointer = { id: decoded.data };\n break;\n case \"nprofile\":\n case \"nevent\":\n case \"naddr\":\n pointer = decoded.data;\n break;\n case \"nsec\":\n return undefined;\n }\n } catch {\n return undefined;\n }\n return { type: \"reference\", pointer, bare };\n}\n\n/**\n * Parse note content (or a full event, to pick up `emoji` tags) into ordered blocks. One\n * left-to-right scan; at every position the rules fire in the alternation order above. Adjacent\n * text merges and no empty text blocks are emitted.\n */\nexport function parseContent(\n content: string | Pick<Event, \"content\" | \"tags\">,\n opts?: ParseContentOptions,\n): ContentBlock[] {\n const emojis = new Map<string, string>();\n let text: string;\n if (typeof content === \"string\") {\n text = content;\n } else {\n for (const tag of content.tags) {\n if (\n tag[0] === \"emoji\" &&\n tag[1] !== undefined &&\n tag[1] !== \"\" &&\n tag[2] !== undefined &&\n tag[2] !== \"\"\n ) {\n emojis.set(tag[1], tag[2]);\n }\n }\n text = content.content;\n }\n\n const scan = opts?.legacyBech32 === true ? SCAN_LEGACY : SCAN;\n scan.lastIndex = 0;\n\n const blocks: ContentBlock[] = [];\n let textStart = 0;\n const flushText = (end: number): void => {\n if (end > textStart) {\n blocks.push({ type: \"text\", text: text.slice(textStart, end) });\n }\n };\n\n for (let m = scan.exec(text); m !== null; m = scan.exec(text)) {\n const start = m.index;\n const [token] = m;\n const end = start + token.length;\n const g = m.groups;\n let block: ContentBlock | undefined;\n\n if (g?.[\"nostr\"] !== undefined) {\n block = referenceFor(token.slice(\"nostr:\".length), false);\n } else if (g?.[\"url\"] !== undefined) {\n block = urlBlock(token, false, opts?.imeta);\n } else if (g?.[\"relay\"] !== undefined) {\n block = urlBlock(token, true, undefined);\n } else if (g?.[\"invoice\"] !== undefined) {\n const prev = start === 0 ? undefined : text.codePointAt(start - 1);\n if (!isChar(prev, WORD_CHAR) && !isChar(text.codePointAt(end), WORD_CHAR)) {\n const invoice = token.toLowerCase().startsWith(\"lightning:\")\n ? token.slice(\"lightning:\".length)\n : token;\n block = { type: \"invoice\", bolt11: invoice.toLowerCase() };\n }\n } else if (g?.[\"hashtag\"] !== undefined) {\n const prev = start === 0 ? undefined : text.codePointAt(start - 1);\n if (!isChar(prev, TAG_CHAR)) {\n block = { type: \"hashtag\", value: token.slice(1) };\n }\n } else if (g?.[\"emoji\"] !== undefined) {\n const url = emojis.get(token.slice(1, -1));\n if (url !== undefined) {\n block = { type: \"emoji\", shortcode: token.slice(1, -1), url };\n }\n } else if (g?.[\"bare\"] !== undefined) {\n const prev = start === 0 ? undefined : text.codePointAt(start - 1);\n if (!isChar(prev, WORD_CHAR) && !isChar(text.codePointAt(end), WORD_CHAR)) {\n block = referenceFor(token, true);\n }\n }\n\n if (block === undefined) {\n // The candidate is plain text; resume scanning one code unit later so any token inside\n // its span can still match.\n scan.lastIndex = start + 1;\n continue;\n }\n flushText(start);\n blocks.push(block);\n // URL-family blocks may end before the regex run once trailing punctuation is trimmed.\n textStart =\n block.type === \"url\" ||\n block.type === \"relay\" ||\n block.type === \"image\" ||\n block.type === \"video\" ||\n block.type === \"audio\"\n ? start + block.url.length\n : end;\n }\n\n flushText(text.length);\n return blocks;\n}\n"],"mappings":"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"}
|