// web/frontend/src/utils/parser.ts import { TracklistEntry } from '../types' type Token = { type: 'literal'; value: string } | { type: 'placeholder'; value: string } export function parseFormat(formatStr: string): Token[] { const validPlaceholders = new Set(['ts', 'tn', 'an', 'al', 'date', 'ext']) const tokens: Token[] = [] let i = 0 while (i < formatStr.length) { const ch = formatStr[i] if (ch === '%') { if (i + 1 < formatStr.length && formatStr[i + 1] === '%') { tokens.push({ type: 'literal', value: '%' }) i += 2 continue } // match %letters const match = formatStr.substring(i).match(/^%([a-zA-Z]+)/) if (!match) { throw new Error(`Invalid placeholder at position ${i}: '${formatStr.substring(i)}'`) } const placeholder = match[1] if (!validPlaceholders.has(placeholder)) { throw new Error(`Unknown placeholder '%${placeholder}'. Allowed: ${Array.from(validPlaceholders).join(', ')}`) } tokens.push({ type: 'placeholder', value: placeholder }) i += match[0].length } else { let j = i while (j < formatStr.length && formatStr[j] !== '%') { j++ } tokens.push({ type: 'literal', value: formatStr.substring(i, j) }) i = j } } return tokens } export function parseLine(line: string, tokens: Token[]): Record { line = line.trim() if (!line) { throw new Error('Empty line') } const result: Record = {} let pos = 0 for (let idx = 0; idx < tokens.length; idx++) { const token = tokens[idx] if (token.type === 'literal') { const literal = token.value if (!line.startsWith(literal, pos)) { throw new Error(`Expected literal '${literal}' at position ${pos}, got '${line.substring(pos)}'`) } pos += literal.length } else { // placeholder const placeholder = token.value // If this is the last token, capture the rest if (idx === tokens.length - 1) { const value = line.substring(pos).trim() result[placeholder] = value || null pos = line.length } else { // Find the next literal to use as delimiter let nextLiteral: string | null = null for (let j = idx + 1; j < tokens.length; j++) { if (tokens[j].type === 'literal') { nextLiteral = tokens[j].value break } } if (nextLiteral === null) { const value = line.substring(pos).trim() result[placeholder] = value || null pos = line.length } else { const nextPos = line.indexOf(nextLiteral, pos) if (nextPos === -1) { throw new Error(`Could not find literal '${nextLiteral}' after placeholder '${placeholder}'`) } const value = line.substring(pos, nextPos).trim() result[placeholder] = value || null pos = nextPos } } } } return result } export function parseTracklistWithFormat(text: string, format: string): TracklistEntry[] { const tokens = parseFormat(format) const lines = text.split('\n').filter(line => line.trim() !== '') const entries: TracklistEntry[] = [] for (const line of lines) { try { const parsed = parseLine(line, tokens) const entry: TracklistEntry = { ts: parsed.ts || '', tn: parsed.tn || '', an: parsed.an || '', al: parsed.al || '', date: parsed.date || '', ext: parsed.ext || '', } entries.push(entry) } catch (error) { // We'll handle errors in the validator; just skip or mark as invalid // For now, we'll push an empty entry with an error flag entries.push({ ts: '', tn: line, an: '' }) } } return entries }