bug: parse legacy tag refs

This commit is contained in:
2023-11-07 12:29:30 +00:00
parent e248889170
commit 758107fd50
3 changed files with 50 additions and 22 deletions

View File

@ -1,13 +1,12 @@
import "./Markdown.css"; import "./Markdown.css";
import { ReactNode, forwardRef, useMemo } from "react"; import { ReactNode, forwardRef, useMemo } from "react";
import { parseNostrLink, transformText } from "@snort/system"; import { transformText } from "@snort/system";
import { marked, Token } from "marked"; import { marked, Token } from "marked";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import markedFootnote, { Footnotes, Footnote, FootnoteRef } from "marked-footnote"; import markedFootnote, { Footnotes, Footnote, FootnoteRef } from "marked-footnote";
import { ProxyImg } from "Element/ProxyImg"; import { ProxyImg } from "Element/ProxyImg";
import Mention from "Element/Embed/Mention";
import NostrLink from "Element/Embed/NostrLink"; import NostrLink from "Element/Embed/NostrLink";
interface MarkdownProps { interface MarkdownProps {
@ -15,11 +14,11 @@ interface MarkdownProps {
tags?: Array<Array<string>>; tags?: Array<Array<string>>;
} }
function renderToken(t: Token | Footnotes | Footnote | FootnoteRef): ReactNode { function renderToken(t: Token | Footnotes | Footnote | FootnoteRef, tags: Array<Array<string>>): ReactNode {
try { try {
switch (t.type) { switch (t.type) {
case "paragraph": { case "paragraph": {
return <p>{t.tokens ? t.tokens.map(renderToken) : t.raw}</p>; return <p>{t.tokens ? t.tokens.map(a => renderToken(a, tags)) : t.raw}</p>;
} }
case "image": { case "image": {
return <ProxyImg src={t.href} />; return <ProxyImg src={t.href} />;
@ -27,17 +26,17 @@ function renderToken(t: Token | Footnotes | Footnote | FootnoteRef): ReactNode {
case "heading": { case "heading": {
switch (t.depth) { switch (t.depth) {
case 1: case 1:
return <h1>{t.tokens ? t.tokens.map(renderToken) : t.raw}</h1>; return <h1>{t.tokens ? t.tokens.map(a => renderToken(a, tags)) : t.raw}</h1>;
case 2: case 2:
return <h2>{t.tokens ? t.tokens.map(renderToken) : t.raw}</h2>; return <h2>{t.tokens ? t.tokens.map(a => renderToken(a, tags)) : t.raw}</h2>;
case 3: case 3:
return <h3>{t.tokens ? t.tokens.map(renderToken) : t.raw}</h3>; return <h3>{t.tokens ? t.tokens.map(a => renderToken(a, tags)) : t.raw}</h3>;
case 4: case 4:
return <h4>{t.tokens ? t.tokens.map(renderToken) : t.raw}</h4>; return <h4>{t.tokens ? t.tokens.map(a => renderToken(a, tags)) : t.raw}</h4>;
case 5: case 5:
return <h5>{t.tokens ? t.tokens.map(renderToken) : t.raw}</h5>; return <h5>{t.tokens ? t.tokens.map(a => renderToken(a, tags)) : t.raw}</h5>;
case 6: case 6:
return <h6>{t.tokens ? t.tokens.map(renderToken) : t.raw}</h6>; return <h6>{t.tokens ? t.tokens.map(a => renderToken(a, tags)) : t.raw}</h6>;
} }
throw new Error("Invalid heading"); throw new Error("Invalid heading");
} }
@ -54,30 +53,30 @@ function renderToken(t: Token | Footnotes | Footnote | FootnoteRef): ReactNode {
return <hr />; return <hr />;
} }
case "blockquote": { case "blockquote": {
return <blockquote>{t.tokens ? t.tokens.map(renderToken) : t.raw}</blockquote>; return <blockquote>{t.tokens ? t.tokens.map(a => renderToken(a, tags)) : t.raw}</blockquote>;
} }
case "link": { case "link": {
return ( return (
<Link to={t.href as string} className="ext" target="_blank"> <Link to={t.href as string} className="ext" target="_blank">
{t.tokens ? t.tokens.map(renderToken) : t.raw} {t.tokens ? t.tokens.map(a => renderToken(a, tags)) : t.raw}
</Link> </Link>
); );
} }
case "list": { case "list": {
if (t.ordered) { if (t.ordered) {
return <ol>{t.items.map(renderToken)}</ol>; return <ol>{(t.items as Token[]).map(a => renderToken(a, tags))}</ol>;
} else { } else {
return <ul>{t.items.map(renderToken)}</ul>; return <ul>{(t.items as Token[]).map(a => renderToken(a, tags))}</ul>;
} }
} }
case "list_item": { case "list_item": {
return <li>{t.tokens ? t.tokens.map(renderToken) : t.raw}</li>; return <li>{t.tokens ? t.tokens.map(a => renderToken(a, tags)) : t.raw}</li>;
} }
case "em": { case "em": {
return <em>{t.tokens ? t.tokens.map(renderToken) : t.raw}</em>; return <em>{t.tokens ? t.tokens.map(a => renderToken(a, tags)) : t.raw}</em>;
} }
case "del": { case "del": {
return <s>{t.tokens ? t.tokens.map(renderToken) : t.raw}</s>; return <s>{t.tokens ? t.tokens.map(a => renderToken(a, tags)) : t.raw}</s>;
} }
case "footnoteRef": { case "footnoteRef": {
return ( return (
@ -94,9 +93,9 @@ function renderToken(t: Token | Footnotes | Footnote | FootnoteRef): ReactNode {
} }
default: { default: {
if ("tokens" in t) { if ("tokens" in t) {
return (t.tokens as Array<Token>).map(renderToken); return (t.tokens as Array<Token>).map(a => renderToken(a, tags));
} }
return transformText(t.raw, []).map(v => { return transformText(t.raw, tags).map(v => {
switch (v.type) { switch (v.type) {
case "link": { case "link": {
if (v.content.startsWith("nostr:")) { if (v.content.startsWith("nostr:")) {
@ -106,7 +105,7 @@ function renderToken(t: Token | Footnotes | Footnote | FootnoteRef): ReactNode {
} }
} }
case "mention": { case "mention": {
return <Mention link={parseNostrLink(v.content)} />; return <NostrLink link={v.content} />;
} }
default: { default: {
return v.content; return v.content;
@ -127,7 +126,7 @@ export const Markdown = forwardRef<HTMLDivElement, MarkdownProps>((props: Markdo
return ( return (
<div className="markdown" ref={ref}> <div className="markdown" ref={ref}>
{parsed.filter(a => a.type !== "footnote" && a.type !== "footnotes").map(a => renderToken(a))} {parsed.filter(a => a.type !== "footnote" && a.type !== "footnotes").map(a => renderToken(a, props.tags ?? []))}
</div> </div>
); );
}); });

View File

@ -9,6 +9,11 @@ export const DefaultConnectTimeout = 2000;
// eslint-disable-next-line no-useless-escape // eslint-disable-next-line no-useless-escape
export const HashtagRegex = /(#[^\s!@#$%^&*()=+.\/,\[{\]};:'"?><]+)/g; export const HashtagRegex = /(#[^\s!@#$%^&*()=+.\/,\[{\]};:'"?><]+)/g;
/**
* Legacy tag reference regex
*/
export const TagRefRegex = /(#\[\d+\])/gm;
/** /**
* How long profile cache should be considered valid for * How long profile cache should be considered valid for
*/ */

View File

@ -7,8 +7,9 @@ import {
InvoiceRegex, InvoiceRegex,
MarkdownCodeRegex, MarkdownCodeRegex,
MentionNostrEntityRegex, MentionNostrEntityRegex,
TagRefRegex,
} from "./const"; } from "./const";
import { validateNostrLink } from "./nostr-link"; import { NostrLink, validateNostrLink } from "./nostr-link";
import { splitByUrl } from "./utils"; import { splitByUrl } from "./utils";
export interface ParsedFragment { export interface ParsedFragment {
@ -176,6 +177,28 @@ function extractHashtags(fragments: Fragment[]) {
.flat(); .flat();
} }
function extractTagRefs(fragments: Fragment[], tags: Array<Array<string>>) {
return fragments
.map(f => {
if (typeof f === "string") {
return f.split(TagRefRegex).map(i => {
if (i.startsWith("#")) {
const tag = tags[Number(i.slice(2, -1))];
if (tag) {
return {
type: "mention",
content: `nostr:${NostrLink.fromTag(tag).encode()}`,
} as ParsedFragment;
}
}
return i;
});
}
return f;
})
.flat();
}
function extractCustomEmoji(fragments: Fragment[], tags: Array<Array<string>>) { function extractCustomEmoji(fragments: Fragment[], tags: Array<Array<string>>) {
return fragments return fragments
.map(f => { .map(f => {
@ -225,6 +248,7 @@ function extractMarkdownCode(fragments: Fragment[]): (string | ParsedFragment)[]
export function transformText(body: string, tags: Array<Array<string>>) { export function transformText(body: string, tags: Array<Array<string>>) {
let fragments = extractLinks([body]); let fragments = extractLinks([body]);
fragments = extractMentions(fragments); fragments = extractMentions(fragments);
fragments = extractTagRefs(fragments, tags);
fragments = extractHashtags(fragments); fragments = extractHashtags(fragments);
fragments = extractInvoices(fragments); fragments = extractInvoices(fragments);
fragments = extractCashuTokens(fragments); fragments = extractCashuTokens(fragments);