#!/usr/bin/env bun // Generate docs/SHARP.md from sidecar/sharp/vocabulary.ts. // // The vocabulary table is what the validator enforces. Writing the reference by hand would mean two // descriptions of one thing, and the doc would quietly stop being true the first time a bound // changed. This makes the drift structurally impossible instead of a discipline problem. // // Run: bun run docs import { INPUT_OPTIONS, OPERATIONS, OUTPUT_FORMATS, type Param } from "../sidecar/sharp/vocabulary"; import { resolve, dirname } from "node:path"; import { fileURLToPath } from "node:url"; const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."); function describeParam(spec: Param): string { const base = (() => { switch (spec.kind) { case "int": return `integer, ${spec.min}–${spec.max}`; case "number": return `number, ${spec.min}–${spec.max}`; case "bool": return "boolean"; case "enum": return spec.values.map((v) => `\`${v}\``).join(" · "); case "color": return "colour — a CSS string like `#ffffff`, or `{r, g, b, alpha}`"; case "numberArray": return `array of ${spec.minLength === spec.maxLength ? spec.minLength : `${spec.minLength}–${spec.maxLength}`} numbers, each ${spec.min}–${spec.max}`; case "assetId": return "**asset id** (never a path)"; case "string": return `string, up to ${spec.maxLength} chars`; } })(); return spec.note ? `${base}. ${spec.note}` : base; } function paramTable(params: Record): string { const keys = Object.keys(params); if (!keys.length) return "_Takes no options._\n"; return [ "| Parameter | Accepts |", "|---|---|", ...keys.map((key) => `| \`${key}\` | ${describeParam(params[key]!)} |`), "", ].join("\n"); } const lines: string[] = [ "# sharp — the operation vocabulary", "", "**Generated from `sidecar/sharp/vocabulary.ts` by `scripts/gen-docs.ts`. Do not edit by hand —", "run `bun run docs`.**", "", "Every operation is named what sharp names it and takes sharp's own arguments, so", "[sharp's documentation](https://sharp.pixelplumbing.com/api-operation) describes what each one", "*does*. This page describes what av-club *accepts*: the bounds, which exist because an unbounded", "number here is a denial of service rather than a picture.", "", "Requests go to `POST /v1/sharp` — see [API.md](API.md) for the envelope.", "", "```json", "{", ' "input": "as_9f86d081884c7d659a2feaa0c55ad015",', ' "options": { "failOn": "warning" },', ' "pipeline": [', ' { "rotate": {} },', ' { "resize": { "width": 1200, "fit": "inside", "withoutEnlargement": true } }', " ],", ' "output": { "format": "avif", "options": { "quality": 55 } }', "}", "```", "", "## Three things that catch people", "", "**`rotate` with no angle applies the EXIF orientation.** Output is re-encoded and metadata is", "dropped, so a photo that looked upright in a phone's gallery arrives sideways on the web unless you", "include `{ \"rotate\": {} }`. This is the most commonly forgotten operation.", "", "**`resize` enlarges by default.** A 300px source asked for 800px comes back 800px and soft. Pass", "`withoutEnlargement: true` unless you actually want that.", "", "**Metadata is always stripped and there is no way to keep it.** `withMetadata` is deliberately not", "in this vocabulary — it is how a client's GPS coordinates end up published. Dimensions come back in", "the response instead.", "", "## Input options", "", "sharp's constructor options, passed as `options`.", "", paramTable(INPUT_OPTIONS), "`limitInputPixels` is **not** accepted: it is the decompression-bomb guard and is set by the", "server. A 1 KB PNG can expand to tens of gigabytes.", "", "## Operations", "", `${Object.keys(OPERATIONS).length} operations, run in the order given.`, "", ]; for (const [name, spec] of Object.entries(OPERATIONS)) { lines.push(`### \`${name}\``, "", spec.describe, ""); if (spec.positional) { lines.push( `Takes \`${spec.positional}\` directly — \`{ "${name}": { "${spec.positional}": … } }\`.`, "", ); } lines.push(paramTable(spec.params)); } lines.push( "## Output formats", "", "Given as `output.format`, with `output.options` carrying the encoder settings. Defaults to `webp`.", "", ); for (const [name, spec] of Object.entries(OUTPUT_FORMATS)) { lines.push(`### \`${name}\``, "", spec.describe, "", paramTable(spec.params)); } const path = resolve(ROOT, "docs/SHARP.md"); await Bun.write(path, lines.join("\n")); console.log( `[gen-docs] docs/SHARP.md — ${Object.keys(OPERATIONS).length} operations, ${Object.keys(OUTPUT_FORMATS).length} output formats`, );