jacquardSnapshot

← snapshot

1648 bytes
/**
 * Client-side mirrors of the Rust validation rules. These never replace the
 * server's checks — jac-serve re-validates everything — they only let the
 * form say no before the network does.
 */

/** Mirror of `Statement::new`: trimmed, 1..=2000 characters. */
export function statementStatus(text: string): {
  trimmedLength: number;
  valid: boolean;
  reason?: string;
} {
  const trimmed = text.trim();
  // Count Unicode scalar values like Rust's chars().count(), not UTF-16 units.
  const trimmedLength = [...trimmed].length;
  if (trimmedLength === 0) {
    return { trimmedLength, valid: false, reason: "statement is empty once trimmed" };
  }
  if (trimmedLength > 2000) {
    return {
      trimmedLength,
      valid: false,
      reason: `over by ${trimmedLength - 2000}`,
    };
  }
  return { trimmedLength, valid: true };
}

/** Mirror of `RefName::parse`. */
export function isValidRefName(s: string): boolean {
  if (s.length === 0 || s.startsWith("/") || s.endsWith("/")) return false;
  return s.split("/").every((seg) => seg.length > 0 && seg !== "." && seg !== "..");
}

/** Mirror of `RepoPath::parse`. */
export function isValidRepoPath(s: string): boolean {
  return isValidRefName(s);
}

/** Mirror of jac-serve's slugify, for the live slug preview. */
export function slugify(name: string): string {
  let slug = "";
  let lastDash = true;
  for (const c of name.trim()) {
    if (/[a-zA-Z0-9]/.test(c)) {
      slug += c.toLowerCase();
      lastDash = false;
    } else if (!lastDash) {
      slug += "-";
      lastDash = true;
    }
  }
  while (slug.endsWith("-")) slug = slug.slice(0, -1);
  return slug;
}