Compare commits
22 Commits
v1.0.0-alp
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d96b9fbc1 | |||
| 1c37d2dc99 | |||
| 1e246af26e | |||
| 7fe0721416 | |||
| d3ad136eab | |||
| cd335a366e | |||
| b307d4de03 | |||
| 91eff4ad07 | |||
| 994053e080 | |||
| bc37756097 | |||
| 3a5733eeee | |||
| 7c2549cbfe | |||
| e3bd385c96 | |||
| 038ce15908 | |||
| 7aebcfc43f | |||
| b7eccde9d0 | |||
| 9305e9f718 | |||
| b907cc2f65 | |||
| 29e2fca2a5 | |||
| d3e908b2b0 | |||
| 5608176a20 | |||
| fa21e72b3f |
@ -13,6 +13,6 @@ jobs:
|
||||
node-version: 18
|
||||
- uses: denoland/setup-deno@v2
|
||||
with:
|
||||
deno-version: v2.1.x
|
||||
deno-version: v2.5.x
|
||||
- run: "deno task test"
|
||||
- run: "deno lint"
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
.env
|
||||
users.yaml
|
||||
build/
|
||||
coverage/
|
||||
|
||||
@ -21,6 +21,10 @@ p, pre, ul, ol, dl, blockquote, hr {
|
||||
margin-bottom: 1.6em;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0.8em;
|
||||
}
|
||||
|
||||
a.anchor {
|
||||
display: none;
|
||||
}
|
||||
@ -41,6 +45,11 @@ pre code {
|
||||
padding: 0.6rem 1rem;
|
||||
}
|
||||
|
||||
.highlight pre {
|
||||
font-size: 0.9em;
|
||||
padding: 0.6rem 1rem;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
140
assets/css/prism.css
Normal file
140
assets/css/prism.css
Normal file
@ -0,0 +1,140 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
@ -47,6 +47,13 @@ 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);
|
||||
}
|
||||
|
||||
11
config.ts
11
config.ts
@ -1,5 +1,5 @@
|
||||
import { load } from "@std/dotenv";
|
||||
import { parse as parseYaml } from "jsr:@std/yaml";
|
||||
import { parse as parseYaml } from "@std/yaml";
|
||||
import { checkFileExists } from "./utils.ts";
|
||||
import { log } from "./log.ts";
|
||||
|
||||
@ -52,6 +52,15 @@ 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;
|
||||
|
||||
39
deno.json
39
deno.json
@ -1,9 +1,9 @@
|
||||
{
|
||||
"tasks": {
|
||||
"dev": "deno run --allow-all --watch server.ts",
|
||||
"server": "deno run --allow-all server.ts",
|
||||
"compile": "deno compile --allow-all --include ./assets/ --output ./build/substr_x86_64-unknown-linux-gnu server.ts",
|
||||
"test": "DENO_ENV=test deno test --allow-read --allow-env"
|
||||
"dev": "deno run -P --watch server.ts",
|
||||
"server": "deno run -P server.ts",
|
||||
"compile": "deno compile -P --include ./assets/ --exclude ./tests/ --output ./build/substr_x86_64-unknown-linux-gnu server.ts",
|
||||
"test": "DENO_ENV=test deno test -P=test"
|
||||
},
|
||||
"imports": {
|
||||
"@deno/gfm": "jsr:@deno/gfm@^0.10.0",
|
||||
@ -18,7 +18,36 @@
|
||||
},
|
||||
"fmt": {
|
||||
"exclude": [
|
||||
"magick.ts"
|
||||
"magick.ts",
|
||||
"assets/css/prism.css",
|
||||
"tests/fixtures/"
|
||||
]
|
||||
},
|
||||
"permissions": {
|
||||
"default": {
|
||||
"read": [
|
||||
".env",
|
||||
".env.test",
|
||||
"users.yaml",
|
||||
"users.yaml.test",
|
||||
"/etc/substr/users.yaml",
|
||||
"/etc/substr/users.yaml.test",
|
||||
"/tmp/substr/",
|
||||
"assets/"
|
||||
],
|
||||
"write": ["/tmp/substr/"],
|
||||
"run": ["which", "magick", "convert"],
|
||||
"env": true,
|
||||
"net": true
|
||||
},
|
||||
"test": {
|
||||
"read": [
|
||||
"tests/",
|
||||
".env.test",
|
||||
"users.yaml.test",
|
||||
"/etc/substr/users.yaml.test"
|
||||
],
|
||||
"env": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3
deno.lock
generated
3
deno.lock
generated
@ -54,6 +54,7 @@
|
||||
"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",
|
||||
@ -71,7 +72,7 @@
|
||||
"npm:marked-alert",
|
||||
"npm:marked-footnote",
|
||||
"npm:marked-gfm-heading-id",
|
||||
"npm:prismjs",
|
||||
"npm:prismjs@^1.29.0",
|
||||
"npm:sanitize-html"
|
||||
]
|
||||
},
|
||||
|
||||
@ -3,7 +3,9 @@ import config from "./config.ts";
|
||||
import { lookupUsernameByPubkey as ldapLookupUsername } from "./ldap.ts";
|
||||
import { lookupPubkeyByUsername as ldapLookupPubkey } from "./ldap.ts";
|
||||
|
||||
export async function lookupUsernameByPubkey(pubkey: string): Promise<string | undefined> {
|
||||
export async function lookupUsernameByPubkey(
|
||||
pubkey: string,
|
||||
): Promise<string | undefined> {
|
||||
let username;
|
||||
for (const [key, value] of Object.entries(config.staticUsers)) {
|
||||
if (value === pubkey) {
|
||||
@ -21,7 +23,9 @@ export async function lookupUsernameByPubkey(pubkey: string): Promise<string | u
|
||||
}
|
||||
}
|
||||
|
||||
export async function lookupPubkeyByUsername(username: string): Promise<string | undefined> {
|
||||
export async function lookupPubkeyByUsername(
|
||||
username: string,
|
||||
): Promise<string | undefined> {
|
||||
const pubkey = config.staticUsers[username];
|
||||
|
||||
if (pubkey) {
|
||||
|
||||
10
feeds.ts
10
feeds.ts
@ -20,6 +20,8 @@ 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>
|
||||
@ -32,13 +34,17 @@ export async function profileAtomFeed(
|
||||
|
||||
return `
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:nostr="urn:nostr:protocol">
|
||||
<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>${name}</name>
|
||||
<name>${profile.name}</name>
|
||||
<uri>${profile.profileUrl}</uri>
|
||||
<nostr:uri>nostr:${profile.nprofile}</nostr:uri>
|
||||
</author>
|
||||
${articlesXml}
|
||||
</feed>
|
||||
|
||||
@ -4,6 +4,7 @@ import { fetchArticlesByAuthor, fetchProfileEvent } from "../nostr.ts";
|
||||
import { profileAtomFeed } from "../feeds.ts";
|
||||
import Profile from "../models/profile.ts";
|
||||
import { notFoundHandler } from "../handlers/errors.ts";
|
||||
import { generateOgProfileImage } from "../magick.ts";
|
||||
|
||||
const userAtomFeedHandler = async function (ctx: Context) {
|
||||
const username = ctx.state.username;
|
||||
@ -20,6 +21,7 @@ const userAtomFeedHandler = async function (ctx: Context) {
|
||||
const profile = new Profile(profileEvent, username);
|
||||
|
||||
if (profile.nip05) {
|
||||
await generateOgProfileImage(profile);
|
||||
const articles = await fetchArticlesByAuthor(pubkey, 10);
|
||||
const atom = await profileAtomFeed(profile, articles);
|
||||
|
||||
|
||||
9
html.ts
9
html.ts
@ -19,6 +19,7 @@ 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>
|
||||
@ -113,9 +114,6 @@ 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`;
|
||||
}
|
||||
@ -212,6 +210,8 @@ 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,6 +231,9 @@ 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}">
|
||||
|
||||
10
ldap.ts
10
ldap.ts
@ -8,7 +8,9 @@ if (ldapEnabled) {
|
||||
client = new Client({ url: ldap.url as string });
|
||||
}
|
||||
|
||||
export async function lookupPubkeyByUsername(username: string): Promise<string | undefined> {
|
||||
export async function lookupPubkeyByUsername(
|
||||
username: string,
|
||||
): Promise<string | undefined> {
|
||||
let pubkey: string | undefined;
|
||||
|
||||
try {
|
||||
@ -35,7 +37,9 @@ export async function lookupPubkeyByUsername(username: string): Promise<string |
|
||||
return pubkey;
|
||||
}
|
||||
|
||||
export async function lookupUsernameByPubkey(pubkey: string): Promise<string | undefined> {
|
||||
export async function lookupUsernameByPubkey(
|
||||
pubkey: string,
|
||||
): Promise<string | undefined> {
|
||||
let username: string | undefined;
|
||||
|
||||
try {
|
||||
@ -49,6 +53,8 @@ export async function lookupUsernameByPubkey(pubkey: string): Promise<string | u
|
||||
if (searchEntries.length > 0) {
|
||||
username = searchEntries[0].cn.toString();
|
||||
}
|
||||
|
||||
await client.unbind();
|
||||
} catch (e) {
|
||||
await client.unbind();
|
||||
throw e;
|
||||
|
||||
@ -4,6 +4,10 @@ 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;
|
||||
|
||||
|
||||
@ -59,6 +59,13 @@ 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}`;
|
||||
}
|
||||
|
||||
@ -35,22 +35,26 @@ export async function nostrUriToUrl(uri: string): Promise<string> {
|
||||
}
|
||||
|
||||
export async function replaceNostrUris(markdown: string): Promise<string> {
|
||||
const protectedRegex = /(`{3,}[\s\S]*?`{3,})|(`[^`]*`)|(<pre>[\s\S]*?<\/pre>)|(https?:\/\/[^\s<>"']+)/gi;
|
||||
const protectedRegex =
|
||||
/(`{3,}[\s\S]*?`{3,})|(`[^`]*`)|(<pre>[\s\S]*?<\/pre>)|(https?:\/\/[^\s<>"']+)/gi;
|
||||
|
||||
// Split text into segments: unprotected text and protected areas (code blocks, URLs)
|
||||
const segments: string[] = [];
|
||||
let lastIndex = 0;
|
||||
|
||||
markdown.replace(protectedRegex, (match, _fencedCode, _inlineCode, _preTag, _url, index) => {
|
||||
segments.push(markdown.slice(lastIndex, index));
|
||||
segments.push(match);
|
||||
lastIndex = index + match.length;
|
||||
return match;
|
||||
});
|
||||
markdown.replace(
|
||||
protectedRegex,
|
||||
(match, _fencedCode, _inlineCode, _preTag, _url, index) => {
|
||||
segments.push(markdown.slice(lastIndex, index));
|
||||
segments.push(match);
|
||||
lastIndex = index + match.length;
|
||||
return match;
|
||||
},
|
||||
);
|
||||
segments.push(markdown.slice(lastIndex));
|
||||
|
||||
// Process each segment
|
||||
let result = '';
|
||||
let result = "";
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
if (i % 2 === 1 || protectedRegex.test(segments[i])) {
|
||||
// Protected segment (code block or URL), leave unchanged
|
||||
@ -78,7 +82,9 @@ async function processUnprotectedText(text: string): Promise<string> {
|
||||
parts.push(await replaceUrisInText(text.slice(lastIndex, match.index)));
|
||||
|
||||
// Process the link target
|
||||
if (nostrUriRegex.test(target) && target.match(nostrUriRegex)![0] === target) {
|
||||
if (
|
||||
nostrUriRegex.test(target) && target.match(nostrUriRegex)![0] === target
|
||||
) {
|
||||
// Target is a Nostr URI, replace with resolved URL
|
||||
const resolvedUrl = await nostrUriToUrl(target);
|
||||
parts.push(`[${linkText}](${resolvedUrl})`);
|
||||
@ -93,12 +99,13 @@ async function processUnprotectedText(text: string): Promise<string> {
|
||||
// Add any remaining text after the last link
|
||||
parts.push(await replaceUrisInText(text.slice(lastIndex)));
|
||||
|
||||
return parts.join('');
|
||||
return parts.join("");
|
||||
}
|
||||
|
||||
async function replaceUrisInText(text: string): Promise<string> {
|
||||
let modifiedText = text;
|
||||
const replacements: { start: number; end: number; replacement: string }[] = [];
|
||||
const replacements: { start: number; end: number; replacement: string }[] =
|
||||
[];
|
||||
|
||||
// Collect all replacements for bare Nostr URIs
|
||||
let match;
|
||||
@ -117,7 +124,8 @@ async function replaceUrisInText(text: string): Promise<string> {
|
||||
// Apply replacements from right to left to avoid index shifting
|
||||
for (let i = replacements.length - 1; i >= 0; i--) {
|
||||
const { start, end, replacement } = replacements[i];
|
||||
modifiedText = modifiedText.slice(0, start) + replacement + modifiedText.slice(end);
|
||||
modifiedText = modifiedText.slice(0, start) + replacement +
|
||||
modifiedText.slice(end);
|
||||
}
|
||||
|
||||
return modifiedText;
|
||||
@ -125,5 +133,5 @@ async function replaceUrisInText(text: string): Promise<string> {
|
||||
|
||||
function cleanUriForTitle(uri: string): string {
|
||||
// Remove "nostr:" prefix, keep "@" for the title
|
||||
return uri.startsWith('nostr:') ? uri.replace(/^nostr:/, '') : uri;
|
||||
return uri.startsWith("nostr:") ? uri.replace(/^nostr:/, "") : uri;
|
||||
}
|
||||
|
||||
@ -69,7 +69,7 @@ describe("Article", () => {
|
||||
describe("#naddr", () => {
|
||||
it("returns a bech32 addressable event ID", () => {
|
||||
expect(article.naddr).toMatch(
|
||||
/naddr1qvzqqqr4gupzq8meqkx80g3yuklzymy0qf/,
|
||||
/^naddr1qvzqqqr4gupzq8meqkx80g3yuklzymy0qf/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -40,4 +40,12 @@ describe("Profile", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#nprofile", () => {
|
||||
it("returns a bech32 profile ID", () => {
|
||||
expect(profile.nprofile).toMatch(
|
||||
/^nprofile1qyt8wumn8ghj7mn0wd68ytntdaek6mmn9ehhyecqyq0hjpvvw73zfed7yf/,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -3,59 +3,94 @@ import { expect } from "@std/expect";
|
||||
import { replaceNostrUris } from "../../nostr/links.ts";
|
||||
|
||||
describe("Nostr links", () => {
|
||||
|
||||
describe("#replaceNostrUris", () => {
|
||||
let mdContent: string;
|
||||
let result: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
mdContent = Deno.readTextFileSync("tests/fixtures/article-2.md"),
|
||||
mdContent = Deno.readTextFileSync("tests/fixtures/article-2.md");
|
||||
result = await replaceNostrUris(mdContent);
|
||||
});
|
||||
|
||||
it("does not replace URIs in URLs", () => {
|
||||
expect(result).toMatch(new RegExp("https://badges.page/p/npub1cpmvpsqtzxl4px44dp4544xwgu0ryv2lscl3qexq42dfakuza02s4fsapc"));
|
||||
expect(result).toMatch(
|
||||
new RegExp(
|
||||
"https://badges.page/p/npub1cpmvpsqtzxl4px44dp4544xwgu0ryv2lscl3qexq42dfakuza02s4fsapc",
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not replace URIs in fenced code blocks", () => {
|
||||
expect(result).toMatch(new RegExp("Follow nostr:npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7"));
|
||||
expect(result).toMatch(
|
||||
new RegExp(
|
||||
"Follow nostr:npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7",
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not replace URIs in inline code blocks", () => {
|
||||
expect(result).toMatch(new RegExp("raucao: nostr:npub1raustrrh5gjwt03zdj8syn9vmt2dwsv9t467m8c3gua636uxu89svgdees"));
|
||||
expect(result).toMatch(
|
||||
new RegExp(
|
||||
"raucao: nostr:npub1raustrrh5gjwt03zdj8syn9vmt2dwsv9t467m8c3gua636uxu89svgdees",
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
describe("for unknown usernames", () => {
|
||||
it("replaces plain nostr:id URIs with a markdown link", () => {
|
||||
expect(result).toMatch(/Amber scheme 1\: \[npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7\]\(https\:\/\/njump\.me\/npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7\)/);
|
||||
expect(result).toMatch(/Amber scheme 2\: \[npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7\]\(https\:\/\/njump\.me\/npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7\)/);
|
||||
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", () => {
|
||||
expect(result).toMatch(/Amber at 1\: \[@npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7\]\(https\:\/\/njump\.me\/npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7\)/);
|
||||
expect(result).toMatch(/Amber at 2\: \[@npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7\]\(https\:\/\/njump\.me\/npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7\)/);
|
||||
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", () => {
|
||||
expect(result).toMatch(/Amber scheme link 1\: \[Amber\]\(https\:\/\/njump\.me\/npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7\)/);
|
||||
expect(result).toMatch(/Amber scheme link 2\: \[Amber\]\(https\:\/\/njump\.me\/npub1am3ermkr250dywukzqnaug64cred3x5jht6f3kdhfp3h0rgtjlpqecxrv7\)/);
|
||||
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", () => {
|
||||
expect(result).toMatch(/raucao scheme 1\: \[npub1raustrrh5gjwt03zdj8syn9vmt2dwsv9t467m8c3gua636uxu89svgdees\]\(\/@raucao\)/);
|
||||
expect(result).toMatch(/raucao scheme 2\: \[npub1raustrrh5gjwt03zdj8syn9vmt2dwsv9t467m8c3gua636uxu89svgdees\]\(\/@raucao\)/);
|
||||
expect(result).toMatch(
|
||||
/raucao scheme 1\: \[npub1raustrrh5gjwt03zdj8syn9vmt2dwsv9t467m8c3gua636uxu89svgdees\]\(\/@raucao\)/,
|
||||
);
|
||||
expect(result).toMatch(
|
||||
/raucao scheme 2\: \[npub1raustrrh5gjwt03zdj8syn9vmt2dwsv9t467m8c3gua636uxu89svgdees\]\(\/@raucao\)/,
|
||||
);
|
||||
});
|
||||
|
||||
it("replaces @id URIs with a markdown link", () => {
|
||||
expect(result).toMatch(/raucao at 1\: \[@npub1raustrrh5gjwt03zdj8syn9vmt2dwsv9t467m8c3gua636uxu89svgdees\]\(\/@raucao\)/);
|
||||
expect(result).toMatch(/raucao at 2\: \[@npub1raustrrh5gjwt03zdj8syn9vmt2dwsv9t467m8c3gua636uxu89svgdees\]\(\/@raucao\)/);
|
||||
expect(result).toMatch(
|
||||
/raucao at 1\: \[@npub1raustrrh5gjwt03zdj8syn9vmt2dwsv9t467m8c3gua636uxu89svgdees\]\(\/@raucao\)/,
|
||||
);
|
||||
expect(result).toMatch(
|
||||
/raucao at 2\: \[@npub1raustrrh5gjwt03zdj8syn9vmt2dwsv9t467m8c3gua636uxu89svgdees\]\(\/@raucao\)/,
|
||||
);
|
||||
});
|
||||
|
||||
it("replaces scheme links with internal links", () => {
|
||||
expect(result).toMatch(/raucao scheme link 1\: \[raucao\]\(\/@raucao\)/);
|
||||
expect(result).toMatch(/raucao scheme link 2\: \[raucao\]\(\/@raucao\)/);
|
||||
expect(result).toMatch(
|
||||
/raucao scheme link 1\: \[raucao\]\(\/@raucao\)/,
|
||||
);
|
||||
expect(result).toMatch(
|
||||
/raucao scheme link 2\: \[raucao\]\(\/@raucao\)/,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user