ImageRenderer API
These are the declarations an extension implements against.
Requests and images
A RenderRequest is what the converter hands a renderer; the two image shapes are what it
may hand back. A URL is used as-is, bytes go through the uploader.
interface RenderRequest {
readonly lang: string;
readonly source: string;
readonly altText: string;
}
interface UrlImage {
readonly kind: "url";
readonly imageUrl: string;
readonly altText?: string;
}
interface BytesImage {
readonly kind: "bytes";
readonly data: Uint8Array;
readonly mimeType: "image/png" | "image/jpeg" | "image/gif";
readonly filename: string;
readonly altText?: string;
}Renderers
output narrows the return type, so a "url" renderer cannot accidentally return bytes.
type ImageRendererOutput = "url" | "bytes" | "both";
type RendererImage<T extends ImageRendererOutput> = T extends "url"
? UrlImage
: T extends "bytes"
? BytesImage
: UrlImage | BytesImage;
interface OperationContext {
readonly signal: AbortSignal | undefined;
readonly deadlineMs: number | undefined;
}
interface ImageRenderer<TOutput extends ImageRendererOutput = ImageRendererOutput> {
readonly id: string;
readonly output: TOutput;
canRender(request: RenderRequest): boolean;
render(request: RenderRequest, context: OperationContext): Promise<RendererImage<TOutput> | null>;
}
type UrlImageRenderer = ImageRenderer<"url">;
type BytesImageRenderer = ImageRenderer<"bytes">;
type BothImageRenderer = ImageRenderer<"both">;Renderer IDs match ^[a-z0-9][a-z0-9._-]{0,63}$ and are unique at converter
construction. Fenced languages are trimmed and lowercased; display math uses math.
canRender is immediate and must return an exact boolean. Thenables are invalid and are
observed only to prevent unhandled rejection. render runs later with the operation
context. Result alt text overrides the request default only when present.
Uploads and receipts
An uploader turns bytes into a Slack file. Every upload leaves a receipt, whether or not the conversion went on to succeed.
interface SlackUploader {
upload(
image: BytesImage,
context: OperationContext,
): Promise<{ readonly fileId: string; readonly receipt: UploadReceipt }>;
}
interface UploadReceipt {
readonly kind: "upload";
readonly provider: string;
readonly rendererId?: string;
readonly filename?: string;
readonly phase: "requested" | "allocated" | "uploaded" | "completed" | "shared" | "unknown";
readonly fileId?: string;
readonly shared: boolean;
readonly processing: "not-requested" | "pending" | "verified" | "unverified";
readonly status?: number;
readonly retryAfterMs?: number;
readonly providerCode?: string;
readonly retrySafety: "never" | "safe" | "may-duplicate";
}Uploader file IDs are trimmed and must match ^[A-Z][A-Z0-9]{1,254}$. A receipt file ID
must match the returned ID; an omitted receipt ID is enriched. Mismatch is a local
invalid-result failure with a conservative receipt.
Normative routing matrix
| Condition | Next/output | Degradation | Receipt / throw |
|---|---|---|---|
Mermaid pie / xychart-beta accepted natively | Native chart before image renderers | Capability/limit fallback only | No receipt |
canRender === false or render === null | Next renderer | None | None |
| Valid HTTPS URL | image_url block | None | No receipt; Slack fetches later |
| Valid bytes + uploader success | slack_file.id block | None | Immutable upload receipt |
| Ordinary renderer/upload failure | Next renderer, then readable source | Buffered; emitted only on terminal fallback | Upload effects remain receipts |
| Bytes without uploader | Later renderer may recover | None on recovery | Terminal ConfigurationError if no success |
| Invalid renderer descriptor | Stop at construction/configuration | None | ConfigurationError |
| Malformed runtime capability/result | Next renderer | Safe validation degradation on terminal fallback | Receipt retained if upload ran |
| Configuration, abort, deadline, shutdown | Stop conversion | Never a degradation | Typed top-level error with known receipts |
Core never fetches renderer URLs. It does validate every URL a renderer returns: absolute, HTTPS, no credentials, no control or surrounding whitespace, within Slack's 3000-character limit. A violation is rejected like any other renderer failure: the chain advances and records a degradation. Slack's later fetch can still fail and produces no conversion receipt.
Use createNodeConverter({ additionalRenderers }) for extension. Universal
slackmark.createConverter() is the complete-replacement API. Puppeteer-specific
renderers/runtime/assets remain in slackmark-node/renderers and
slackmark-node/puppeteer.