Skip to the content

Capsid menu

components/media/media-record.ts

115 lines and 7 functions in DrDustinEdwards/capsomer, at commit d3966cc on 2026-10-24. This file on GitHub.

Functions

The functions of media-record.ts
FunctionLinesComplexityCoveredCRAPMutation
byteSize55 to 593No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
suggestedAlt65 to 701No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
altStateOf74 to 773No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
parseTags79 to 864No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
flagsFor93 to 10111No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
describeRecord104 to 1097No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet
copySnippets113 to 1282No data: no coverage report in this mapNo data: no coverage report in this mapNo data: mutation testing has not run on this repo yet

Imports

It imports no other module of this repo. Imported by components/media/media-inspector.ts, components/media/media-tile.ts, components/media/media.ts.

Source

// The media library's records: the facts of one file and the plain functions that read them.

// ---------------------------------------------------------------------------------------
// The records, pure.

export type AltState = "missing" | "decorative" | "set";
export type TileState = "ready" | "uploading" | "failed" | "binned";

export interface UsedIn {
  title: string;
  href: string;
  // How the post uses it: "image in the body", "cover".
  how?: string;
}

export interface MediaRecord {
  key: string;
  name: string;
  url: string;
  thumb?: string;
  kind: "image" | "document";
  type: string;
  bytes: number;
  width?: number;
  height?: number;
  uploaded?: string;
  alt: string;
  altState: AltState;
  title: string;
  caption: string;
  tags: string[];
  used: UsedIn[];
  state: TileState;
  suggestedTags?: string[];
  progress?: number;
  error?: string;
  // The site's version of the file's metadata (site-api v0.4): a form posts it back as the
  // version it last saw.
  version?: string;
  // Where it is used was not read (a list of files has no usedBy; the inspected one does): no
  // Unattached flag, and the inspector says it was not checked.
  usedUnknown?: boolean;
}

// What the inspector edits.
export interface MediaFields {
  alt: string;
  decorative: boolean;
  title: string;
  caption: string;
  tags: string[];
}

// Decimal unit names, binary division: the site's own rule, so a size reads the same on both.
export function byteSize(size: number): string {
  if (size < 1024) return `${size} B`;
  if (size < 1024 * 1024) return `${Math.round(size / 1024)} kB`;
  return `${(size / (1024 * 1024)).toFixed(1)} MB`;
}

export const LARGE_BYTES = 1024 * 1024;

// "foxhound-hero.jpg" gives "foxhound hero": offered as alt text, never applied (alt text nobody
// read is worse than a visibly empty field).
export function suggestedAlt(name: string): string {
  return name
    .replace(/\.[a-z0-9]+$/i, "")
    .replace(/[-_]+/g, " ")
    .trim();
}

// The state of a file's alt text. Missing and decorative are different: missing is a to-do,
// decorative is a decision the author made.
export function altStateOf(alt: string, decorative: boolean): AltState {
  if (decorative) return "decorative";
  return alt.trim() ? "set" : "missing";
}

export function parseTags(text: string): string[] {
  const out: string[] = [];
  for (const raw of text.split(",")) {
    const t = raw.trim().toLowerCase();
    if (t && !out.includes(t)) out.push(t);
  }
  return out;
}

// What a tile says about the file besides its name: the things a person should not miss.
export interface Flag {
  tone: "warn" | "info" | "nodata";
  word: string;
}
export function flagsFor(r: Pick<MediaRecord, "kind" | "altState" | "used" | "state" | "usedUnknown">): Flag[] {
  const out: Flag[] = [];
  if (r.state === "binned") out.push({ tone: "nodata", word: "In Trash" });
  if (r.state === "uploading" || r.state === "failed") return out;
  if (r.kind === "image" && r.altState === "missing") out.push({ tone: "warn", word: "No alt text" });
  if (r.kind === "image" && r.altState === "decorative") out.push({ tone: "info", word: "Decorative" });
  if (r.state !== "binned" && !r.usedUnknown && r.used.length === 0) out.push({ tone: "nodata", word: "Unattached" });
  return out;
}

// What a tile's controls are called, in one line a screen reader reads after the file's name.
export function describeRecord(r: Pick<MediaRecord, "kind" | "type" | "bytes" | "state" | "progress" | "error">): string {
  const kind = r.type.split("/").pop()?.toUpperCase() || (r.kind === "image" ? "Image" : "File");
  if (r.state === "uploading") return `${kind}, uploading${r.progress != null ? ` ${Math.round(r.progress)} percent` : ""}`;
  if (r.state === "failed") return `${kind}, upload failed${r.error ? `: ${r.error}` : ""}`;
  return `${kind}, ${byteSize(r.bytes)}`;
}

// The three things a person copies: the address, the Markdown, the HTML. An image goes in as an
// image and a document as a link: an <img> for a PDF breaks the page.
export function copySnippets(r: Pick<MediaRecord, "url" | "kind" | "alt" | "name">): Array<{ id: string; label: string; name: string; value: string }> {
  const alt = r.alt.trim();
  const title = suggestedAlt(r.name);
  if (r.kind === "image") {
    return [
      { id: "address", label: "Copy address", name: "Copy address", value: r.url },
      { id: "markdown", label: "Markdown", name: "Copy the Markdown image", value: `![${alt}](${r.url})` },
      { id: "html", label: "HTML tag", name: "Copy the HTML tag", value: `<img src="${r.url}" alt="${alt}">` },
    ];
  }
  return [
    { id: "address", label: "Copy address", name: "Copy address", value: r.url },
    { id: "markdown", label: "Markdown", name: "Copy the Markdown link", value: `[${title}](${r.url})` },
    { id: "html", label: "HTML link", name: "Copy the HTML link", value: `<a href="${r.url}">${title}</a>` },
  ];
}