120 lines
3.8 KiB
TypeScript
120 lines
3.8 KiB
TypeScript
// 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<string, string | null> {
|
|
line = line.trim()
|
|
if (!line) {
|
|
throw new Error('Empty line')
|
|
}
|
|
|
|
const result: Record<string, string | null> = {}
|
|
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
|
|
}
|