Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions src/hooks/useRendererRegistry.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { BlockRenderer, RendererRegistry } from '@/types.js'
import { resolveRendererFromRegistry } from './useRendererRegistry.js'

const block = {} as Parameters<BlockRenderer>[0]['block']

const makeRenderer = (
name: string,
options: {
canRender?: boolean
priority?: number
} = {},
): BlockRenderer => {
const renderer = (() => null) as BlockRenderer
renderer.displayName = name
if (options.canRender !== undefined) {
renderer.canRender = () => options.canRender ?? false
}
if (options.priority !== undefined) {
renderer.priority = () => options.priority ?? 0
}
return renderer
}

describe('resolveRendererFromRegistry', () => {
afterEach(() => {
vi.restoreAllMocks()
})

it('returns a registered renderer override without warning', () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
const selected = makeRenderer('selected')
const fallback = makeRenderer('fallback', {canRender: true, priority: 10})
const registry: RendererRegistry = {
default: makeRenderer('default'),
fallback,
selected,
}

const resolved = resolveRendererFromRegistry({
block,
registry,
rendererKey: 'selected',
})

expect(resolved).toBe(selected)
expect(warn).not.toHaveBeenCalled()
})

it('warns and falls through when a renderer override is not registered', () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
const lowPriority = makeRenderer('low', {canRender: true, priority: 1})
const highPriority = makeRenderer('high', {canRender: true, priority: 10})
const registry: RendererRegistry = {
default: makeRenderer('default'),
highPriority,
lowPriority,
}

const resolved = resolveRendererFromRegistry({
block,
registry,
rendererKey: 'misspelled',
})

expect(resolved).toBe(highPriority)
expect(warn).toHaveBeenCalledWith(expect.stringContaining('misspelled'))
expect(warn).toHaveBeenCalledWith(expect.stringContaining('default, highPriority, lowPriority'))
})
})
50 changes: 38 additions & 12 deletions src/hooks/useRendererRegistry.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BlockRendererProps } from '../types'
import type { BlockRendererProps, RendererRegistry } from '../types'
import { rendererProp } from '@/data/properties.js'
import { usePropertyValue, useData } from '@/hooks/block.js'
import { blockRenderersFacet } from '@/extensions/core.js'
Expand All @@ -11,6 +11,42 @@ export const refreshRendererRegistry = async () => {
refreshAppRuntime()
}

interface ResolveRendererOptions extends BlockRendererProps {
registry: RendererRegistry
rendererKey?: string
}

export const resolveRendererFromRegistry = ({
block,
context,
registry,
rendererKey,
}: ResolveRendererOptions) => {
if (rendererKey) {
const requestedRenderer = registry[rendererKey]
if (requestedRenderer) {
return requestedRenderer
}

const availableRenderers = Object.keys(registry).sort()
const availableMessage = availableRenderers.length > 0
? availableRenderers.join(', ')
: '(none)'
console.warn(
`[useRenderer] renderer "${rendererKey}" is not registered; ` +
`falling back to renderer predicates. Available renderers: ${availableMessage}`,
)
}

const possibleRenderers = Object.values(registry)
.filter(renderer => renderer.canRender?.({block, context}))

const firstPriority = possibleRenderers.sort((a, b) =>
(b.priority?.({block, context}) || 0) - (a.priority?.({block, context}) || 0))[0]

return firstPriority ?? registry.default
}

export const useRenderer = ({block, context}: BlockRendererProps) => {
'use no memo'
useData(block)
Expand All @@ -23,21 +59,11 @@ export const useRenderer = ({block, context}: BlockRendererProps) => {
const runtime = useAppRuntime()
const registry = runtime.read(blockRenderersFacet)

if (rendererKey && registry[rendererKey]) {
return registry[rendererKey]
}

/**
* todo, caching of renderer for each block?
* maybe do per/type?
* also allowing people to switch between renderers would be good
*/

const possibleRenderers = Object.values(registry)
.filter(renderer => renderer.canRender?.({block, context}))

const firstPriority = possibleRenderers.sort((a, b) =>
(b.priority?.({block, context}) || 0) - (a.priority?.({block, context}) || 0))[0]

return firstPriority ?? registry.default
return resolveRendererFromRegistry({block, context, registry, rendererKey})
}