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

ConditionNext/outputDegradationReceipt / throw
Mermaid pie / xychart-beta accepted nativelyNative chart before image renderersCapability/limit fallback onlyNo receipt
canRender === false or render === nullNext rendererNoneNone
Valid HTTPS URLimage_url blockNoneNo receipt; Slack fetches later
Valid bytes + uploader successslack_file.id blockNoneImmutable upload receipt
Ordinary renderer/upload failureNext renderer, then readable sourceBuffered; emitted only on terminal fallbackUpload effects remain receipts
Bytes without uploaderLater renderer may recoverNone on recoveryTerminal ConfigurationError if no success
Invalid renderer descriptorStop at construction/configurationNoneConfigurationError
Malformed runtime capability/resultNext rendererSafe validation degradation on terminal fallbackReceipt retained if upload ran
Configuration, abort, deadline, shutdownStop conversionNever a degradationTyped 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.