OverviewFilesCommitsIssuesDocsReleasesPeers

favela-markdown commits / a234450

0.1.2: embeds() — which files a record references
The renderer needs a type to choose <img> or <video>, and a type comes from the daemon, so a surface has to know which oids a record references BEFORE it renders. ⛔ Uses the SAME parse `render` does, never a regular expression. A regex matches a blob URI inside a code fence or an escaped bracket and reports a reference the renderer will never make — the extractor and the renderer disagreeing about what a record references is how a surface fetches the wrong files. Pinned by a test. ⚠ `textOf` only walks a paragraph, so a link label came back empty through it; `labelOf` walks the node's own children instead. 63/63, 2389/2389. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
a2344501e3301cb5c61cb6f1a409011a3bd3fe26
by plankatron <fleeky@gmail.com> · 2026-09-07T01:24:30.000Z

Changed files (3) +59 -2

lib/render.js +39 -0
……@@ -191,3 +191,42 @@ export function render (source, opts = {}) {
191191}
192192
193193export default render
194
195/**
196 * embeds(source) -> [{ oid, alt, kind }] — every `favela://blob/<oid>` a record references.
197 *
198 * ⛔ Uses the SAME parse `render` does, not a regular expression. A regex would match a blob URI
199 * inside a code fence or an escaped bracket and report a reference the renderer will never make —
200 * and the two disagreeing about what a record references is how a surface fetches the wrong files.
201 *
202 * `kind` is the authoring gesture: `image` for `![…]()`, `link` for `[…]()`. Source order, because
203 * §3.5 has the embed pipeline consuming them in it.
204 *
205 * TOTAL — never throws; an unparseable record has no embeds.
206 */
207const labelOf = (n) => (n.children || []).map((c) => c.value || labelOf(c)).join('')
208
209export function embeds (source) {
210 const src = String(source == null ? '' : source).replace(/\r\n?/g, '\n')
211 let tree
212 try { tree = fromMarkdown(src, MICROMARK) } catch { return [] }
213
214 const out = []
215 const walk = (node) => {
216 if (!node || typeof node !== 'object') return
217 if (node.type === 'image' || node.type === 'link') {
218 const oid = blobOid(node.url || '')
219 if (oid) {
220 out.push({
221 oid,
222 // ⛔ Not `textOf`, which only walks a paragraph — a link's label is its own children.
223 alt: node.type === 'image' ? (node.alt || '') : labelOf(node),
224 kind: node.type === 'image' ? 'image' : 'link'
225 })
226 }
227 }
228 for (const c of node.children || []) walk(c)
229 }
230 try { walk(tree) } catch { return out }
231 return out
232}
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "favela-markdown",
3 "version": "0.1.1",
3 "version": "0.1.2",
44 "description": "The P2P-native markdown record layer for favela. A post is a markdown document; atproto and ActivityPub are projections derived from it on read.",
55 "type": "module",
66 "license": "MIT",
test/render.test.js +19 -1
……@@ -8,7 +8,7 @@
88// same fail-closed posture reach.js has for privacy, applied to markup.
99
1010import test from 'brittle'
11import { render } from '../lib/render.js'
11import { render, embeds } from '../lib/render.js'
1212
1313test('basic formatting survives, unlike in strip()', (t) => {
1414 t.is(render('hello **world**'), '<p>hello <strong>world</strong></p>')
……@@ -154,3 +154,21 @@ test('⛔ the author-URL allowlist is UNCHANGED by any of this', (t) => {
154154 t.absent(render('[x](javascript:alert(1))', o).includes('href'), 'and links too')
155155 t.ok(render('![x](https://example.com/a.png)', o).includes('<img'), 'http still works')
156156})
157
158test('★ embeds() reports every blob a record references, in source order', (t) => {
159 const A = 'a1'.repeat(16); const B = 'b2'.repeat(16)
160 const md = '![a cat](favela://blob/' + A + ')\n\nand [the report](favela://blob/' + B + ')'
161 const got = embeds(md)
162 t.is(got.length, 2)
163 t.alike(got[0], { oid: A, alt: 'a cat', kind: 'image' })
164 t.alike(got[1], { oid: B, alt: 'the report', kind: 'link' }, 'a link label is its own children')
165})
166
167test('⛔ embeds() uses the PARSE, so a blob URI inside a code fence is not a reference', (t) => {
168 // A regex would report this and the surface would fetch a file the renderer never shows — the
169 // two disagreeing about what a record references is exactly the bug worth preventing.
170 const md = '```\n![x](favela://blob/' + 'c3'.repeat(16) + ')\n```'
171 t.alike(embeds(md), [], 'code is not a reference')
172 t.alike(embeds('no embeds here'), [])
173 t.alike(embeds(null), [], 'total — never throws')
174})