mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-19 06:54:23 +01:00
refactor: change parser to parse raw text content
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { For } from 'solid-js';
|
||||
import parseTextNote, { type ParsedTextNoteNode } from '@/core/parseTextNote';
|
||||
import parseTextNote, { resolveTagReference, type ParsedTextNoteNode } from '@/core/parseTextNote';
|
||||
import type { Event as NostrEvent } from 'nostr-tools';
|
||||
import PlainTextDisplay from '@/components/textNote/PlainTextDisplay';
|
||||
import MentionedUserDisplay from '@/components/textNote/MentionedUserDisplay';
|
||||
@@ -21,19 +21,23 @@ const TextNoteContentDisplay = (props: TextNoteContentDisplayProps) => {
|
||||
const { config } = useConfig();
|
||||
const event = () => eventWrapper(props.event);
|
||||
return (
|
||||
<For each={parseTextNote(props.event)}>
|
||||
<For each={parseTextNote(props.event.content)}>
|
||||
{(item: ParsedTextNoteNode) => {
|
||||
if (item.type === 'PlainText') {
|
||||
return <PlainTextDisplay plainText={item} />;
|
||||
}
|
||||
if (item.type === 'MentionedUser') {
|
||||
return <MentionedUserDisplay pubkey={item.pubkey} />;
|
||||
}
|
||||
if (item.type === 'MentionedEvent') {
|
||||
if (props.embedding) {
|
||||
return <MentionedEventDisplay mentionedEvent={item} />;
|
||||
if (item.type === 'TagReference') {
|
||||
const resolved = resolveTagReference(item, props.event);
|
||||
if (resolved == null) return null;
|
||||
if (resolved.type === 'MentionedUser') {
|
||||
return <MentionedUserDisplay pubkey={resolved.pubkey} />;
|
||||
}
|
||||
if (resolved.type === 'MentionedEvent') {
|
||||
if (props.embedding) {
|
||||
return <MentionedEventDisplay mentionedEvent={resolved} />;
|
||||
}
|
||||
return <EventLink eventId={resolved.eventId} />;
|
||||
}
|
||||
return <EventLink eventId={item.eventId} />;
|
||||
}
|
||||
if (item.type === 'Bech32Entity') {
|
||||
if (item.data.type === 'note' && props.embedding) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import assert from 'assert';
|
||||
import { describe, it } from 'vitest';
|
||||
import { type Event as NostrEvent } from 'nostr-tools';
|
||||
|
||||
import parseTextNote, { type ParsedTextNoteNode } from './parseTextNote';
|
||||
import parseTextNote, { resolveTagReference, type ParsedTextNoteNode, TagReference } from './parseTextNote';
|
||||
|
||||
describe('parseTextNote', () => {
|
||||
/*
|
||||
@@ -21,15 +22,7 @@ describe('parseTextNote', () => {
|
||||
*/
|
||||
|
||||
it('should parse text note with the url with hash', () => {
|
||||
const parsed = parseTextNote({
|
||||
id: '',
|
||||
sig: '',
|
||||
kind: 1,
|
||||
content: 'this is url\nhttps://github.com/syusui-s/rabbit/#readme #rabbit',
|
||||
tags: [],
|
||||
created_at: 1678377182,
|
||||
pubkey: '9366708117c4a7edf9178acdce538c95059b9eb3394808cdd90564094172d972',
|
||||
});
|
||||
const parsed = parseTextNote('this is url\nhttps://github.com/syusui-s/rabbit/#readme #rabbit');
|
||||
|
||||
const expected: ParsedTextNoteNode[] = [
|
||||
{ type: 'PlainText', content: 'this is url\n' },
|
||||
@@ -42,15 +35,7 @@ describe('parseTextNote', () => {
|
||||
});
|
||||
|
||||
it('should parse text note with the url with hash and hashtag', () => {
|
||||
const parsed = parseTextNote({
|
||||
id: '',
|
||||
sig: '',
|
||||
kind: 1,
|
||||
content: 'this is url\nhttps://github.com/syusui-s/rabbit/#readme #rabbit',
|
||||
tags: [],
|
||||
created_at: 1678377182,
|
||||
pubkey: '9366708117c4a7edf9178acdce538c95059b9eb3394808cdd90564094172d972',
|
||||
});
|
||||
const parsed = parseTextNote('this is url\nhttps://github.com/syusui-s/rabbit/#readme #rabbit');
|
||||
|
||||
const expected: ParsedTextNoteNode[] = [
|
||||
{ type: 'PlainText', content: 'this is url\n' },
|
||||
@@ -63,15 +48,7 @@ describe('parseTextNote', () => {
|
||||
});
|
||||
|
||||
it('should parse text note which includes punycode URL', () => {
|
||||
const parsed = parseTextNote({
|
||||
id: '',
|
||||
sig: '',
|
||||
kind: 1,
|
||||
content: 'This is Japanese domain: https://xn--p8j9a0d9c9a.xn--q9jyb4c/',
|
||||
tags: [],
|
||||
created_at: 1678377182,
|
||||
pubkey: '9366708117c4a7edf9178acdce538c95059b9eb3394808cdd90564094172d972',
|
||||
});
|
||||
const parsed = parseTextNote('This is Japanese domain: https://xn--p8j9a0d9c9a.xn--q9jyb4c/');
|
||||
|
||||
const expected: ParsedTextNoteNode[] = [
|
||||
{ type: 'PlainText', content: 'This is Japanese domain: ' },
|
||||
@@ -82,16 +59,7 @@ describe('parseTextNote', () => {
|
||||
});
|
||||
|
||||
it('should parse text note which includes image URLs', () => {
|
||||
const parsed = parseTextNote({
|
||||
id: '',
|
||||
sig: '',
|
||||
kind: 1,
|
||||
content:
|
||||
'https://i.gyazo.com/8f177b9953fdb9513ad00d0743d9c608.png\nhttps://i.gyazo.com/346ad7260f6a999720c2d13317ff795f.jpg',
|
||||
tags: [],
|
||||
created_at: 1678377182,
|
||||
pubkey: '9366708117c4a7edf9178acdce538c95059b9eb3394808cdd90564094172d972',
|
||||
});
|
||||
const parsed = parseTextNote('https://i.gyazo.com/8f177b9953fdb9513ad00d0743d9c608.png\nhttps://i.gyazo.com/346ad7260f6a999720c2d13317ff795f.jpg');
|
||||
|
||||
const expected: ParsedTextNoteNode[] = [
|
||||
{ type: 'URL', content: 'https://i.gyazo.com/8f177b9953fdb9513ad00d0743d9c608.png' },
|
||||
@@ -103,15 +71,7 @@ describe('parseTextNote', () => {
|
||||
});
|
||||
|
||||
it('should parse text note which includes URL with + symbol', () => {
|
||||
const parsed = parseTextNote({
|
||||
id: '',
|
||||
sig: '',
|
||||
kind: 1,
|
||||
content: 'this is my page\nhttps://example.com/abc+def?q=ghi+jkl#lmn+opq',
|
||||
tags: [],
|
||||
created_at: 1678377182,
|
||||
pubkey: '9366708117c4a7edf9178acdce538c95059b9eb3394808cdd90564094172d972',
|
||||
});
|
||||
const parsed = parseTextNote('this is my page\nhttps://example.com/abc+def?q=ghi+jkl#lmn+opq');
|
||||
|
||||
const expected: ParsedTextNoteNode[] = [
|
||||
{ type: 'PlainText', content: 'this is my page\n' },
|
||||
@@ -123,15 +83,7 @@ describe('parseTextNote', () => {
|
||||
|
||||
//
|
||||
it('should parse text note which includes URL with + symbol', () => {
|
||||
const parsed = parseTextNote({
|
||||
id: '',
|
||||
sig: '',
|
||||
kind: 1,
|
||||
content: 'I wrote this page\nhttps://example.com/test(test)?q=(q)#(h)',
|
||||
tags: [],
|
||||
created_at: 1678377182,
|
||||
pubkey: '9366708117c4a7edf9178acdce538c95059b9eb3394808cdd90564094172d972',
|
||||
});
|
||||
const parsed = parseTextNote('I wrote this page\nhttps://example.com/test(test)?q=(q)#(h)');
|
||||
|
||||
const expected: ParsedTextNoteNode[] = [
|
||||
{ type: 'PlainText', content: 'I wrote this page\n' },
|
||||
@@ -145,15 +97,7 @@ describe('parseTextNote', () => {
|
||||
});
|
||||
|
||||
it('should parse text note which includes wss URL', () => {
|
||||
const parsed = parseTextNote({
|
||||
id: '',
|
||||
sig: '',
|
||||
kind: 1,
|
||||
content: 'this is my using relays: wss://relay.damus.io, wss://relay.snort.social',
|
||||
tags: [],
|
||||
created_at: 1678377182,
|
||||
pubkey: '9366708117c4a7edf9178acdce538c95059b9eb3394808cdd90564094172d972',
|
||||
});
|
||||
const parsed = parseTextNote('this is my using relays: wss://relay.damus.io, wss://relay.snort.social');
|
||||
|
||||
const expected: ParsedTextNoteNode[] = [
|
||||
{ type: 'PlainText', content: 'this is my using relays: ' },
|
||||
@@ -166,50 +110,20 @@ describe('parseTextNote', () => {
|
||||
});
|
||||
|
||||
it('should parse text note with pubkey mentions', () => {
|
||||
const parsed = parseTextNote({
|
||||
id: '',
|
||||
sig: '',
|
||||
kind: 1,
|
||||
content: 'this is pubkey\n#[0] #[1]',
|
||||
tags: [
|
||||
['p', '9366708117c4a7edf9178acdce538c95059b9eb3394808cdd90564094172d972'],
|
||||
['p', '80d3a41d8a00679c0105faac2cdf7643c9ba26835cff096bf7f9c7a0eee8c8fc'],
|
||||
],
|
||||
created_at: 1678377182,
|
||||
pubkey: '9366708117c4a7edf9178acdce538c95059b9eb3394808cdd90564094172d972',
|
||||
});
|
||||
const parsed = parseTextNote('this is pubkey\n#[0] #[1]');
|
||||
|
||||
const expected: ParsedTextNoteNode[] = [
|
||||
{ type: 'PlainText', content: 'this is pubkey\n' },
|
||||
{
|
||||
type: 'MentionedUser',
|
||||
tagIndex: 0,
|
||||
content: '#[0]',
|
||||
pubkey: '9366708117c4a7edf9178acdce538c95059b9eb3394808cdd90564094172d972',
|
||||
},
|
||||
{ type: 'TagReference', tagIndex: 0, content: '#[0]'},
|
||||
{ type: 'PlainText', content: ' ' },
|
||||
{
|
||||
type: 'MentionedUser',
|
||||
tagIndex: 1,
|
||||
content: '#[1]',
|
||||
pubkey: '80d3a41d8a00679c0105faac2cdf7643c9ba26835cff096bf7f9c7a0eee8c8fc',
|
||||
},
|
||||
{ type: 'TagReference', tagIndex: 1, content: '#[1]'},
|
||||
];
|
||||
|
||||
assert.deepStrictEqual(parsed, expected);
|
||||
});
|
||||
|
||||
it('should parse text note which includes npub string', () => {
|
||||
const parsed = parseTextNote({
|
||||
id: '',
|
||||
sig: '',
|
||||
kind: 1,
|
||||
content:
|
||||
'this is pubkey\nnpub1srf6g8v2qpnecqg9l2kzehmkg0ym5f5rtnlsj6lhl8r6pmhger7q5mtt3q\nhello',
|
||||
tags: [],
|
||||
created_at: 1678377182,
|
||||
pubkey: '9366708117c4a7edf9178acdce538c95059b9eb3394808cdd90564094172d972',
|
||||
});
|
||||
const parsed = parseTextNote('this is pubkey\nnpub1srf6g8v2qpnecqg9l2kzehmkg0ym5f5rtnlsj6lhl8r6pmhger7q5mtt3q\nhello');
|
||||
|
||||
const expected: ParsedTextNoteNode[] = [
|
||||
{ type: 'PlainText', content: 'this is pubkey\n' },
|
||||
@@ -227,3 +141,63 @@ describe('parseTextNote', () => {
|
||||
assert.deepStrictEqual(parsed, expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveTagReference', () => {
|
||||
it('should resolve a tag reference refers a user', () => {
|
||||
const tagReference: TagReference = {
|
||||
type: 'TagReference',
|
||||
tagIndex: 1,
|
||||
content: '#[1]',
|
||||
};
|
||||
const dummyEvent: NostrEvent = {
|
||||
id: '',
|
||||
sig: '',
|
||||
kind: 1,
|
||||
content: '#[1]',
|
||||
tags: [
|
||||
['p', '9366708117c4a7edf9178acdce538c95059b9eb3394808cdd90564094172d972'],
|
||||
['p', '80d3a41d8a00679c0105faac2cdf7643c9ba26835cff096bf7f9c7a0eee8c8fc'],
|
||||
],
|
||||
created_at: 1678377182,
|
||||
pubkey: '9366708117c4a7edf9178acdce538c95059b9eb3394808cdd90564094172d972',
|
||||
};
|
||||
const result = resolveTagReference(tagReference, dummyEvent);
|
||||
const expected = {
|
||||
type: 'MentionedUser',
|
||||
tagIndex: 1,
|
||||
content: '#[1]',
|
||||
pubkey: '80d3a41d8a00679c0105faac2cdf7643c9ba26835cff096bf7f9c7a0eee8c8fc',
|
||||
};
|
||||
|
||||
assert.deepStrictEqual(result, expected);
|
||||
});
|
||||
|
||||
it('should resolve a tag reference refers an other text note', () => {
|
||||
const tagReference: TagReference = {
|
||||
type: 'TagReference',
|
||||
tagIndex: 1,
|
||||
content: '#[1]',
|
||||
};
|
||||
const dummyEvent: NostrEvent = {
|
||||
id: '',
|
||||
sig: '',
|
||||
kind: 1,
|
||||
content: '',
|
||||
tags: [
|
||||
['p', '80d3a41d8a00679c0105faac2cdf7643c9ba26835cff096bf7f9c7a0eee8c8fc'],
|
||||
['e', 'b9cefcb857fa487d5794156e85b30a7f98cb21721040631210262091d86ff6f212', '', 'reply'],
|
||||
],
|
||||
created_at: 1678377182,
|
||||
pubkey: '9366708117c4a7edf9178acdce538c95059b9eb3394808cdd90564094172d972',
|
||||
};
|
||||
const result = resolveTagReference(tagReference, dummyEvent);
|
||||
const expected = {
|
||||
type: 'MentionedEvent',
|
||||
tagIndex: 1,
|
||||
marker: 'reply',
|
||||
content: '#[1]',
|
||||
eventId: 'b9cefcb857fa487d5794156e85b30a7f98cb21721040631210262091d86ff6f212',
|
||||
};
|
||||
assert.deepStrictEqual(result, expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,19 +11,10 @@ export type PlainText = {
|
||||
content: string;
|
||||
};
|
||||
|
||||
export type MentionedEvent = {
|
||||
type: 'MentionedEvent';
|
||||
content: string;
|
||||
export type TagReference = {
|
||||
type: 'TagReference';
|
||||
tagIndex: number;
|
||||
eventId: string;
|
||||
marker: 'reply' | 'root' | 'mention' | undefined;
|
||||
};
|
||||
|
||||
export type MentionedUser = {
|
||||
type: 'MentionedUser';
|
||||
content: string;
|
||||
tagIndex: number;
|
||||
pubkey: string;
|
||||
};
|
||||
|
||||
export type Bech32Entity = {
|
||||
@@ -46,16 +37,25 @@ export type UrlText = {
|
||||
content: string;
|
||||
};
|
||||
|
||||
export type ParsedTextNoteNode =
|
||||
| PlainText
|
||||
| MentionedEvent
|
||||
| MentionedUser
|
||||
| Bech32Entity
|
||||
| HashTag
|
||||
| UrlText;
|
||||
export type ParsedTextNoteNode = PlainText | TagReference | Bech32Entity | HashTag | UrlText;
|
||||
|
||||
export type ParsedTextNote = ParsedTextNoteNode[];
|
||||
|
||||
export type MentionedEvent = {
|
||||
type: 'MentionedEvent';
|
||||
content: string;
|
||||
tagIndex: number;
|
||||
eventId: string;
|
||||
marker: 'reply' | 'root' | 'mention' | undefined;
|
||||
};
|
||||
|
||||
export type MentionedUser = {
|
||||
type: 'MentionedUser';
|
||||
content: string;
|
||||
tagIndex: number;
|
||||
pubkey: string;
|
||||
};
|
||||
|
||||
const tagRefRegex = /(?:#\[(?<idx>\d+)\])/g;
|
||||
const hashTagRegex = /#(?<hashtag>[^[-^`:-@!-/{-~\d\s][^[-^`:-@!-/{-~\s]+)/g;
|
||||
// raw NIP-19 codes, NIP-21 links (NIP-27)
|
||||
@@ -64,19 +64,19 @@ const mentionRegex = /(?:nostr:)?(?<mention>(npub|note|nprofile|nevent)1[ac-hj-n
|
||||
const urlRegex =
|
||||
/(?<url>(?:https?|wss?):\/\/[-a-zA-Z0-9.:]+(?:\/[-[\]~!$&'()*+.,:;@&=%\w]+|\/)*(?:\?[-[\]~!$&'()*+.,/:;%@&=\w?]+)?(?:#[-[\]~!$&'()*+.,/:;%@\w&=?#]+)?)/g;
|
||||
|
||||
const parseTextNote = (event: NostrEvent): ParsedTextNote => {
|
||||
const parseTextNote = (textNoteContent: string) => {
|
||||
const matches = [
|
||||
...event.content.matchAll(tagRefRegex),
|
||||
...event.content.matchAll(hashTagRegex),
|
||||
...event.content.matchAll(mentionRegex),
|
||||
...event.content.matchAll(urlRegex),
|
||||
...textNoteContent.matchAll(tagRefRegex),
|
||||
...textNoteContent.matchAll(hashTagRegex),
|
||||
...textNoteContent.matchAll(mentionRegex),
|
||||
...textNoteContent.matchAll(urlRegex),
|
||||
].sort((a, b) => (a.index as number) - (b.index as number));
|
||||
let pos = 0;
|
||||
const result: ParsedTextNote = [];
|
||||
|
||||
const pushPlainText = (index: number | undefined) => {
|
||||
if (index != null && pos !== index) {
|
||||
const content = event.content.slice(pos, index);
|
||||
const content = textNoteContent.slice(pos, index);
|
||||
const plainText: PlainText = { type: 'PlainText', content };
|
||||
result.push(plainText);
|
||||
}
|
||||
@@ -94,34 +94,13 @@ const parseTextNote = (event: NostrEvent): ParsedTextNote => {
|
||||
result.push(url);
|
||||
} else if (match.groups?.idx) {
|
||||
const tagIndex = parseInt(match.groups.idx, 10);
|
||||
const tag = event.tags[tagIndex];
|
||||
if (tag == null) return;
|
||||
|
||||
pushPlainText(index);
|
||||
|
||||
const tagName = tag[0];
|
||||
if (tagName === 'p') {
|
||||
const mentionedUser: MentionedUser = {
|
||||
type: 'MentionedUser',
|
||||
tagIndex,
|
||||
content: match[0],
|
||||
pubkey: tag[1],
|
||||
};
|
||||
result.push(mentionedUser);
|
||||
} else if (tagName === 'e') {
|
||||
const mention = eventWrapper(event)
|
||||
.taggedEvents()
|
||||
.find((ev) => ev.index === tagIndex);
|
||||
|
||||
const mentionedEvent: MentionedEvent = {
|
||||
type: 'MentionedEvent',
|
||||
tagIndex,
|
||||
content: match[0],
|
||||
eventId: tag[1],
|
||||
marker: mention?.marker,
|
||||
};
|
||||
result.push(mentionedEvent);
|
||||
}
|
||||
result.push({
|
||||
type: 'TagReference',
|
||||
tagIndex,
|
||||
content: match[0],
|
||||
});
|
||||
} else if (match.groups?.mention) {
|
||||
pushPlainText(index);
|
||||
try {
|
||||
@@ -133,7 +112,7 @@ const parseTextNote = (event: NostrEvent): ParsedTextNote => {
|
||||
};
|
||||
result.push(bech32Entity);
|
||||
} catch (e) {
|
||||
console.error(`failed to parse Bech32 entity (NIP-19) but ignore this: ${match[0]}`);
|
||||
console.warn(`failed to parse Bech32 entity (NIP-19): ${match[0]}`);
|
||||
pushPlainText(index + match[0].length);
|
||||
return;
|
||||
}
|
||||
@@ -150,8 +129,8 @@ const parseTextNote = (event: NostrEvent): ParsedTextNote => {
|
||||
pos = index + match[0].length;
|
||||
});
|
||||
|
||||
if (pos !== event.content.length) {
|
||||
const content = event.content.slice(pos);
|
||||
if (pos !== textNoteContent.length) {
|
||||
const content = textNoteContent.slice(pos);
|
||||
const plainText: PlainText = { type: 'PlainText', content };
|
||||
result.push(plainText);
|
||||
}
|
||||
@@ -159,4 +138,39 @@ const parseTextNote = (event: NostrEvent): ParsedTextNote => {
|
||||
return result;
|
||||
};
|
||||
|
||||
export const resolveTagReference = (
|
||||
{ tagIndex, content }: TagReference,
|
||||
event: NostrEvent,
|
||||
): MentionedUser | MentionedEvent | null => {
|
||||
const tag = event.tags[tagIndex];
|
||||
if (tag == null) return null;
|
||||
|
||||
const tagName = tag[0];
|
||||
|
||||
if (tagName === 'p') {
|
||||
return {
|
||||
type: 'MentionedUser',
|
||||
tagIndex,
|
||||
content,
|
||||
pubkey: tag[1],
|
||||
} satisfies MentionedUser;
|
||||
}
|
||||
|
||||
if (tagName === 'e') {
|
||||
const mention = eventWrapper(event)
|
||||
.taggedEvents()
|
||||
.find((ev) => ev.index === tagIndex);
|
||||
|
||||
return {
|
||||
type: 'MentionedEvent',
|
||||
tagIndex,
|
||||
content,
|
||||
eventId: tag[1],
|
||||
marker: mention?.marker,
|
||||
} satisfies MentionedEvent;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default parseTextNote;
|
||||
|
||||
Reference in New Issue
Block a user