Expand i18n locales and improve reviews UI
Add many new language packs with RTL support, refresh brand assets, and harden review rendering with tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* The hostile review body, exactly as the card spec's acceptance test demands:
|
||||
* HTML tags, a 500-character URL, a pasted npub, and 40 consecutive newlines.
|
||||
* Everything in it is legal relay content someone could sign tomorrow, and the card
|
||||
* must render all of it as inert text inside its own bounds.
|
||||
*/
|
||||
|
||||
/** A 500-character URL: scheme + host + a long generated path. */
|
||||
export const LONG_URL = `https://very.long.example.com/${'segment/'.repeat(57)}page`.slice(0, 500);
|
||||
|
||||
/** An npub pasted into prose: an unbroken 63-character token, no nostr: prefix. */
|
||||
export const PASTED_NPUB = 'npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6';
|
||||
|
||||
export const HOSTILE_REVIEW_CONTENT = [
|
||||
`<script>alert('xss')</script><img src=x onerror=alert(1)> <b>bold?</b>`,
|
||||
`Look at ${LONG_URL} for details.`,
|
||||
`My key is ${PASTED_NPUB} thanks`,
|
||||
'\n'.repeat(40),
|
||||
'The end. \u{1F9C0}✅',
|
||||
].join('\n');
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* The review body renderer, against hostile relay content.
|
||||
*
|
||||
* `lib/review-body.ts` is deliberately import-free so this file can load it under
|
||||
* plain node with type stripping: `pnpm test` (node --experimental-strip-types).
|
||||
* If these fail, an event someone can sign today can break or script the card.
|
||||
*/
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { escapeHtml, reviewBodyHtml } from '../src/lib/review-body.ts';
|
||||
import { HOSTILE_REVIEW_CONTENT, LONG_URL, PASTED_NPUB } from './hostile-review.mjs';
|
||||
|
||||
/** The output with this module's own <a> elements removed: nothing else may be markup. */
|
||||
function withoutOwnLinks(html) {
|
||||
return html.replace(/<a class="rev-link" [^>]*>.*?<\/a>/gs, '');
|
||||
}
|
||||
|
||||
test('the hostile fixture renders with no live markup', () => {
|
||||
const html = reviewBodyHtml(HOSTILE_REVIEW_CONTENT);
|
||||
const rest = withoutOwnLinks(html);
|
||||
assert.ok(!rest.includes('<'), `unescaped markup survived: ${rest.slice(0, 200)}`);
|
||||
assert.ok(html.includes('<script>'), 'script tag must render as text');
|
||||
assert.ok(!/<img|<b>|<script/.test(html), 'no element from the body ever becomes real');
|
||||
});
|
||||
|
||||
test('40 newlines collapse to a single blank line', () => {
|
||||
const html = reviewBodyHtml(HOSTILE_REVIEW_CONTENT);
|
||||
assert.ok(!/\n{3,}/.test(html), '3+ consecutive newlines must not survive');
|
||||
});
|
||||
|
||||
test('the 500-char URL links out but shows at most 40 characters', () => {
|
||||
const html = reviewBodyHtml(HOSTILE_REVIEW_CONTENT);
|
||||
const match = /<a class="rev-link" href="([^"]+)" target="_blank" rel="nofollow ugc noopener">([^<]+)<\/a>/.exec(html);
|
||||
assert.ok(match, 'the URL must become a link');
|
||||
assert.equal(match[1], LONG_URL, 'the full URL goes in the href');
|
||||
assert.ok(match[2].length <= 40, `shown text is ${match[2].length} chars, wanted <= 40`);
|
||||
assert.ok(match[2].includes('…'), 'truncation is middle-out, marked with an ellipsis');
|
||||
assert.ok(match[2].startsWith('https://very.long.exampl'), 'the head survives');
|
||||
});
|
||||
|
||||
test('a pasted npub stays inert text', () => {
|
||||
const html = reviewBodyHtml(HOSTILE_REVIEW_CONTENT);
|
||||
assert.ok(html.includes(PASTED_NPUB), 'the npub passes through as text');
|
||||
assert.ok(!html.includes(`href="${PASTED_NPUB}`), 'and is never a link');
|
||||
});
|
||||
|
||||
test('emoji pass through untouched', () => {
|
||||
const html = reviewBodyHtml(HOSTILE_REVIEW_CONTENT);
|
||||
assert.ok(html.includes('\u{1F9C0}✅'));
|
||||
});
|
||||
|
||||
test('nostr: URIs render as short plain text, never links', () => {
|
||||
const html = reviewBodyHtml(`see nostr:${PASTED_NPUB} for who I am`);
|
||||
assert.ok(!html.includes('<a'), 'a nostr URI must not become a link');
|
||||
assert.ok(!html.includes('nostr:'), 'the scheme is dropped');
|
||||
assert.ok(html.includes('npub180cvv07…yjh6w6'), 'the payload is shortened middle-out');
|
||||
});
|
||||
|
||||
test('control characters and bidi overrides are stripped', () => {
|
||||
const html = reviewBodyHtml('a\u0000bc \u202Eevil\u202C d\u200Be');
|
||||
assert.equal(html, 'abc evil de', 'controls, overrides and zero-widths gone');
|
||||
});
|
||||
|
||||
test('trailing sentence punctuation stays out of the href', () => {
|
||||
const html = reviewBodyHtml('read this (https://example.com/page).');
|
||||
assert.ok(html.includes('href="https://example.com/page"'), 'the parenthesis and dot are prose');
|
||||
assert.ok(html.includes('</a>).'), 'and stay visible after the link');
|
||||
});
|
||||
|
||||
test('short URLs are shown whole', () => {
|
||||
const html = reviewBodyHtml('https://mint.example.com');
|
||||
assert.ok(html.includes('>https://mint.example.com</a>'));
|
||||
});
|
||||
|
||||
test('escapeHtml escapes all five characters', () => {
|
||||
assert.equal(escapeHtml(`<a b="c" & 'd'>`), '<a b="c" & 'd'>');
|
||||
});
|
||||
Reference in New Issue
Block a user