mirror of
https://github.com/Permissionless-Software-Foundation/psf-memo.git
synced 2026-09-21 16:52:01 -07:00
Format post URLs and bare domains as links
Render http(s) URLs and bare domains in post text as anchors that open in a new tab. Explicit URLs keep their scheme; bare domains get an https href while their visible text stays as written. Embeddable YouTube links keep their embedded-player behavior. By coder.
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
Unit tests for the post content renderer.
|
||||
|
||||
The renderer turns post text into static HTML: http(s) URLs and bare domains
|
||||
become anchors that open in a new tab, while embeddable YouTube links keep
|
||||
their embedded-player behavior.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const test = require('node:test')
|
||||
const assert = require('node:assert/strict')
|
||||
const React = require('react')
|
||||
const ReactDOMServer = require('react-dom/server')
|
||||
const PostContent = require('../../src/components/post-feed/post-content')
|
||||
|
||||
function render (text) {
|
||||
return ReactDOMServer.renderToStaticMarkup(React.createElement(PostContent, { text }))
|
||||
}
|
||||
|
||||
test('renders an http URL as an anchor that opens in a new tab', () => {
|
||||
const html = render('visit https://memo.fullstackcash.net for details')
|
||||
assert.match(html, /<a[^>]+href="https:\/\/memo\.fullstackcash\.net"/)
|
||||
assert.match(html, /<a[^>]+target="_blank"/)
|
||||
assert.match(html, /<a[^>]+rel="noopener noreferrer"/)
|
||||
})
|
||||
|
||||
test('renders an http URL with its original scheme', () => {
|
||||
const html = render('link http://example.com/path here')
|
||||
assert.match(html, /<a[^>]+href="http:\/\/example\.com\/path"/)
|
||||
})
|
||||
|
||||
test('renders a bare domain as an https anchor with unchanged visible text', () => {
|
||||
const html = render('go to www.example.com now')
|
||||
assert.match(html, /<a[^>]+href="https:\/\/www\.example\.com"/)
|
||||
assert.match(html, />www\.example\.com<\/a>/)
|
||||
})
|
||||
|
||||
test('renders no anchor for plain text', () => {
|
||||
const html = render('just a normal memo')
|
||||
assert.doesNotMatch(html, /<a[\s>]/)
|
||||
})
|
||||
|
||||
test('renders an embedded YouTube player and a separate link together', () => {
|
||||
const html = render('watch https://youtu.be/dQw4w9WgXcQ then read https://memo.fullstackcash.net')
|
||||
assert.match(html, /<iframe[^>]+src="https:\/\/www\.youtube\.com\/embed\/dQw4w9WgXcQ"/)
|
||||
assert.match(html, /<a[^>]+href="https:\/\/memo\.fullstackcash\.net"/)
|
||||
assert.doesNotMatch(html, /<a[^>]+href="https:\/\/youtu\.be\/dQw4w9WgXcQ"/)
|
||||
})
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
Unit tests for the post link parser.
|
||||
|
||||
The parser turns a post's text into text and link segments. A link segment
|
||||
is either an http(s) URL (scheme preserved) or a bare domain (linked with an
|
||||
https scheme while its visible text is left as written). Trailing sentence
|
||||
punctuation never becomes part of the link. Plain text and email addresses
|
||||
stay plain text.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const test = require('node:test')
|
||||
const assert = require('node:assert/strict')
|
||||
const { parsePostLinks } = require('../../src/services/post-links')
|
||||
|
||||
test('parsePostLinks returns a single text segment for plain text', () => {
|
||||
const text = 'just a normal memo'
|
||||
assert.deepEqual(parsePostLinks(text), [{ type: 'text', text }])
|
||||
})
|
||||
|
||||
test('parsePostLinks returns an empty text segment for an empty string', () => {
|
||||
assert.deepEqual(parsePostLinks(''), [{ type: 'text', text: '' }])
|
||||
})
|
||||
|
||||
test('parsePostLinks links an https URL and preserves its scheme', () => {
|
||||
assert.deepEqual(parsePostLinks('visit https://memo.fullstackcash.net for details'), [
|
||||
{ type: 'text', text: 'visit ' },
|
||||
{ type: 'link', href: 'https://memo.fullstackcash.net', text: 'https://memo.fullstackcash.net' },
|
||||
{ type: 'text', text: ' for details' }
|
||||
])
|
||||
})
|
||||
|
||||
test('parsePostLinks links an http URL and preserves its scheme', () => {
|
||||
assert.deepEqual(parsePostLinks('link http://example.com/path here'), [
|
||||
{ type: 'text', text: 'link ' },
|
||||
{ type: 'link', href: 'http://example.com/path', text: 'http://example.com/path' },
|
||||
{ type: 'text', text: ' here' }
|
||||
])
|
||||
})
|
||||
|
||||
test('parsePostLinks keeps trailing punctuation out of the link', () => {
|
||||
assert.deepEqual(parsePostLinks('read https://memo.fullstackcash.net, then reply'), [
|
||||
{ type: 'text', text: 'read ' },
|
||||
{ type: 'link', href: 'https://memo.fullstackcash.net', text: 'https://memo.fullstackcash.net' },
|
||||
{ type: 'text', text: ', then reply' }
|
||||
])
|
||||
})
|
||||
|
||||
test('parsePostLinks links a bare domain with an https href and unchanged visible text', () => {
|
||||
assert.deepEqual(parsePostLinks('visit memo.fullstackcash.net for details'), [
|
||||
{ type: 'text', text: 'visit ' },
|
||||
{ type: 'link', href: 'https://memo.fullstackcash.net', text: 'memo.fullstackcash.net' },
|
||||
{ type: 'text', text: ' for details' }
|
||||
])
|
||||
})
|
||||
|
||||
test('parsePostLinks links a bare domain with a path', () => {
|
||||
assert.deepEqual(parsePostLinks('see memo.fullstackcash.net/feed now'), [
|
||||
{ type: 'text', text: 'see ' },
|
||||
{ type: 'link', href: 'https://memo.fullstackcash.net/feed', text: 'memo.fullstackcash.net/feed' },
|
||||
{ type: 'text', text: ' now' }
|
||||
])
|
||||
})
|
||||
|
||||
test('parsePostLinks links a bare www domain', () => {
|
||||
assert.deepEqual(parsePostLinks('go to www.example.com now'), [
|
||||
{ type: 'text', text: 'go to ' },
|
||||
{ type: 'link', href: 'https://www.example.com', text: 'www.example.com' },
|
||||
{ type: 'text', text: ' now' }
|
||||
])
|
||||
})
|
||||
|
||||
test('parsePostLinks keeps trailing punctuation out of a bare-domain link', () => {
|
||||
assert.deepEqual(parsePostLinks('visit memo.fullstackcash.net, it is live'), [
|
||||
{ type: 'text', text: 'visit ' },
|
||||
{ type: 'link', href: 'https://memo.fullstackcash.net', text: 'memo.fullstackcash.net' },
|
||||
{ type: 'text', text: ', it is live' }
|
||||
])
|
||||
})
|
||||
|
||||
test('parsePostLinks handles multiple links in one post', () => {
|
||||
assert.deepEqual(
|
||||
parsePostLinks('watch https://youtu.be/dQw4w9WgXcQ then read https://memo.fullstackcash.net'),
|
||||
[
|
||||
{ type: 'text', text: 'watch ' },
|
||||
{ type: 'link', href: 'https://youtu.be/dQw4w9WgXcQ', text: 'https://youtu.be/dQw4w9WgXcQ' },
|
||||
{ type: 'text', text: ' then read ' },
|
||||
{ type: 'link', href: 'https://memo.fullstackcash.net', text: 'https://memo.fullstackcash.net' }
|
||||
]
|
||||
)
|
||||
})
|
||||
|
||||
test('parsePostLinks does not link an email address', () => {
|
||||
const text = 'write to me at chris@example.com please'
|
||||
assert.deepEqual(parsePostLinks(text), [{ type: 'text', text }])
|
||||
})
|
||||
|
||||
test('parsePostLinks does not link a dotted user name before an @ sign', () => {
|
||||
const text = 'write to first.last@example.com please'
|
||||
assert.deepEqual(parsePostLinks(text), [{ type: 'text', text }])
|
||||
})
|
||||
|
||||
test('parsePostLinks round-trips: segments reconstruct the original text', () => {
|
||||
const samples = [
|
||||
'just a normal memo',
|
||||
'visit memo.fullstackcash.net for details',
|
||||
'read https://memo.fullstackcash.net, then reply',
|
||||
'go to www.example.com now',
|
||||
'watch https://youtu.be/dQw4w9WgXcQ then read https://memo.fullstackcash.net',
|
||||
'write to chris@example.com please',
|
||||
''
|
||||
]
|
||||
for (const text of samples) {
|
||||
const rebuilt = parsePostLinks(text)
|
||||
.map((segment) => segment.text)
|
||||
.join('')
|
||||
assert.equal(rebuilt, text)
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user