subforge
Version:
High-performance subtitle toolkit for parsing, converting, and authoring across 20+ formats.
97 lines (80 loc) • 2.63 kB
text/typescript
import type { SubtitleDocument, SubtitleEvent, TextSegment } from '../../../core/types.ts'
import { generateCSS } from './css.ts'
/**
* Serialize subtitle document to SAMI format
*
* @param doc - Subtitle document to serialize
* @returns SAMI formatted string
*
* @example
* ```ts
* const sami = toSAMI(doc)
* ```
*/
export function toSAMI(doc: SubtitleDocument): string {
const title = doc.info.title || 'Subtitle'
let result = '<SAMI>\n<HEAD>\n'
result += `<TITLE>${escapeHTML(title)}</TITLE>\n`
result += '<STYLE TYPE="text/css">\n<!--\n'
// Generate CSS from styles
result += generateCSS(doc.styles)
result += '-->\n</STYLE>\n</HEAD>\n<BODY>\n'
// Sort events by start time
const sortedEvents = [...doc.events].sort((a, b) => a.start - b.start)
// Output SYNC points
for (const event of sortedEvents) {
const text = event.dirty && event.segments.length > 0
? serializeTags(event.segments)
: escapeHTML(event.text)
const className = event.style !== 'Default' ? event.style.toUpperCase().replace(/[^A-Z0-9]/g, '') : 'ENCC'
result += `<SYNC Start=${event.start}><P Class=${className}>${text}</P>\n`
// Add clear marker at end time
if (event.end > event.start) {
result += `<SYNC Start=${event.end}><P Class=${className}> </P>\n`
}
}
result += '</BODY>\n</SAMI>\n'
return result
}
function serializeTags(segments: TextSegment[]): string {
let result = ''
for (const seg of segments) {
let text = escapeHTML(seg.text)
const openTags: string[] = []
const closeTags: string[] = []
if (seg.style?.bold) {
openTags.push('<b>')
closeTags.unshift('</b>')
}
if (seg.style?.italic) {
openTags.push('<i>')
closeTags.unshift('</i>')
}
if (seg.style?.underline) {
openTags.push('<u>')
closeTags.unshift('</u>')
}
if (seg.style?.strikeout) {
openTags.push('<s>')
closeTags.unshift('</s>')
}
if (seg.style?.primaryColor !== undefined) {
const color = seg.style.primaryColor
const r = color & 0xFF
const g = (color >> 8) & 0xFF
const b = (color >> 16) & 0xFF
const hex = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`
openTags.push(`<font color="${hex}">`)
closeTags.unshift('</font>')
}
result += openTags.join('') + text + closeTags.join('')
}
return result
}
function escapeHTML(text: string): string {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
}