/** Repository topics: short, lowercase, hyphenated tags an admin sets for discovery (GitHub's rules). */
export const MAX_TOPICS = 20;
export const MAX_TOPIC_LENGTH = 50;
const TOPIC = /^[a-z0-9][a-z0-9-]*$/;
export class TopicError extends Error {}
/**
* Normalizes user input (an array, or one string split on commas and whitespace): lowercased, spaces and
* underscores become hyphens, duplicates dropped. Throws TopicError naming the first invalid topic.
*/
export function normalizeTopics(input: string | readonly string[]): string[] {
const raw = typeof input === "string" ? input.split(/[\s,]+/) : input;
const out: string[] = [];
for (const value of raw) {
const topic = value
.trim()
.toLowerCase()
.replace(/[\s_]+/g, "-");
if (!topic) continue;
if (topic.length > MAX_TOPIC_LENGTH) throw new TopicError(`Topics are at most ${MAX_TOPIC_LENGTH} characters`);
if (!TOPIC.test(topic)) {
throw new TopicError(
`"${topic}": topics use lowercase letters, numbers and hyphens, and start with a letter or number`,
);
}
if (!out.includes(topic)) out.push(topic);
}
if (out.length > MAX_TOPICS) throw new TopicError(`A repository has at most ${MAX_TOPICS} topics`);
return out;
}
/** The stored JSON column, tolerating anything malformed (it was written by normalizeTopics). */
export function parseTopics(stored: string | null | undefined): string[] {
if (!stored) return [];
try {
const parsed: unknown = JSON.parse(stored);
return Array.isArray(parsed) ? parsed.filter((topic): topic is string => typeof topic === "string") : [];
} catch {
return [];