1 Commits

Author SHA1 Message Date
2720e5b119 Improve Nostr link handling
Some checks are pending
CI / Test and lint (push) Waiting to run
Fixes a bunch of problems with how Nostr links are created and replaced
in Markdown content
2025-04-21 16:44:14 +04:00
17 changed files with 20 additions and 353 deletions

View File

@@ -21,10 +21,6 @@ p, pre, ul, ol, dl, blockquote, hr {
margin-bottom: 1.6em;
}
li {
margin-bottom: 0.8em;
}
a.anchor {
display: none;
}
@@ -45,11 +41,6 @@ pre code {
padding: 0.6rem 1rem;
}
.highlight pre {
font-size: 0.9em;
padding: 0.6rem 1rem;
}
img {
max-width: 100%;
}

View File

@@ -1,140 +0,0 @@
/**
* prism.js default theme for JavaScript, CSS and HTML
* Based on dabblet (http://dabblet.com)
* @author Lea Verou
*/
code[class*="language-"],
pre[class*="language-"] {
color: black;
background: none;
text-shadow: 0 1px white;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
font-size: 1em;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection,
code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection {
text-shadow: none;
background: #b3d4fc;
}
pre[class*="language-"]::selection, pre[class*="language-"] ::selection,
code[class*="language-"]::selection, code[class*="language-"] ::selection {
text-shadow: none;
background: #b3d4fc;
}
@media print {
code[class*="language-"],
pre[class*="language-"] {
text-shadow: none;
}
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: .5em 0;
overflow: auto;
}
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
background: #f5f2f0;
}
/* Inline code */
:not(pre) > code[class*="language-"] {
padding: .1em;
border-radius: .3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: slategray;
}
.token.punctuation {
color: #999;
}
.token.namespace {
opacity: .7;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
color: #905;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #690;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: #9a6e3a;
/* This background color was intended by the author of this theme. */
background: hsla(0, 0%, 100%, .5);
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #07a;
}
.token.function,
.token.class-name {
color: #DD4A68;
}
.token.regex,
.token.important,
.token.variable {
color: #e90;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}

View File

@@ -47,13 +47,6 @@ pre code {
color: var(--text-color-dark-bg);
}
.highlight pre,
.highlight code {
background-color: var(--background-color-body);
border: 2px solid #e8e3da;
color: var(--text-color-body);
}
dl dt {
color: var(--text-color-discreet);
}

View File

@@ -52,15 +52,6 @@ const config = {
},
query_timeout: parseInt(Deno.env.get("RELAY_TIMEOUT_MS") || "5000"),
njump_url: Deno.env.get("NJUMP_URL") || "https://njump.me",
prism: {
// TODO make configurable via ENV
// Supported languages: https://app.unpkg.com/prismjs@1.29.0/files/components
extraLanguages: [
"bash",
"markdown",
"typescript"
]
}
};
const staticUsersConfigured = Object.keys(staticUsers).length > 0;

View File

@@ -2,8 +2,8 @@
"tasks": {
"dev": "deno run --allow-all --watch server.ts",
"server": "deno run --allow-all server.ts",
"compile": "deno compile --allow-all --include ./assets/ --exclude ./tests/ --output ./build/substr_x86_64-unknown-linux-gnu server.ts",
"test": "DENO_ENV=test deno test --allow-read --allow-env"
"compile": "deno compile --allow-all --include ./assets/ --output ./build/substr_x86_64-unknown-linux-gnu server.ts",
"test": "DENO_ENV=test deno test tests/nostr_test.ts --allow-read --allow-env"
},
"imports": {
"@deno/gfm": "jsr:@deno/gfm@^0.10.0",

3
deno.lock generated
View File

@@ -54,7 +54,6 @@
"npm:nostr-tools@^2.7.0": "2.10.4",
"npm:nostr-wasm@0.1.0": "0.1.0",
"npm:path-to-regexp@6.2.1": "6.2.1",
"npm:prismjs@1.29.0": "1.29.0",
"npm:prismjs@^1.29.0": "1.29.0",
"npm:sanitize-html@^2.13.0": "2.13.1",
"npm:websocket-ts@^2.1.5": "2.1.5",
@@ -72,7 +71,7 @@
"npm:marked-alert",
"npm:marked-footnote",
"npm:marked-gfm-heading-id",
"npm:prismjs@^1.29.0",
"npm:prismjs",
"npm:sanitize-html"
]
},

View File

@@ -1,4 +1,3 @@
// deno-lint-ignore-file require-await
import config from "./config.ts";
import { lookupUsernameByPubkey as ldapLookupUsername } from "./ldap.ts";
import { lookupPubkeyByUsername as ldapLookupPubkey } from "./ldap.ts";

View File

@@ -20,8 +20,6 @@ export async function profileAtomFeed(
<id>${articleId}</id>
<title>${article.title}</title>
<link href="${article.url}" />
<link rel="alternate" type="text/html" href="${article.url}" />
<link rel="alternate" type="application/nostr+json" href="nostr:${article.naddr}" />
<updated>${isoDate(article.updatedAt)}</updated>
<published>${isoDate(article.publishedAt)}</published>
<summary>${article.summary}</summary>
@@ -36,15 +34,11 @@ export async function profileAtomFeed(
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>${profile.name} on Nostr (Articles)</title>
<link rel="alternate" type="text/html" href="${profile.profileUrl}" />
<link rel="alternate" type="application/nostr+json" href="nostr:${profile.npub}" />
<id>${feedId}</id>
<updated>${isoDate(lastUpdate)}</updated>
<icon>${profile.avatarImageUrl}</icon>
<author>
<name>${profile.name}</name>
<uri>${profile.profileUrl}</uri>
<nostr:uri>nostr:${profile.nprofile}</nostr>
<name>${name}</name>
</author>
${articlesXml}
</feed>

View File

@@ -19,7 +19,6 @@ function htmlLayout({ title, body, metaHtml }: HtmlLayoutOptions): string {
<title>${title}</title>
${metaHtml || ""}
<link rel="stylesheet" type="text/css" href="/assets/css/layout.css" />
<link rel="stylesheet" type="text/css" href="/assets/css/prism.css" />
<link rel="stylesheet" type="text/css" href="/assets/css/themes/default-light.css" />
</head>
<body>
@@ -114,6 +113,9 @@ export function articleListHtml(articles: Article[]): string {
function userAddressHtml(profile: Profile) {
let html = "";
if (profile.nip05) {
html += `<dt>Nostr address</dt><dd>${profile.nip05}</dd>\n`;
}
if (profile.lud16) {
html += `<dt>Lightning address</dt><dd>${profile.lud16}</dd>\n`;
}
@@ -210,8 +212,6 @@ function feedLinksHtml(profile: Profile) {
function profileMetaHtml(profile: Profile) {
return `
<link rel="icon" href="${profile.avatarImageUrl}" type="image/png">
<link rel="me" type="application/nostr+json" href="nostr:${profile.nprofile}" title="${profile.name}">
<link rel="alternate" type="application/nostr+json" href="nostr:${profile.nprofile}" title="${profile.name} on Nostr">
<meta property="og:url" content="${profile.profileUrl}">
<meta property="og:type" content="website">
<meta property="og:title" content="${profile.name} on Nostr">
@@ -231,9 +231,6 @@ function articleMetaHtml(article: Article, profile: Profile) {
return `
<link rel="icon" href="${profile.avatarImageUrl}" type="image/png">
<link rel="alternate" type="application/nostr+json" href="nostr:${article.naddr}" title="This article on Nostr">
<link rel="author" type="text/html" href="${profile.profileUrl}" title="${profile.name}">
<link rel="author" type="application/nostr+json" href="nostr:${profile.nprofile}" title="${profile.name}">
<meta property="og:url" content="${article.url}">
<meta property="og:type" content="website">
<meta property="og:title" content="${article.title}">

View File

@@ -4,10 +4,6 @@ import { NostrEvent as NEvent } from "@nostrify/nostrify";
import { replaceNostrUris } from "../nostr/links.ts";
import config from "../config.ts";
for (const language of config.prism.extraLanguages) {
await import(`npm:prismjs@1.29.0/components/prism-${language}.js`);
}
export default class Article {
event: NEvent;

View File

@@ -59,13 +59,6 @@ export default class Profile {
return nip19.npubEncode(this.pubkey);
}
get nprofile(): string {
return nip19.nprofileEncode({
pubkey: this.pubkey,
relays: [config.relay_urls[0]],
});
}
get profileUrl(): string {
return `${config.base_url}/@${this.username}`;
}

View File

@@ -1,4 +1,5 @@
import { NostrEvent, NostrFilter, NPool, NRelay1 } from "@nostrify/nostrify";
import { nip19 } from "@nostr/tools";
import config from "./config.ts";
import Article from "./models/article.ts";

View File

@@ -66,6 +66,7 @@ export async function replaceNostrUris(markdown: string): Promise<string> {
async function processUnprotectedText(text: string): Promise<string> {
const markdownLinkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
let modifiedText = text;
let lastIndex = 0;
const parts: string[] = [];

140
prism.css
View File

@@ -1,140 +0,0 @@
/**
* prism.js default theme for JavaScript, CSS and HTML
* Based on dabblet (http://dabblet.com)
* @author Lea Verou
*/
code[class*="language-"],
pre[class*="language-"] {
color: black;
background: none;
text-shadow: 0 1px white;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
font-size: 1em;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection,
code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection {
text-shadow: none;
background: #b3d4fc;
}
pre[class*="language-"]::selection, pre[class*="language-"] ::selection,
code[class*="language-"]::selection, code[class*="language-"] ::selection {
text-shadow: none;
background: #b3d4fc;
}
@media print {
code[class*="language-"],
pre[class*="language-"] {
text-shadow: none;
}
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: .5em 0;
overflow: auto;
}
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
background: #f5f2f0;
}
/* Inline code */
:not(pre) > code[class*="language-"] {
padding: .1em;
border-radius: .3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: slategray;
}
.token.punctuation {
color: #999;
}
.token.namespace {
opacity: .7;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
color: #905;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #690;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: #9a6e3a;
/* This background color was intended by the author of this theme. */
background: hsla(0, 0%, 100%, .5);
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #07a;
}
.token.function,
.token.class-name {
color: #DD4A68;
}
.token.regex,
.token.important,
.token.variable {
color: #e90;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}

View File

@@ -69,7 +69,7 @@ describe("Article", () => {
describe("#naddr", () => {
it("returns a bech32 addressable event ID", () => {
expect(article.naddr).toMatch(
/^naddr1qvzqqqr4gupzq8meqkx80g3yuklzymy0qf/,
/naddr1qvzqqqr4gupzq8meqkx80g3yuklzymy0qf/,
);
});
});

View File

@@ -40,12 +40,4 @@ describe("Profile", () => {
);
});
});
describe("#nprofile", () => {
it("returns a bech32 profile ID", () => {
expect(profile.nprofile).toMatch(
/^nprofile1qyt8wumn8ghj7mn0wd68ytntdaek6mmn9ehhyecqyq0hjpvvw73zfed7yf/,
);
});
});
});

View File

@@ -1,6 +1,6 @@
import { beforeAll, describe, it } from "@std/testing/bdd";
import { expect } from "@std/expect";
import { replaceNostrUris } from "../../nostr/links.ts";
import { replaceNostrUris } from "../nostr/links.ts";
describe("Nostr links", () => {
@@ -13,47 +13,47 @@ describe("Nostr links", () => {
result = await replaceNostrUris(mdContent);
});
it("does not replace URIs in URLs", () => {
it("does not replace URIs in URLs", async () => {
expect(result).toMatch(new RegExp("https://badges.page/p/npub1cpmvpsqtzxl4px44dp4544xwgu0ryv2lscl3qexq42dfakuza02s4fsapc"));
});
it("does not replace URIs in fenced code blocks", () => {
it("does not replace URIs in fenced code blocks", async () => {
expect(result).toMatch(new RegExp("Follow nostr:npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7"));
});
it("does not replace URIs in inline code blocks", () => {
it("does not replace URIs in inline code blocks", async () => {
expect(result).toMatch(new RegExp("raucao: nostr:npub1raustrrh5gjwt03zdj8syn9vmt2dwsv9t467m8c3gua636uxu89svgdees"));
});
describe("for unknown usernames", () => {
it("replaces plain nostr:id URIs with a markdown link", () => {
it("replaces plain nostr:id URIs with a markdown link", async () => {
expect(result).toMatch(/Amber scheme 1\: \[npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7\]\(https\:\/\/njump\.me\/npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7\)/);
expect(result).toMatch(/Amber scheme 2\: \[npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7\]\(https\:\/\/njump\.me\/npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7\)/);
});
it("replaces @id URIs with a markdown link", () => {
it("replaces @id URIs with a markdown link", async () => {
expect(result).toMatch(/Amber at 1\: \[@npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7\]\(https\:\/\/njump\.me\/npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7\)/);
expect(result).toMatch(/Amber at 2\: \[@npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7\]\(https\:\/\/njump\.me\/npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7\)/);
});
it("replaces nostr links with external links", () => {
it("replaces nostr links with external links", async () => {
expect(result).toMatch(/Amber scheme link 1\: \[Amber\]\(https\:\/\/njump\.me\/npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7\)/);
expect(result).toMatch(/Amber scheme link 2\: \[Amber\]\(https\:\/\/njump\.me\/npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7\)/);
});
});
describe("for known usernames", () => {
it("replaces plain nostr:id URIs with a markdown link", () => {
it("replaces plain nostr:id URIs with a markdown link", async () => {
expect(result).toMatch(/raucao scheme 1\: \[npub1raustrrh5gjwt03zdj8syn9vmt2dwsv9t467m8c3gua636uxu89svgdees\]\(\/@raucao\)/);
expect(result).toMatch(/raucao scheme 2\: \[npub1raustrrh5gjwt03zdj8syn9vmt2dwsv9t467m8c3gua636uxu89svgdees\]\(\/@raucao\)/);
});
it("replaces @id URIs with a markdown link", () => {
it("replaces @id URIs with a markdown link", async () => {
expect(result).toMatch(/raucao at 1\: \[@npub1raustrrh5gjwt03zdj8syn9vmt2dwsv9t467m8c3gua636uxu89svgdees\]\(\/@raucao\)/);
expect(result).toMatch(/raucao at 2\: \[@npub1raustrrh5gjwt03zdj8syn9vmt2dwsv9t467m8c3gua636uxu89svgdees\]\(\/@raucao\)/);
});
it("replaces scheme links with internal links", () => {
it("replaces scheme links with internal links", async () => {
expect(result).toMatch(/raucao scheme link 1\: \[raucao\]\(\/@raucao\)/);
expect(result).toMatch(/raucao scheme link 2\: \[raucao\]\(\/@raucao\)/);
});