subforge
Version:
High-performance subtitle toolkit for parsing, converting, and authoring across 20+ formats.
234 lines (213 loc) • 6.03 kB
text/typescript
import type { Effect } from './types.ts'
/**
* Handler for parsing and serializing a specific effect type.
* @template E - The effect type this handler processes
*/
interface EffectHandler<E extends Effect = Effect> {
/** Effect type identifier */
type: E['type']
/** Parse raw string into effect parameters */
parse(raw: string): E['params'] | null
/** Serialize effect parameters to string */
serialize(params: E['params']): string
}
const handlers = new Map<string, EffectHandler>()
/**
* Registers a handler for parsing and serializing an effect type.
* @template E - The effect type to register
* @param handler - The effect handler implementation
* @example
* ```ts
* registerEffect({
* type: 'blur',
* parse: (raw) => ({ strength: parseFloat(raw) }),
* serialize: (p) => String(p.strength)
* })
* ```
*/
export function registerEffect<E extends Effect>(handler: EffectHandler<E>): void {
handlers.set(handler.type, handler as EffectHandler)
}
/**
* Retrieves the registered handler for an effect type.
* @param type - The effect type identifier
* @returns The effect handler, or undefined if not registered
*/
export function getEffectHandler(type: string): EffectHandler | undefined {
return handlers.get(type)
}
registerEffect({
type: 'blur',
parse: (raw) => {
const strength = parseFloat(raw)
if (isNaN(strength)) return null
return { strength }
},
serialize: (p) => String((p as { strength: number }).strength)
})
registerEffect({
type: 'karaoke',
parse: (raw) => {
const match = raw.match(/^(k|kf|ko|K)(\d+)$/)
if (!match) return null
const mode = match[1] === 'kf' || match[1] === 'K' ? 'fade'
: match[1] === 'ko' ? 'outline' : 'fill'
return { duration: parseInt(match[2]!) * 10, mode }
},
serialize: (p) => {
const params = p as { duration: number; mode: 'fill' | 'fade' | 'outline' }
const prefix = params.mode === 'fade' ? 'kf' : params.mode === 'outline' ? 'ko' : 'k'
return `${prefix}${params.duration / 10}`
}
})
registerEffect({
type: 'border',
parse: (raw) => {
const size = parseFloat(raw)
if (isNaN(size)) return null
return { size }
},
serialize: (p) => String((p as { size: number }).size)
})
registerEffect({
type: 'shadow',
parse: (raw) => {
const depth = parseFloat(raw)
if (isNaN(depth)) return null
return { depth }
},
serialize: (p) => String((p as { depth: number }).depth)
})
registerEffect({
type: 'spacing',
parse: (raw) => {
const value = parseFloat(raw)
if (isNaN(value)) return null
return { value }
},
serialize: (p) => String((p as { value: number }).value)
})
registerEffect({
type: 'scale',
parse: (raw) => {
const parts = raw.split(',')
if (parts.length !== 2) return null
const x = parseFloat(parts[0]!)
const y = parseFloat(parts[1]!)
if (isNaN(x) || isNaN(y)) return null
return { x, y }
},
serialize: (p) => {
const params = p as { x: number; y: number }
return `${params.x},${params.y}`
}
})
registerEffect({
type: 'rotate',
parse: (raw) => {
const z = parseFloat(raw)
if (isNaN(z)) return null
return { z }
},
serialize: (p) => {
const params = p as { x?: number; y?: number; z?: number }
return String(params.z ?? 0)
}
})
registerEffect({
type: 'fade',
parse: (raw) => {
const parts = raw.split(',')
if (parts.length !== 2) return null
const fadeIn = parseInt(parts[0]!)
const fadeOut = parseInt(parts[1]!)
if (isNaN(fadeIn) || isNaN(fadeOut)) return null
return { in: fadeIn, out: fadeOut }
},
serialize: (p) => {
const params = p as { in: number; out: number }
return `${params.in},${params.out}`
}
})
registerEffect({
type: 'fadeComplex',
parse: (raw) => {
const parts = raw.split(',')
if (parts.length !== 7) return null
const nums = parts.map(p => parseInt(p))
if (nums.some(n => isNaN(n))) return null
return {
alphas: [nums[0]!, nums[1]!, nums[2]!] as [number, number, number],
times: [nums[3]!, nums[4]!, nums[5]!, nums[6]!] as [number, number, number, number]
}
},
serialize: (p) => {
const params = p as { alphas: [number, number, number]; times: [number, number, number, number] }
return [...params.alphas, ...params.times].join(',')
}
})
registerEffect({
type: 'move',
parse: (raw) => {
const parts = raw.split(',')
if (parts.length < 4) return null
const nums = parts.map(p => parseFloat(p))
if (nums.slice(0, 4).some(n => isNaN(n))) return null
const result: { from: [number, number]; to: [number, number]; t1?: number; t2?: number } = {
from: [nums[0]!, nums[1]!],
to: [nums[2]!, nums[3]!]
}
if (parts.length >= 6 && !isNaN(nums[4]!) && !isNaN(nums[5]!)) {
result.t1 = nums[4]
result.t2 = nums[5]
}
return result
},
serialize: (p) => {
const params = p as { from: [number, number]; to: [number, number]; t1?: number; t2?: number }
const parts = [...params.from, ...params.to]
if (params.t1 !== undefined && params.t2 !== undefined) {
parts[parts.length] = params.t1
parts[parts.length] = params.t2
}
return parts.join(',')
}
})
registerEffect({
type: 'clip',
parse: (raw) => {
return { path: raw, inverse: false }
},
serialize: (p) => (p as { path: string }).path
})
registerEffect({
type: 'drawing',
parse: (raw) => {
const scale = parseInt(raw)
if (isNaN(scale)) return null
return { scale, commands: '' }
},
serialize: (p) => String((p as { scale: number }).scale)
})
registerEffect({
type: 'reset',
parse: (raw) => {
return { style: raw || undefined }
},
serialize: (p) => (p as { style?: string }).style ?? ''
})
registerEffect({
type: 'image',
parse: () => null,
serialize: () => ''
})
registerEffect({
type: 'vobsub',
parse: () => null,
serialize: () => ''
})
registerEffect({
type: 'pgs',
parse: () => null,
serialize: () => ''
})