@konemono/nostr-content-parser
Version:
Parse Nostr content into tokens
256 lines (223 loc) • 8.76 kB
HTML
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Nostr Parser Live Demo with Tags</title>
<style>
body {
font-family: sans-serif;
padding: 2rem;
}
textarea {
width: 100%;
font-family: monospace;
margin-bottom: 1rem;
}
#input {
height: 150px;
}
#tags {
height: 100px;
}
pre {
background: #f4f4f4;
padding: 1rem;
white-space: pre-wrap;
word-wrap: break-word;
max-height: 400px;
overflow: auto;
}
#htmlOutput {
background: #fffbea;
padding: 1rem;
border: 1px solid #ddd;
margin-top: 1rem;
max-height: 400px;
overflow: auto;
white-space: pre-line;
word-break: normal;
word-wrap: break-word;
}
label {
font-weight: bold;
display: block;
margin-bottom: 0.5rem;
}
.section {
margin-bottom: 1.5rem;
}
</style>
</head>
<body>
<h1>Nostr Parser Live Demo with Tags</h1>
<div class="section">
<label>
<input type="checkbox" id="prefixOnly" checked />
Include Nostr prefix only (nostr:...)
</label>
<label>
<input type="checkbox" id="hashtagsFromTagsOnly" checked />
Hashtags from tags only (t-tag only)
</label>
</div>
<div class="section">
<label for="input">Input Content</label>
<textarea id="input"></textarea>
</div>
<div class="section">
<label for="tags">Tags (JSON array)</label>
<textarea id="tags"></textarea>
</div>
<div class="section">
<label>Parsed Tokens</label>
<pre id="output"></pre>
</div>
<div class="section">
<label>Parsed Tokens (HTML)</label>
<div id="htmlOutput"></div>
</div>
<script type="module">
import { parseContent } from "../src/parseContent";
import { TokenType } from "../src/patterns";
const inputEl = document.getElementById("input");
const outputEl = document.getElementById("output");
const prefixOnlyEl = document.getElementById("prefixOnly");
const hashtagsFromTagsOnlyEl = document.getElementById(
"hashtagsFromTagsOnly"
);
const tagsEl = document.getElementById("tags");
const htmlOutputEl = document.getElementById("htmlOutput");
const initialContent = `✅ nostr:付きNostr ID
nostr:npub1sjcvg64knxkrt6ev52rywzu9uzqakgy8ehhk8yezxmpewsthst6sw3jqcw
nostr:note1yxwmh466t60gxand59s2qvqzpf08ygml7gkakwjsavd0gaarec7s9rzy44
nostr:nevent1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq
✅ nostr:なし(prefixOnlyのチェックで動作変化)
npub1sjcvg64knxkrt6ev52rywzu9uzqakgy8ehhk8yezxmpewsthst6sw3jqcw
note1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq
✅ 旧引用
#[0]
✅ カスタム絵文字(:monosimple: / :monokashiwa:)
This is a :monosimple: and also a :monokashiwa:.
✅ リレーURL
wss://nos.lol
ws://localhost:7000
ws://127.0.0.1:4869
✅ URLチェック(前後に日本語や記号が付く場合)
このサイト→https://example.com/nostr?ref=test←もチェック
末尾の句点。https://example.com/path/to/resource。
前後に括弧:(https://test.com) や「https://test.com」
✅ NIP識別子(プレーンテキスト)
NIP-01 is basic. Others: NIP-19, NIP-B0.
✅ Invoice
lnbc500n1p598rhqpp5d4kgq0lr88glxhs4sxva6uduv3543q9d5vaad355krvjnkmjwgcshp5cxjk04cumj5hpn0vvjcxpsvkszfqxr2m4fnmxtutcljdkz5gx72qcqzzsxqyz5vqsp5ughur83l2uzqffyen9q3has96xjflfyfd49fmjswmhcaec9m0qjq9qxpqysgq554y0gn9nrh9vtxpm350v55dvga0uk5csydutcs7t5t6vtcgjth30sfuuv986vud4rewv5u6kldtpumuj3sgrdhlv57dm3wra3sey7qpcsc394
✅ 雑なテキスト確認
hello@example.com も test@lnurl.example も含めて
#hashtag や @mention などは無効として扱われる?
bitcoin:bc1qexampleaddressforbtc12345
✅ 混合
https://nostviewstr.vercel.app/npub1sjcvg64knxkrt6ev52rywzu9uzqakgy8ehhk8yezxmpewsthst6sw3jqcw/30003/monomoji
https://nostviewstr.vercel.app/naddr1qvzqqqr4xvpzpp9sc34tdxdvxh4jeg5xgu9ctcypmvsg0n00vwfjydkrjaqh0qh4qy28wue69uhnzv3h9cczuvpwxyargwpk8yhszynhwden5te0den8yetvv9ujuctswqhszrnhwden5te0dehhxtnvdakz7qgewaehxw309ahx7umywf5hvefwv9c8qtmjv4kxz7f0qyshwumn8ghj7mn0wd68yttjv4kxz7fddfczumt0vd6xzmn99e3k7mf0qy08wumn8ghj7mn0wd68ytnrdakhq6tvv5kk2unjdaezumn9wshszxrhwden5te0dehhxarj9e5hgarpdekk7tndv4hz7qg7waehxw309ah8yetvv9uj66ns9e3j6um5v4kxcctj9ehx2ap0qyshwumn8ghj7un9d3shjtt2wqhxummnw3ezuamfwfjkgmn9wshx5up0qyvhwumn8ghj7un9d3shjtnddakk7um5wgh8q6twdvhsz9nhwden5te0wfjkccte9ekk7um5wgh8qatz9uq36amnwvaz7tmnwf68yetvv9ujucedwd6x2mrvv9ezumn9wshsz9rhwden5te0wahhgtnwdaehgu3wdejhgtcpzdmhxue69uhhwmm59e6hg7r09ehkuef0qy88wumn8ghj77tpvf6jumt99uqqsmt0dehk6mm2dy40yxmy
`;
const initialTags = `[
["a","30023:84b0c46ab699ac35eb2ca286470b85e081db2087cdef63932236c397417782f5:mono-tools"],
["emoji", "monosimple", "https://i.imgur.com/n0Cqc5T.png"],
["emoji", "monokashiwa", "https://i.imgur.com/aRcM4IC.png"]
]`;
inputEl.value = initialContent;
tagsEl.value = initialTags;
async function updateOutput() {
let tags = [];
try {
tags = JSON.parse(tagsEl.value);
} catch {
outputEl.textContent = "Invalid JSON in tags";
htmlOutputEl.textContent = "";
return;
}
const includeNostrPrefixOnly = prefixOnlyEl.checked;
const hashtagsFromTagsOnly = hashtagsFromTagsOnlyEl.checked;
const tokens = parseContent(inputEl.value, tags, {
includeNostrPrefixOnly,
hashtagsFromTagsOnly,
});
outputEl.textContent = JSON.stringify(tokens, null, 2);
htmlOutputEl.innerHTML = await renderContentToHTML(
inputEl.value,
tags,
{
includeNostrPrefixOnly,
hashtagsFromTagsOnly,
}
);
}
function escapeHtml(str) {
return str.replace(/[&<>"']/g, (m) => {
switch (m) {
case "&":
return "&";
case "<":
return "<";
case ">":
return ">";
case '"':
return """;
case "'":
return "'";
default:
return m;
}
});
}
function renderContentToHTML(content, tags, options) {
const tokens = parseContent(content, tags, options);
return tokens
.map((token) => {
switch (token.type) {
case TokenType.NIP19:
return `<a href="https://njump.me/${escapeHtml(
token.metadata.plainNip19
)}" class="nostr-link">${escapeHtml(token.content)}</a>`;
case TokenType.HASHTAG:
return `<a href="/tag/${escapeHtml(
token.content.slice(1)
)}" class="hashtag">${escapeHtml(token.content)}</a>`;
case TokenType.URL:
case TokenType.LN_URL:
return `<a href="${escapeHtml(
token.content
)}" target="_blank" rel="noopener noreferrer" class="url">${escapeHtml(
token.content
)}</a>`;
case TokenType.CUSTOM_EMOJI: {
if (token.metadata.url) {
return `<img src="${escapeHtml(
token.metadata.url
)}" alt="${escapeHtml(
token.metadata.name
)}" class="custom-emoji" />`;
}
return escapeHtml(token.content);
}
case TokenType.NIP_IDENTIFIER: {
return `<a href="https://github.com/nostr-protocol/nips/blob/master/${escapeHtml(
token.metadata.number
)}.md" class="nostr-link">${escapeHtml(token.content)}</a>`;
}
case TokenType.LNBC: {
return `<a href="lightning:${escapeHtml(
token.content
)}" class="lnbc-link">${escapeHtml(token.content)}</a>`;
}
default:
return escapeHtml(token.content);
}
})
.join("");
}
inputEl.addEventListener("input", updateOutput);
prefixOnlyEl.addEventListener("change", updateOutput);
tagsEl.addEventListener("input", updateOutput);
updateOutput();
</script>
</body>
</html>