refactor: change parser to parse raw text content

This commit is contained in:
Shusui MOYATANI
2023-04-06 19:19:09 +09:00
parent 8e1714f476
commit eda4382524
3 changed files with 153 additions and 161 deletions

View File

@@ -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 === 'TagReference') {
const resolved = resolveTagReference(item, props.event);
if (resolved == null) return null;
if (resolved.type === 'MentionedUser') {
return <MentionedUserDisplay pubkey={resolved.pubkey} />;
}
if (item.type === 'MentionedEvent') {
if (resolved.type === 'MentionedEvent') {
if (props.embedding) {
return <MentionedEventDisplay mentionedEvent={item} />;
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) {

View File

@@ -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);
});
});

View File

@@ -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',
result.push({
type: 'TagReference',
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);
}
});
} 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;