| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| export const SPACE_OWNER = 'build-small-hackathon'; |
|
|
| |
| export function rawReadmeUrl(space: string): string { |
| return `https://huggingface.co/spaces/${SPACE_OWNER}/${encodeURIComponent(space)}/raw/main/README.md`; |
| } |
|
|
| |
| |
| |
| |
| export function normalizeSpaceName(input: string): string { |
| let s = input.trim(); |
| |
| s = s.replace(/^https?:\/\/huggingface\.co\//i, ''); |
| s = s.replace(/^\/+/, ''); |
| s = s.replace(/^spaces\//i, ''); |
| s = s.replace(new RegExp(`^${SPACE_OWNER}/`, 'i'), ''); |
| |
| s = s.split('/')[0]; |
| return s.trim(); |
| } |
|
|
| |
| |
| |
| export const MANAGED_PREFIXES = ['track:', 'sponsor:', 'achievement:'] as const; |
| export const isManagedTag = (t: string) => MANAGED_PREFIXES.some((p) => t.startsWith(p)); |
| export const trackTag = (id: string) => `track:${id}`; |
| export const sponsorTag = (id: string) => `sponsor:${id}`; |
| export const achievementTag = (id: string) => `achievement:${id}`; |
|
|
| export interface ParsedReadme { |
| hasFrontmatter: boolean; |
| |
| frontmatter: string; |
| |
| body: string; |
| |
| tags: string[]; |
| |
| tagsIndent: string; |
| |
| tagsRange: { start: number; end: number } | null; |
| } |
|
|
| const FRONTMATTER_RE = /^?---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n?/; |
|
|
| const unquote = (s: string) => s.trim().replace(/^['"]|['"]$/g, ''); |
|
|
| |
| function findTagsBlock( |
| fm: string |
| ): { tags: string[]; indent: string; start: number; end: number } | null { |
| const lines = fm.split('\n'); |
| const offsets: number[] = []; |
| let acc = 0; |
| for (const l of lines) { |
| offsets.push(acc); |
| acc += l.length + 1; |
| } |
|
|
| for (let i = 0; i < lines.length; i++) { |
| const m = /^tags:[ \t]*(.*)$/.exec(lines[i]); |
| if (!m) continue; |
| const start = offsets[i]; |
| const inline = m[1].trim(); |
|
|
| |
| if (inline.startsWith('[')) { |
| const tags = inline |
| .replace(/^\[|\]$/g, '') |
| .split(',') |
| .map(unquote) |
| .filter(Boolean); |
| return { tags, indent: ' ', start, end: offsets[i] + lines[i].length }; |
| } |
|
|
| |
| if (inline) { |
| return { tags: [unquote(inline)], indent: ' ', start, end: offsets[i] + lines[i].length }; |
| } |
|
|
| |
| |
| |
| const tags: string[] = []; |
| let indent = ' '; |
| let last = i; |
| for (let j = i + 1; j < lines.length; j++) { |
| const lm = /^([ \t]*)-[ \t]+(.*)$/.exec(lines[j]); |
| if (!lm) break; |
| if (tags.length === 0) indent = lm[1]; |
| tags.push(unquote(lm[2])); |
| last = j; |
| } |
| return { tags, indent, start, end: offsets[last] + lines[last].length }; |
| } |
| return null; |
| } |
|
|
| export function parseReadme(raw: string): ParsedReadme { |
| const m = FRONTMATTER_RE.exec(raw); |
| if (!m) { |
| return { |
| hasFrontmatter: false, |
| frontmatter: '', |
| body: raw, |
| tags: [], |
| tagsIndent: ' ', |
| tagsRange: null |
| }; |
| } |
| const frontmatter = m[1]; |
| const body = raw.slice(m[0].length); |
| const block = findTagsBlock(frontmatter); |
| return { |
| hasFrontmatter: true, |
| frontmatter, |
| body, |
| tags: block?.tags ?? [], |
| tagsIndent: block?.indent ?? ' ', |
| tagsRange: block ? { start: block.start, end: block.end } : null |
| }; |
| } |
|
|
| |
| export function splitManaged(tags: string[]): { |
| tracks: Set<string>; |
| sponsors: Set<string>; |
| achievements: Set<string>; |
| } { |
| const tracks = new Set<string>(); |
| const sponsors = new Set<string>(); |
| const achievements = new Set<string>(); |
| for (const t of tags) { |
| if (t.startsWith('track:')) tracks.add(t.slice('track:'.length)); |
| else if (t.startsWith('sponsor:')) sponsors.add(t.slice('sponsor:'.length)); |
| else if (t.startsWith('achievement:')) achievements.add(t.slice('achievement:'.length)); |
| } |
| return { tracks, sponsors, achievements }; |
| } |
|
|
| |
| export const tagsBlock = (tags: string[], indent = ' ') => |
| tags.length ? 'tags:\n' + tags.map((t) => `${indent}- ${t}`).join('\n') : 'tags:'; |
|
|
| |
| |
| |
| |
| |
| export function regenerateReadme(raw: string, selectedTags: string[]): string { |
| const parsed = parseReadme(raw); |
| const preserved = parsed.tags.filter((t) => !isManagedTag(t)); |
| const nextTags = [...preserved, ...selectedTags]; |
| const block = tagsBlock(nextTags, parsed.tagsIndent); |
|
|
| if (!parsed.hasFrontmatter) { |
| return `---\n${block}\n---\n\n${parsed.body}`; |
| } |
|
|
| let fm = parsed.frontmatter; |
| if (parsed.tagsRange) { |
| fm = fm.slice(0, parsed.tagsRange.start) + block + fm.slice(parsed.tagsRange.end); |
| } else { |
| fm = fm.length ? `${fm}\n${block}` : block; |
| } |
| return `---\n${fm}\n---\n${parsed.body}`; |
| } |
|
|
| |
| export function diffTags( |
| original: string[], |
| next: string[] |
| ): { added: string[]; removed: string[] } { |
| const o = new Set(original); |
| const n = new Set(next); |
| return { |
| added: next.filter((t) => !o.has(t)), |
| removed: original.filter((t) => !n.has(t)) |
| }; |
| } |
|
|