A calm place to write long-form, and publish it to the open social web. skypress.blog/
0

Configure Feed

Select the types of activity you want to include in your feed.

1import { describe, expect, it } from 'vitest'; 2import { renderArticle } from './render-article'; 3 4const AUTHOR = { pdsUrl: 'https://pds.example', did: 'did:plc:alice' }; 5 6describe( 'renderArticle', () => { 7 it( 'renders block content to HTML', () => { 8 const { html } = renderArticle( 9 { 10 content: { 11 blocks: [ 12 { name: 'core/heading', attributes: { level: 2, content: 'Section' } }, 13 { name: 'core/paragraph', attributes: { content: 'A paragraph.' } }, 14 ], 15 }, 16 }, 17 AUTHOR 18 ); 19 expect( html ).toContain( '<h2 class="wp-block-heading">Section</h2>' ); 20 expect( html ).toContain( '<p>A paragraph.</p>' ); 21 } ); 22 23 it( 'never lets a script in block content survive (sanitise is the last step)', () => { 24 const { html } = renderArticle( 25 { 26 content: { 27 blocks: [ 28 { 29 name: 'core/paragraph', 30 attributes: { content: 'Hello <script>alert(1)</script>world' }, 31 }, 32 ], 33 }, 34 }, 35 AUTHOR 36 ); 37 expect( html ).not.toContain( '<script' ); 38 expect( html ).not.toContain( 'alert(1)' ); 39 expect( html ).toContain( 'Hello' ); 40 } ); 41 42 it( 'strips event-handler attributes smuggled through rich text', () => { 43 const { html } = renderArticle( 44 { 45 content: { 46 blocks: [ 47 { 48 name: 'core/paragraph', 49 attributes: { content: '<em onmouseover="steal()">hover me</em>' }, 50 }, 51 ], 52 }, 53 }, 54 AUTHOR 55 ); 56 expect( html ).toContain( '<em>hover me</em>' ); 57 expect( html ).not.toContain( 'onmouseover' ); 58 } ); 59 60 it( 'rewrites blob-backed image URLs to the author PDS before rendering', () => { 61 const { html } = renderArticle( 62 { 63 content: { 64 blocks: [ 65 { 66 name: 'core/image', 67 attributes: { 68 url: 'https://stale-pds.example/old-blob.png', 69 alt: 'A photo', 70 skypressBlob: { 71 $type: 'blob', 72 ref: { $link: 'bafyreidcid' }, 73 mimeType: 'image/png', 74 size: 1234, 75 }, 76 }, 77 }, 78 ], 79 }, 80 }, 81 AUTHOR 82 ); 83 // `&amp;` (not `&`) — the URL came out the far side of the sanitiser. 84 expect( html ).toContain( 85 'src="https://pds.example/xrpc/com.atproto.sync.getBlob?did=did%3Aplc%3Aalice&amp;cid=bafyreidcid"' 86 ); 87 expect( html ).not.toContain( 'stale-pds.example' ); 88 } ); 89 90 it( 'prefers the stored textContent for the plain-text view', () => { 91 const { text } = renderArticle( 92 { 93 textContent: 'Stored summary.', 94 content: { 95 blocks: [ { name: 'core/paragraph', attributes: { content: 'Body.' } } ], 96 }, 97 }, 98 AUTHOR 99 ); 100 expect( text ).toBe( 'Stored summary.' ); 101 } ); 102 103 it( 'falls back to the block text when textContent is absent or empty', () => { 104 const { text } = renderArticle( 105 { 106 textContent: '', 107 content: { 108 blocks: [ 109 { name: 'core/heading', attributes: { level: 2, content: 'Section' } }, 110 { name: 'core/paragraph', attributes: { content: 'Body text.' } }, 111 ], 112 }, 113 }, 114 AUTHOR 115 ); 116 expect( text ).toBe( 'Section\n\nBody text.' ); 117 } ); 118 119 it( 'returns empty html and text for a document with no blocks', () => { 120 const { html, text } = renderArticle( {}, AUTHOR ); 121 expect( html ).toBe( '' ); 122 expect( text ).toBe( '' ); 123 } ); 124 125 it( 'highlights code blocks only when opted in', () => { 126 const doc = { 127 content: { 128 blocks: [ { name: 'core/code', attributes: { content: 'const answer = 42;' } } ], 129 }, 130 }; 131 const off = renderArticle( doc, AUTHOR ); 132 const on = renderArticle( doc, AUTHOR, { highlight: true } ); 133 134 // Default (RSS path) stays plain. 135 expect( off.html ).toContain( '<pre class="wp-block-code"><code>' ); 136 expect( off.html ).not.toContain( 'hljs' ); 137 138 // Opt-in produces sanitiser-safe token spans. 139 expect( on.html ).toContain( 'class="hljs' ); 140 expect( on.html ).toContain( '<span class="hljs-' ); 141 } ); 142} );