subforge
Version:
High-performance subtitle toolkit for parsing, converting, and authoring across 20+ formats.
1,670 lines (1,667 loc) • 54.4 kB
JavaScript
// src/formats/text/ass/time.ts
function parseTime(s) {
const len = s.length;
if (len < 10)
throw new Error(`Invalid ASS timestamp: ${s}`);
const colon1 = s.indexOf(":");
if (colon1 === -1)
throw new Error(`Invalid ASS timestamp: ${s}`);
let h = 0;
for (let i = 0;i < colon1; i++) {
h = h * 10 + (s.charCodeAt(i) - 48);
}
if (s.charCodeAt(colon1 + 6) !== 46)
throw new Error(`Invalid ASS timestamp: ${s}`);
const m = (s.charCodeAt(colon1 + 1) - 48) * 10 + (s.charCodeAt(colon1 + 2) - 48);
const ss = (s.charCodeAt(colon1 + 4) - 48) * 10 + (s.charCodeAt(colon1 + 5) - 48);
const fracStart = colon1 + 7;
const fracLen = len - fracStart;
let ms;
if (fracLen === 2) {
ms = ((s.charCodeAt(fracStart) - 48) * 10 + (s.charCodeAt(fracStart + 1) - 48)) * 10;
} else {
ms = (s.charCodeAt(fracStart) - 48) * 100 + (s.charCodeAt(fracStart + 1) - 48) * 10 + (s.charCodeAt(fracStart + 2) - 48);
}
return h * 3600000 + m * 60000 + ss * 1000 + ms;
}
function formatTime(ms) {
const h = Math.floor(ms / 3600000);
const m = Math.floor(ms % 3600000 / 60000);
const s = Math.floor(ms % 60000 / 1000);
const cs = Math.floor(ms % 1000 / 10);
return `${h}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}.${cs.toString().padStart(2, "0")}`;
}
// src/formats/text/ass/color.ts
function parseColor(s) {
const match = s.match(/^&[Hh]([0-9A-Fa-f]{6,8})&?$/);
if (!match)
throw new Error(`Invalid ASS color: ${s}`);
const hex = match[1].padStart(8, "0");
return parseInt(hex, 16);
}
function formatColor(color) {
return `&H${(color >>> 0).toString(16).toUpperCase().padStart(8, "0")}&`;
}
function parseAlpha(s) {
const match = s.match(/^&H([0-9A-Fa-f]{2})&?$/);
if (!match)
throw new Error(`Invalid ASS alpha: ${s}`);
return parseInt(match[1], 16);
}
function formatAlpha(alpha) {
return `&H${(alpha & 255).toString(16).toUpperCase().padStart(2, "0")}&`;
}
// src/formats/text/ass/tags.ts
function processEscapes(text) {
return text.replace(/\\N/g, `
`).replace(/\\n/g, `
`).replace(/\\h/g, " ");
}
function serializeEscapes(text) {
return text.replace(/\n/g, "\\N").replace(/\u00A0/g, "\\h");
}
function parseTags(raw) {
const segments = [];
let currentStyle = null;
let currentEffects = [];
let textStart = 0;
let i = 0;
while (i < raw.length) {
if (raw[i] === "{") {
const closeIdx = raw.indexOf("}", i);
if (closeIdx === -1) {
i++;
continue;
}
if (i > textStart) {
segments[segments.length] = {
text: processEscapes(raw.slice(textStart, i)),
style: currentStyle ? { ...currentStyle } : null,
effects: [...currentEffects]
};
}
const tagBlock = raw.slice(i + 1, closeIdx);
const result = parseTagBlock(tagBlock, currentStyle, currentEffects);
currentStyle = result.style;
currentEffects = result.effects;
i = closeIdx + 1;
textStart = i;
} else {
i++;
}
}
if (textStart < raw.length) {
segments[segments.length] = {
text: processEscapes(raw.slice(textStart)),
style: currentStyle ? { ...currentStyle } : null,
effects: [...currentEffects]
};
}
return segments;
}
var tagDefs = [
{ pattern: /^b(\d+)$/, handler: (m, s) => {
const val = parseInt(m[1]);
if (val === 0)
s.bold = false;
else if (val === 1)
s.bold = true;
else
s.bold = val;
return { hasStyleChanges: true };
} },
{ pattern: /^i([01])$/, handler: (m, s) => {
s.italic = m[1] === "1";
return { hasStyleChanges: true };
} },
{ pattern: /^u([01])$/, handler: (m, s) => {
s.underline = m[1] === "1";
return { hasStyleChanges: true };
} },
{ pattern: /^s([01])$/, handler: (m, s) => {
s.strikeout = m[1] === "1";
return { hasStyleChanges: true };
} },
{ pattern: /^fn(.+)$/, handler: (m, s) => {
s.fontName = m[1];
return { hasStyleChanges: true };
} },
{ pattern: /^fs(\d+(?:\.\d+)?)$/, handler: (m, s) => {
s.fontSize = parseFloat(m[1]);
return { hasStyleChanges: true };
} },
{ pattern: /^an([1-9])$/, handler: (m, s) => {
s.alignment = parseInt(m[1]);
return { hasStyleChanges: true };
} },
{ pattern: /^a(\d+)$/, handler: (m, s) => {
const val = parseInt(m[1]);
let alignment;
if (val >= 1 && val <= 3)
alignment = val;
else if (val >= 5 && val <= 7)
alignment = val + 2;
else if (val >= 9 && val <= 11)
alignment = val - 5;
else
return { hasStyleChanges: false };
s.alignment = alignment;
return { hasStyleChanges: true };
} },
{ pattern: /^fe(\d+)$/, handler: (m, s) => {
s.fontEncoding = parseInt(m[1]);
return { hasStyleChanges: true };
} },
{ pattern: /^q([0-3])$/, handler: (m, s) => {
s.wrapStyle = parseInt(m[1]);
return { hasStyleChanges: true };
} },
{
pattern: /^(?:c|1c)(&H[0-9A-Fa-f]+&?)$/,
handler: (m, s) => {
try {
s.primaryColor = parseColor(m[1]);
return { hasStyleChanges: true };
} catch {
return { hasStyleChanges: false };
}
}
},
{
pattern: /^2c(&H[0-9A-Fa-f]+&?)$/,
handler: (m, s) => {
try {
s.secondaryColor = parseColor(m[1]);
return { hasStyleChanges: true };
} catch {
return { hasStyleChanges: false };
}
}
},
{
pattern: /^3c(&H[0-9A-Fa-f]+&?)$/,
handler: (m, s) => {
try {
s.outlineColor = parseColor(m[1]);
return { hasStyleChanges: true };
} catch {
return { hasStyleChanges: false };
}
}
},
{
pattern: /^4c(&H[0-9A-Fa-f]+&?)$/,
handler: (m, s) => {
try {
s.backColor = parseColor(m[1]);
return { hasStyleChanges: true };
} catch {
return { hasStyleChanges: false };
}
}
},
{
pattern: /^alpha(&H[0-9A-Fa-f]+&?)$/,
handler: (m, s) => {
try {
s.alpha = parseAlpha(m[1]);
return { hasStyleChanges: true };
} catch {
return { hasStyleChanges: false };
}
}
},
{
pattern: /^1a(&H[0-9A-Fa-f]+&?)$/,
handler: (m, s) => {
try {
s.primaryAlpha = parseAlpha(m[1]);
return { hasStyleChanges: true };
} catch {
return { hasStyleChanges: false };
}
}
},
{
pattern: /^2a(&H[0-9A-Fa-f]+&?)$/,
handler: (m, s) => {
try {
s.secondaryAlpha = parseAlpha(m[1]);
return { hasStyleChanges: true };
} catch {
return { hasStyleChanges: false };
}
}
},
{
pattern: /^3a(&H[0-9A-Fa-f]+&?)$/,
handler: (m, s) => {
try {
s.outlineAlpha = parseAlpha(m[1]);
return { hasStyleChanges: true };
} catch {
return { hasStyleChanges: false };
}
}
},
{
pattern: /^4a(&H[0-9A-Fa-f]+&?)$/,
handler: (m, s) => {
try {
s.backAlpha = parseAlpha(m[1]);
return { hasStyleChanges: true };
} catch {
return { hasStyleChanges: false };
}
}
},
{
pattern: /^pos\((-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)\)$/,
handler: (m, s) => {
s.pos = [parseFloat(m[1]), parseFloat(m[2])];
return { hasStyleChanges: true };
}
},
{
pattern: /^org\((-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)\)$/,
handler: (m, _, e) => {
const idx = e.findIndex((ef) => ef.type === "origin");
if (idx !== -1)
e.splice(idx, 1);
e[e.length] = { type: "origin", params: { x: parseFloat(m[1]), y: parseFloat(m[2]) } };
return { hasStyleChanges: false };
}
},
{
pattern: /^k(\d+)$/,
handler: (_, __, e) => {
const duration = parseInt(_[1]) * 10;
const idx = e.findIndex((ef) => ef.type === "karaoke");
if (idx !== -1)
e.splice(idx, 1);
e[e.length] = { type: "karaoke", params: { duration, mode: "fill" } };
return { hasStyleChanges: false };
}
},
{
pattern: /^K(\d+)$/,
handler: (_, __, e) => {
const duration = parseInt(_[1]) * 10;
const idx = e.findIndex((ef) => ef.type === "karaoke");
if (idx !== -1)
e.splice(idx, 1);
e[e.length] = { type: "karaoke", params: { duration, mode: "fade" } };
return { hasStyleChanges: false };
}
},
{
pattern: /^kf(\d+)$/,
handler: (_, __, e) => {
const duration = parseInt(_[1]) * 10;
const idx = e.findIndex((ef) => ef.type === "karaoke");
if (idx !== -1)
e.splice(idx, 1);
e[e.length] = { type: "karaoke", params: { duration, mode: "fade" } };
return { hasStyleChanges: false };
}
},
{
pattern: /^ko(\d+)$/,
handler: (_, __, e) => {
const duration = parseInt(_[1]) * 10;
const idx = e.findIndex((ef) => ef.type === "karaoke");
if (idx !== -1)
e.splice(idx, 1);
e[e.length] = { type: "karaoke", params: { duration, mode: "outline" } };
return { hasStyleChanges: false };
}
},
{
pattern: /^kt(\d+)$/,
handler: (m, _, e) => {
const time = parseInt(m[1]);
const idx = e.findIndex((ef) => ef.type === "karaokeAbsolute");
if (idx !== -1)
e.splice(idx, 1);
e[e.length] = { type: "karaokeAbsolute", params: { time } };
return { hasStyleChanges: false };
}
},
{
pattern: /^(?:blur|be)(\d+(?:\.\d+)?)$/,
handler: (m, _, e) => {
const strength = parseFloat(m[1]);
const idx = e.findIndex((ef) => ef.type === "blur");
if (idx !== -1)
e.splice(idx, 1);
e[e.length] = { type: "blur", params: { strength } };
return { hasStyleChanges: false };
}
},
{
pattern: /^bord(\d+(?:\.\d+)?)$/,
handler: (m, _, e) => {
const size = parseFloat(m[1]);
const idx = e.findIndex((ef) => ef.type === "border");
if (idx !== -1)
e.splice(idx, 1);
e[e.length] = { type: "border", params: { size } };
return { hasStyleChanges: false };
}
},
{
pattern: /^xbord(\d+(?:\.\d+)?)$/,
handler: (m, _, e) => {
const x = parseFloat(m[1]);
let existing = e.find((ef) => ef.type === "border");
if (!existing) {
existing = { type: "border", params: { size: 0 } };
e.push(existing);
}
existing.params.x = x;
return { hasStyleChanges: false };
}
},
{
pattern: /^ybord(\d+(?:\.\d+)?)$/,
handler: (m, _, e) => {
const y = parseFloat(m[1]);
let existing = e.find((ef) => ef.type === "border");
if (!existing) {
existing = { type: "border", params: { size: 0 } };
e.push(existing);
}
existing.params.y = y;
return { hasStyleChanges: false };
}
},
{
pattern: /^shad(\d+(?:\.\d+)?)$/,
handler: (m, _, e) => {
const depth = parseFloat(m[1]);
const idx = e.findIndex((ef) => ef.type === "shadow");
if (idx !== -1)
e.splice(idx, 1);
e[e.length] = { type: "shadow", params: { depth } };
return { hasStyleChanges: false };
}
},
{
pattern: /^xshad(-?\d+(?:\.\d+)?)$/,
handler: (m, _, e) => {
const x = parseFloat(m[1]);
let existing = e.find((ef) => ef.type === "shadow");
if (!existing) {
existing = { type: "shadow", params: { depth: 0 } };
e.push(existing);
}
existing.params.x = x;
return { hasStyleChanges: false };
}
},
{
pattern: /^yshad(-?\d+(?:\.\d+)?)$/,
handler: (m, _, e) => {
const y = parseFloat(m[1]);
let existing = e.find((ef) => ef.type === "shadow");
if (!existing) {
existing = { type: "shadow", params: { depth: 0 } };
e.push(existing);
}
existing.params.y = y;
return { hasStyleChanges: false };
}
},
{
pattern: /^fscx(\d+(?:\.\d+)?)$/,
handler: (m, _, e) => {
const x = parseFloat(m[1]);
const existing = e.find((ef) => ef.type === "scale");
if (existing)
existing.params.x = x;
else
e[e.length] = { type: "scale", params: { x, y: 100 } };
return { hasStyleChanges: false };
}
},
{
pattern: /^fscy(\d+(?:\.\d+)?)$/,
handler: (m, _, e) => {
const y = parseFloat(m[1]);
const existing = e.find((ef) => ef.type === "scale");
if (existing)
existing.params.y = y;
else
e[e.length] = { type: "scale", params: { x: 100, y } };
return { hasStyleChanges: false };
}
},
{
pattern: /^frx(-?\d+(?:\.\d+)?)$/,
handler: (m, _, e) => {
const angle = parseFloat(m[1]);
let existing = e.find((ef) => ef.type === "rotate");
if (!existing) {
existing = { type: "rotate", params: {} };
e.push(existing);
}
existing.params.x = angle;
return { hasStyleChanges: false };
}
},
{
pattern: /^fry(-?\d+(?:\.\d+)?)$/,
handler: (m, _, e) => {
const angle = parseFloat(m[1]);
let existing = e.find((ef) => ef.type === "rotate");
if (!existing) {
existing = { type: "rotate", params: {} };
e.push(existing);
}
existing.params.y = angle;
return { hasStyleChanges: false };
}
},
{
pattern: /^(?:frz|fr)(-?\d+(?:\.\d+)?)$/,
handler: (m, _, e) => {
const angle = parseFloat(m[1]);
let existing = e.find((ef) => ef.type === "rotate");
if (!existing) {
existing = { type: "rotate", params: {} };
e.push(existing);
}
existing.params.z = angle;
return { hasStyleChanges: false };
}
},
{
pattern: /^fax(-?\d+(?:\.\d+)?)$/,
handler: (m, _, e) => {
const shear = parseFloat(m[1]);
let existing = e.find((ef) => ef.type === "shear");
if (!existing) {
existing = { type: "shear", params: {} };
e.push(existing);
}
existing.params.x = shear;
return { hasStyleChanges: false };
}
},
{
pattern: /^fay(-?\d+(?:\.\d+)?)$/,
handler: (m, _, e) => {
const shear = parseFloat(m[1]);
let existing = e.find((ef) => ef.type === "shear");
if (!existing) {
existing = { type: "shear", params: {} };
e.push(existing);
}
existing.params.y = shear;
return { hasStyleChanges: false };
}
},
{
pattern: /^fsp(-?\d+(?:\.\d+)?)$/,
handler: (m, _, e) => {
const value = parseFloat(m[1]);
const idx = e.findIndex((ef) => ef.type === "spacing");
if (idx !== -1)
e.splice(idx, 1);
e[e.length] = { type: "spacing", params: { value } };
return { hasStyleChanges: false };
}
},
{
pattern: /^fad\((\d+),(\d+)\)$/,
handler: (m, _, e) => {
const idx = e.findIndex((ef) => ef.type === "fade");
if (idx !== -1)
e.splice(idx, 1);
e[e.length] = { type: "fade", params: { in: parseInt(m[1]), out: parseInt(m[2]) } };
return { hasStyleChanges: false };
}
},
{
pattern: /^fade\((\d+),(\d+),(\d+),(\d+),(\d+),(\d+),(\d+)\)$/,
handler: (m, _, e) => {
const idx = e.findIndex((ef) => ef.type === "fadeComplex");
if (idx !== -1)
e.splice(idx, 1);
e[e.length] = {
type: "fadeComplex",
params: {
alphas: [parseInt(m[1]), parseInt(m[2]), parseInt(m[3])],
times: [parseInt(m[4]), parseInt(m[5]), parseInt(m[6]), parseInt(m[7])]
}
};
return { hasStyleChanges: false };
}
},
{
pattern: /^move\((-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?),(-?\d+(?:\.\d+)?)(?:,(\d+),(\d+))?\)$/,
handler: (m, _, e) => {
const params = {
from: [parseFloat(m[1]), parseFloat(m[2])],
to: [parseFloat(m[3]), parseFloat(m[4])]
};
if (m[5] && m[6]) {
params.t1 = parseInt(m[5]);
params.t2 = parseInt(m[6]);
}
const idx = e.findIndex((ef) => ef.type === "move");
if (idx !== -1)
e.splice(idx, 1);
e[e.length] = { type: "move", params };
return { hasStyleChanges: false };
}
},
{
pattern: /^clip(\(.+\))$/,
handler: (m, _, e) => {
const idx = e.findIndex((ef) => ef.type === "clip");
if (idx !== -1)
e.splice(idx, 1);
e[e.length] = { type: "clip", params: { path: m[1], inverse: false } };
return { hasStyleChanges: false };
}
},
{
pattern: /^iclip(\(.+\))$/,
handler: (m, _, e) => {
const idx = e.findIndex((ef) => ef.type === "clip");
if (idx !== -1)
e.splice(idx, 1);
e[e.length] = { type: "clip", params: { path: m[1], inverse: true } };
return { hasStyleChanges: false };
}
},
{
pattern: /^p(\d+)$/,
handler: (m, _, e) => {
const scale = parseInt(m[1]);
if (scale > 0) {
const idx = e.findIndex((ef) => ef.type === "drawing");
if (idx !== -1)
e.splice(idx, 1);
e[e.length] = { type: "drawing", params: { scale, commands: "" } };
}
return { hasStyleChanges: false };
}
},
{
pattern: /^pbo(-?\d+(?:\.\d+)?)$/,
handler: (m, _, e) => {
const offset = parseFloat(m[1]);
const idx = e.findIndex((ef) => ef.type === "drawingBaseline");
if (idx !== -1)
e.splice(idx, 1);
e[e.length] = { type: "drawingBaseline", params: { offset } };
return { hasStyleChanges: false };
}
},
{
pattern: /^r(.*)$/,
handler: (m, s, e) => {
const idx = e.findIndex((ef) => ef.type === "reset");
if (idx !== -1)
e.splice(idx, 1);
e[e.length] = { type: "reset", params: { style: m[1] || undefined } };
for (const key of Object.keys(s))
delete s[key];
return { hasStyleChanges: false };
}
},
{
pattern: /^t\((.+)\)$/,
handler: (m, _, e) => {
const inner = m[1];
const tMatch = inner.match(/^(?:(\d+),(\d+),)?(?:(\d+(?:\.\d+)?),)?(.+)$/);
if (tMatch) {
e[e.length] = {
type: "animate",
params: {
start: tMatch[1] ? parseInt(tMatch[1]) : 0,
end: tMatch[2] ? parseInt(tMatch[2]) : 0,
accel: tMatch[3] ? parseFloat(tMatch[3]) : undefined,
target: {}
}
};
}
return { hasStyleChanges: false };
}
}
];
function parseTagBlock(block, currentStyle, currentEffects) {
const style = currentStyle ? { ...currentStyle } : {};
const effects = [...currentEffects];
let hasStyleChanges = currentStyle !== null;
const tags = block.split("\\").filter((t) => t.length > 0);
for (const tagStr of tags) {
let matched = false;
for (const def of tagDefs) {
const match = tagStr.match(def.pattern);
if (match) {
const result = def.handler(match, style, effects);
if (result.hasStyleChanges)
hasStyleChanges = true;
matched = true;
break;
}
}
if (!matched && tagStr.length > 0) {
effects[effects.length] = { type: "unknown", params: { format: "ass", raw: `\\${tagStr}` } };
}
}
return {
style: hasStyleChanges ? style : null,
effects
};
}
function serializeTags(segments) {
let result = "";
let prevStyle = null;
for (const seg of segments) {
const tags = [];
if (seg.style) {
if (seg.style.bold !== undefined && seg.style.bold !== prevStyle?.bold) {
if (typeof seg.style.bold === "number") {
tags[tags.length] = `\\b${seg.style.bold}`;
} else {
tags[tags.length] = `\\b${seg.style.bold ? "1" : "0"}`;
}
}
if (seg.style.italic !== undefined && seg.style.italic !== prevStyle?.italic) {
tags[tags.length] = `\\i${seg.style.italic ? "1" : "0"}`;
}
if (seg.style.underline !== undefined && seg.style.underline !== prevStyle?.underline) {
tags[tags.length] = `\\u${seg.style.underline ? "1" : "0"}`;
}
if (seg.style.strikeout !== undefined && seg.style.strikeout !== prevStyle?.strikeout) {
tags[tags.length] = `\\s${seg.style.strikeout ? "1" : "0"}`;
}
if (seg.style.fontName !== undefined && seg.style.fontName !== prevStyle?.fontName) {
tags[tags.length] = `\\fn${seg.style.fontName}`;
}
if (seg.style.fontSize !== undefined && seg.style.fontSize !== prevStyle?.fontSize) {
tags[tags.length] = `\\fs${seg.style.fontSize}`;
}
if (seg.style.fontEncoding !== undefined && seg.style.fontEncoding !== prevStyle?.fontEncoding) {
tags[tags.length] = `\\fe${seg.style.fontEncoding}`;
}
if (seg.style.wrapStyle !== undefined && seg.style.wrapStyle !== prevStyle?.wrapStyle) {
tags[tags.length] = `\\q${seg.style.wrapStyle}`;
}
if (seg.style.primaryColor !== undefined && seg.style.primaryColor !== prevStyle?.primaryColor) {
tags[tags.length] = `\\c&H${(seg.style.primaryColor >>> 0).toString(16).toUpperCase().padStart(8, "0")}&`;
}
if (seg.style.secondaryColor !== undefined && seg.style.secondaryColor !== prevStyle?.secondaryColor) {
tags[tags.length] = `\\2c&H${(seg.style.secondaryColor >>> 0).toString(16).toUpperCase().padStart(8, "0")}&`;
}
if (seg.style.outlineColor !== undefined && seg.style.outlineColor !== prevStyle?.outlineColor) {
tags[tags.length] = `\\3c&H${(seg.style.outlineColor >>> 0).toString(16).toUpperCase().padStart(8, "0")}&`;
}
if (seg.style.backColor !== undefined && seg.style.backColor !== prevStyle?.backColor) {
tags[tags.length] = `\\4c&H${(seg.style.backColor >>> 0).toString(16).toUpperCase().padStart(8, "0")}&`;
}
if (seg.style.alpha !== undefined && seg.style.alpha !== prevStyle?.alpha) {
tags[tags.length] = `\\alpha&H${seg.style.alpha.toString(16).toUpperCase().padStart(2, "0")}&`;
}
if (seg.style.primaryAlpha !== undefined && seg.style.primaryAlpha !== prevStyle?.primaryAlpha) {
tags[tags.length] = `\\1a&H${seg.style.primaryAlpha.toString(16).toUpperCase().padStart(2, "0")}&`;
}
if (seg.style.secondaryAlpha !== undefined && seg.style.secondaryAlpha !== prevStyle?.secondaryAlpha) {
tags[tags.length] = `\\2a&H${seg.style.secondaryAlpha.toString(16).toUpperCase().padStart(2, "0")}&`;
}
if (seg.style.outlineAlpha !== undefined && seg.style.outlineAlpha !== prevStyle?.outlineAlpha) {
tags[tags.length] = `\\3a&H${seg.style.outlineAlpha.toString(16).toUpperCase().padStart(2, "0")}&`;
}
if (seg.style.backAlpha !== undefined && seg.style.backAlpha !== prevStyle?.backAlpha) {
tags[tags.length] = `\\4a&H${seg.style.backAlpha.toString(16).toUpperCase().padStart(2, "0")}&`;
}
if (seg.style.alignment !== undefined && seg.style.alignment !== prevStyle?.alignment) {
tags[tags.length] = `\\an${seg.style.alignment}`;
}
if (seg.style.pos !== undefined) {
tags[tags.length] = `\\pos(${seg.style.pos[0]},${seg.style.pos[1]})`;
}
}
for (const effect of seg.effects) {
switch (effect.type) {
case "karaoke": {
const p = effect.params;
const prefix = p.mode === "fade" ? "kf" : p.mode === "outline" ? "ko" : "k";
tags[tags.length] = `\\${prefix}${p.duration / 10}`;
break;
}
case "karaokeAbsolute": {
const p = effect.params;
tags[tags.length] = `\\kt${p.time}`;
break;
}
case "blur": {
const p = effect.params;
tags[tags.length] = `\\blur${p.strength}`;
break;
}
case "border": {
const p = effect.params;
if (p.size)
tags[tags.length] = `\\bord${p.size}`;
if (p.x !== undefined)
tags[tags.length] = `\\xbord${p.x}`;
if (p.y !== undefined)
tags[tags.length] = `\\ybord${p.y}`;
break;
}
case "shadow": {
const p = effect.params;
if (p.depth)
tags[tags.length] = `\\shad${p.depth}`;
if (p.x !== undefined)
tags[tags.length] = `\\xshad${p.x}`;
if (p.y !== undefined)
tags[tags.length] = `\\yshad${p.y}`;
break;
}
case "origin": {
const p = effect.params;
tags[tags.length] = `\\org(${p.x},${p.y})`;
break;
}
case "scale": {
const p = effect.params;
if (p.x !== 100)
tags[tags.length] = `\\fscx${p.x}`;
if (p.y !== 100)
tags[tags.length] = `\\fscy${p.y}`;
break;
}
case "rotate": {
const p = effect.params;
if (p.x !== undefined)
tags[tags.length] = `\\frx${p.x}`;
if (p.y !== undefined)
tags[tags.length] = `\\fry${p.y}`;
if (p.z !== undefined)
tags[tags.length] = `\\frz${p.z}`;
break;
}
case "shear": {
const p = effect.params;
if (p.x !== undefined)
tags[tags.length] = `\\fax${p.x}`;
if (p.y !== undefined)
tags[tags.length] = `\\fay${p.y}`;
break;
}
case "spacing": {
const p = effect.params;
tags[tags.length] = `\\fsp${p.value}`;
break;
}
case "fade": {
const p = effect.params;
tags[tags.length] = `\\fad(${p.in},${p.out}`;
break;
}
case "fadeComplex": {
const p = effect.params;
tags[tags.length] = `\\fade(${p.alphas.join(",")},${p.times.join(",")}`;
break;
}
case "move": {
const p = effect.params;
if (p.t1 !== undefined && p.t2 !== undefined) {
tags[tags.length] = `\\move(${p.from[0]},${p.from[1]},${p.to[0]},${p.to[1]},${p.t1},${p.t2}`;
} else {
tags[tags.length] = `\\move(${p.from[0]},${p.from[1]},${p.to[0]},${p.to[1]}`;
}
break;
}
case "clip": {
const p = effect.params;
tags[tags.length] = `\\${p.inverse ? "iclip" : "clip"}${p.path}`;
break;
}
case "drawing": {
const p = effect.params;
tags[tags.length] = `\\p${p.scale}`;
break;
}
case "drawingBaseline": {
const p = effect.params;
tags[tags.length] = `\\pbo${p.offset}`;
break;
}
case "reset": {
const p = effect.params;
tags[tags.length] = `\\r${p.style ?? ""}`;
break;
}
case "unknown": {
const p = effect.params;
tags[tags.length] = p.raw;
break;
}
}
}
if (tags.length > 0) {
result += `{${tags.join("")}}`;
}
result += serializeEscapes(seg.text);
prevStyle = seg.style;
}
return result;
}
function stripTags(raw) {
return raw.replace(/\{[^}]*\}/g, "");
}
// src/core/errors.ts
class SubforgeError extends Error {
code;
line;
column;
constructor(code, message, position) {
super(`[${code}] ${message} at line ${position.line}, column ${position.column}`);
this.name = "SubforgeError";
this.code = code;
this.line = position.line;
this.column = position.column;
}
}
function toParseError(err) {
if (err instanceof SubforgeError) {
return {
line: err.line,
column: err.column,
code: err.code,
message: err.message
};
}
if (err instanceof Error) {
return {
line: 1,
column: 1,
code: "MALFORMED_EVENT",
message: err.message
};
}
return {
line: 1,
column: 1,
code: "MALFORMED_EVENT",
message: String(err)
};
}
function unwrap(result) {
if (result.ok)
return result.document;
const first = result.errors[0];
if (first) {
throw new SubforgeError(first.code, first.message, { line: first.line, column: first.column });
}
throw new Error("Parse failed");
}
// src/core/document.ts
var idCounter = 0;
var EMPTY_SEGMENTS = [];
function generateId() {
return ++idCounter;
}
function reserveIds(count) {
const start = idCounter + 1;
idCounter += count;
return start;
}
function createDocument(init) {
return {
info: {
title: "",
playResX: 1920,
playResY: 1080,
scaleBorderAndShadow: true,
wrapStyle: 0,
...init?.info
},
styles: init?.styles ?? new Map([["Default", createDefaultStyle()]]),
events: init?.events ?? [],
comments: init?.comments ?? [],
fonts: init?.fonts,
graphics: init?.graphics,
regions: init?.regions
};
}
function createDefaultStyle() {
return {
name: "Default",
fontName: "Arial",
fontSize: 48,
primaryColor: 16777215,
secondaryColor: 255,
outlineColor: 0,
backColor: 0,
bold: false,
italic: false,
underline: false,
strikeout: false,
scaleX: 100,
scaleY: 100,
spacing: 0,
angle: 0,
borderStyle: 1,
outline: 2,
shadow: 2,
alignment: 2,
marginL: 10,
marginR: 10,
marginV: 10,
encoding: 1
};
}
function createEvent(start, end, text, opts) {
return {
id: generateId(),
start,
end,
layer: 0,
style: "Default",
actor: "",
marginL: 0,
marginR: 0,
marginV: 0,
effect: "",
text,
segments: [],
dirty: false,
...opts
};
}
function createKaraokeEvent(start, end, syllables, opts) {
const segments = syllables.map((syl) => ({
text: syl.text,
style: syl.style ? { ...syl.style } : null,
effects: [{ type: "karaoke", params: { duration: syl.duration, mode: "fill" } }]
}));
return {
id: generateId(),
start,
end,
layer: 0,
style: "Default",
actor: "",
marginL: 0,
marginR: 0,
marginV: 0,
effect: "",
text: "",
segments,
dirty: true,
...opts
};
}
function cloneDocument(doc) {
return {
info: { ...doc.info },
styles: new Map(doc.styles),
events: doc.events.map((e) => ({
...e,
segments: [...e.segments],
image: e.image ? { ...e.image, data: e.image.data.slice() } : undefined,
vobsub: e.vobsub ? { ...e.vobsub } : undefined,
pgs: e.pgs ? { ...e.pgs } : undefined
})),
comments: [...doc.comments],
fonts: doc.fonts ? [...doc.fonts] : undefined,
graphics: doc.graphics ? [...doc.graphics] : undefined,
regions: doc.regions ? [...doc.regions] : undefined
};
}
function cloneEvent(event) {
return {
...event,
id: generateId(),
segments: event.segments.map((s) => ({ ...s, effects: [...s.effects] })),
image: event.image ? { ...event.image, data: event.image.data.slice() } : undefined,
vobsub: event.vobsub ? { ...event.vobsub } : undefined,
pgs: event.pgs ? { ...event.pgs } : undefined
};
}
// src/formats/text/ass/parser.ts
class ASSLexer {
src;
pos = 0;
len;
line = 1;
lastLineStart = 0;
lastLine = 0;
constructor(src) {
this.src = src;
this.len = src.length;
}
getPosition() {
return { line: this.line, column: 1 };
}
isEOF() {
return this.pos >= this.len;
}
skipLine() {
const nlPos = this.src.indexOf(`
`, this.pos);
if (nlPos === -1) {
this.pos = this.len;
} else {
this.pos = nlPos + 1;
this.line++;
}
}
readLine() {
this.lastLineStart = this.pos;
this.lastLine = this.line;
const start = this.pos;
let nlPos = this.src.indexOf(`
`, this.pos);
if (nlPos === -1)
nlPos = this.len;
let end = nlPos;
if (end > start && this.src.charCodeAt(end - 1) === 13)
end--;
this.pos = nlPos < this.len ? nlPos + 1 : this.len;
this.line++;
return this.src.substring(start, end);
}
unreadLine() {
this.pos = this.lastLineStart;
this.line = this.lastLine;
}
peekLine() {
const savedPos = this.pos;
const savedLine = this.line;
const line = this.readLine();
this.pos = savedPos;
this.line = savedLine;
return line;
}
}
class ASSParser {
lexer;
doc;
errors = [];
opts;
eventIndex = 0;
constructor(input, opts = {}) {
this.lexer = new ASSLexer(input);
this.opts = {
onError: opts.onError ?? "collect",
strict: opts.strict ?? false,
preserveOrder: opts.preserveOrder ?? true
};
this.doc = createDocument();
}
parse() {
while (!this.lexer.isEOF()) {
const line = this.lexer.peekLine().trim();
if (line.startsWith("[")) {
this.parseSection();
} else {
this.lexer.skipLine();
}
}
return { ok: this.errors.length === 0, document: this.doc, errors: this.errors, warnings: [] };
}
parseSection() {
const line = this.lexer.readLine().trim();
const sectionMatch = line.match(/^\[(.+)\]$/);
if (!sectionMatch)
return;
const section = sectionMatch[1].toLowerCase();
switch (section) {
case "script info":
this.parseScriptInfo();
break;
case "v4+ styles":
case "v4 styles":
this.parseStyles();
break;
case "events":
this.parseEvents();
break;
case "fonts":
this.parseFonts();
break;
case "graphics":
this.parseGraphics();
break;
default:
this.skipSection();
}
}
skipSection() {
while (!this.lexer.isEOF()) {
const line = this.lexer.peekLine().trim();
if (line.startsWith("["))
break;
this.lexer.skipLine();
}
}
parseScriptInfo() {
while (!this.lexer.isEOF()) {
const line = this.lexer.peekLine().trim();
if (line.startsWith("["))
break;
this.lexer.readLine();
if (line.startsWith(";") || line === "")
continue;
const colonIdx = line.indexOf(":");
if (colonIdx === -1)
continue;
const key = line.substring(0, colonIdx).trim().toLowerCase();
const value = line.substring(colonIdx + 1).trim();
switch (key) {
case "title":
this.doc.info.title = value;
break;
case "original author":
case "original script":
this.doc.info.author = value;
break;
case "playresx":
this.doc.info.playResX = parseInt(value) || 1920;
break;
case "playresy":
this.doc.info.playResY = parseInt(value) || 1080;
break;
case "scaleborderandshadow":
this.doc.info.scaleBorderAndShadow = value.toLowerCase() === "yes";
break;
case "wrapstyle":
this.doc.info.wrapStyle = parseInt(value) || 0;
break;
}
}
}
parseStyles() {
let format = [];
while (!this.lexer.isEOF()) {
const line = this.lexer.peekLine().trim();
if (line.startsWith("["))
break;
this.lexer.readLine();
if (line.startsWith(";") || line === "")
continue;
if (line.toLowerCase().startsWith("format:")) {
format = line.substring(7).split(",").map((s) => s.trim().toLowerCase());
continue;
}
if (line.toLowerCase().startsWith("style:")) {
const style = this.parseStyleLine(line.substring(6), format);
if (style) {
this.doc.styles.set(style.name, style);
}
}
}
}
parseStyleLine(data, format) {
const values = this.splitFields(data, format.length);
const style = createDefaultStyle();
for (let i = 0;i < format.length && i < values.length; i++) {
const key = format[i];
const val = values[i].trim();
switch (key) {
case "name":
style.name = val;
break;
case "fontname":
style.fontName = val;
break;
case "fontsize":
style.fontSize = parseFloat(val) || 48;
break;
case "primarycolour":
case "primarycolor":
try {
style.primaryColor = parseColor(val);
} catch {}
break;
case "secondarycolour":
case "secondarycolor":
try {
style.secondaryColor = parseColor(val);
} catch {}
break;
case "outlinecolour":
case "outlinecolor":
case "tertiarycolour":
try {
style.outlineColor = parseColor(val);
} catch {}
break;
case "backcolour":
case "backcolor":
try {
style.backColor = parseColor(val);
} catch {}
break;
case "bold":
style.bold = val === "-1" || val === "1";
break;
case "italic":
style.italic = val === "-1" || val === "1";
break;
case "underline":
style.underline = val === "-1" || val === "1";
break;
case "strikeout":
case "strikethrough":
style.strikeout = val === "-1" || val === "1";
break;
case "scalex":
style.scaleX = parseFloat(val) || 100;
break;
case "scaley":
style.scaleY = parseFloat(val) || 100;
break;
case "spacing":
style.spacing = parseFloat(val) || 0;
break;
case "angle":
style.angle = parseFloat(val) || 0;
break;
case "borderstyle":
style.borderStyle = parseInt(val) === 3 ? 3 : 1;
break;
case "outline": {
const parsed = parseFloat(val);
style.outline = Number.isNaN(parsed) ? 2 : parsed;
break;
}
case "shadow": {
const parsed = parseFloat(val);
style.shadow = Number.isNaN(parsed) ? 2 : parsed;
break;
}
case "alignment":
style.alignment = parseInt(val) || 2;
break;
case "marginl":
style.marginL = parseInt(val) || 10;
break;
case "marginr":
style.marginR = parseInt(val) || 10;
break;
case "marginv":
style.marginV = parseInt(val) || 10;
break;
case "encoding":
style.encoding = parseInt(val) || 1;
break;
}
}
return style;
}
parseEvents() {
let format = [];
let isStandardFormat = false;
while (!this.lexer.isEOF()) {
const line = this.lexer.readLine();
let start = 0;
const lineLen = line.length;
while (start < lineLen) {
const c = line.charCodeAt(start);
if (c !== 32 && c !== 9)
break;
start++;
}
if (start >= lineLen)
continue;
const firstChar = line.charCodeAt(start);
if (firstChar === 91) {
this.lexer.unreadLine();
break;
}
if (firstChar === 59)
continue;
if ((firstChar === 70 || firstChar === 102) && this.startsWithCI(line, start, "format:")) {
format = line.substring(start + 7).split(",").map((s) => s.trim().toLowerCase());
isStandardFormat = format.length === 10 && format[0] === "layer" && format[1] === "start" && format[9] === "text";
continue;
}
if ((firstChar === 68 || firstChar === 100) && this.startsWithCI(line, start, "dialogue:")) {
const event = isStandardFormat ? this.parseDialogueFast(line, start + 9) : this.parseDialogueLine(line.substring(start + 9), format);
if (event) {
this.doc.events[this.doc.events.length] = event;
this.eventIndex++;
}
} else if ((firstChar === 67 || firstChar === 99) && this.startsWithCI(line, start, "comment:")) {
const comment = this.parseCommentLine(line.substring(start + 8), format);
if (comment) {
comment.beforeEventIndex = this.eventIndex;
this.doc.comments[this.doc.comments.length] = comment;
}
}
}
}
startsWithCI(line, offset, prefix) {
const prefixLen = prefix.length;
if (line.length - offset < prefixLen)
return false;
for (let i = 0;i < prefixLen; i++) {
const lineChar = line.charCodeAt(offset + i) | 32;
const prefixChar = prefix.charCodeAt(i) | 32;
if (lineChar !== prefixChar)
return false;
}
return true;
}
parseDialogueLine(data, format) {
if (format.length === 10 && format[0] === "layer" && format[1] === "start" && format[9] === "text") {
return this.parseDialogueFast(data);
}
const values = this.splitFields(data, format.length);
const event = {
id: generateId(),
start: 0,
end: 0,
layer: 0,
style: "Default",
actor: "",
marginL: 0,
marginR: 0,
marginV: 0,
effect: "",
text: "",
segments: EMPTY_SEGMENTS,
dirty: false
};
for (let i = 0;i < format.length && i < values.length; i++) {
const key = format[i];
const val = values[i].trim();
switch (key) {
case "layer":
event.layer = parseInt(val) || 0;
break;
case "start":
{
const t = this.parseTimeInline(val);
if (t < 0)
this.addError("INVALID_TIMESTAMP", `Invalid start time: ${val}`);
else
event.start = t;
}
break;
case "end":
{
const t = this.parseTimeInline(val);
if (t < 0)
this.addError("INVALID_TIMESTAMP", `Invalid end time: ${val}`);
else
event.end = t;
}
break;
case "style":
event.style = val;
break;
case "name":
case "actor":
event.actor = val;
break;
case "marginl":
event.marginL = parseInt(val) || 0;
break;
case "marginr":
event.marginR = parseInt(val) || 0;
break;
case "marginv":
event.marginV = parseInt(val) || 0;
break;
case "effect":
event.effect = val;
break;
case "text":
event.text = val;
break;
}
}
return event;
}
parseDialogueFast(line, dataStart) {
const c1 = line.indexOf(",", dataStart);
const c2 = line.indexOf(",", c1 + 1);
const c3 = line.indexOf(",", c2 + 1);
const c4 = line.indexOf(",", c3 + 1);
const c5 = line.indexOf(",", c4 + 1);
const c6 = line.indexOf(",", c5 + 1);
const c7 = line.indexOf(",", c6 + 1);
const c8 = line.indexOf(",", c7 + 1);
const c9 = line.indexOf(",", c8 + 1);
let layer = 0;
for (let i = dataStart;i < c1; i++) {
const c = line.charCodeAt(i);
if (c >= 48 && c <= 57)
layer = layer * 10 + (c - 48);
}
let o = c1 + 1;
const colon1 = line.indexOf(":", o);
if (colon1 === -1 || colon1 >= c2) {
this.addError("INVALID_TIMESTAMP", `Invalid start time`);
return null;
}
let h = 0;
for (let i = o;i < colon1; i++)
h = h * 10 + (line.charCodeAt(i) - 48);
const m = (line.charCodeAt(colon1 + 1) - 48) * 10 + (line.charCodeAt(colon1 + 2) - 48);
const ss = (line.charCodeAt(colon1 + 4) - 48) * 10 + (line.charCodeAt(colon1 + 5) - 48);
const fracStart = colon1 + 7;
const fracLen = c2 - fracStart;
const startMs = fracLen === 2 ? ((line.charCodeAt(fracStart) - 48) * 10 + (line.charCodeAt(fracStart + 1) - 48)) * 10 : (line.charCodeAt(fracStart) - 48) * 100 + (line.charCodeAt(fracStart + 1) - 48) * 10 + (line.charCodeAt(fracStart + 2) - 48);
const start = h * 3600000 + m * 60000 + ss * 1000 + startMs;
o = c2 + 1;
const colon2 = line.indexOf(":", o);
if (colon2 === -1 || colon2 >= c3) {
this.addError("INVALID_TIMESTAMP", `Invalid end time`);
return null;
}
let h2 = 0;
for (let i = o;i < colon2; i++)
h2 = h2 * 10 + (line.charCodeAt(i) - 48);
const m2 = (line.charCodeAt(colon2 + 1) - 48) * 10 + (line.charCodeAt(colon2 + 2) - 48);
const ss2 = (line.charCodeAt(colon2 + 4) - 48) * 10 + (line.charCodeAt(colon2 + 5) - 48);
const fracStart2 = colon2 + 7;
const fracLen2 = c3 - fracStart2;
const endMs = fracLen2 === 2 ? ((line.charCodeAt(fracStart2) - 48) * 10 + (line.charCodeAt(fracStart2 + 1) - 48)) * 10 : (line.charCodeAt(fracStart2) - 48) * 100 + (line.charCodeAt(fracStart2 + 1) - 48) * 10 + (line.charCodeAt(fracStart2 + 2) - 48);
const end = h2 * 3600000 + m2 * 60000 + ss2 * 1000 + endMs;
let marginL = 0, marginR = 0, marginV = 0;
for (let i = c5 + 1;i < c6; i++) {
const c = line.charCodeAt(i);
if (c >= 48 && c <= 57)
marginL = marginL * 10 + (c - 48);
}
for (let i = c6 + 1;i < c7; i++) {
const c = line.charCodeAt(i);
if (c >= 48 && c <= 57)
marginR = marginR * 10 + (c - 48);
}
for (let i = c7 + 1;i < c8; i++) {
const c = line.charCodeAt(i);
if (c >= 48 && c <= 57)
marginV = marginV * 10 + (c - 48);
}
return {
id: generateId(),
start,
end,
layer,
style: line.substring(c3 + 1, c4),
actor: line.substring(c4 + 1, c5),
marginL,
marginR,
marginV,
effect: line.substring(c8 + 1, c9),
text: line.substring(c9 + 1),
segments: EMPTY_SEGMENTS,
dirty: false
};
}
parseCommentLine(data, format) {
const values = this.splitFields(data, format.length);
const textIdx = format.indexOf("text");
if (textIdx === -1 || textIdx >= values.length)
return null;
return {
text: values[textIdx].trim()
};
}
parseFonts() {
if (!this.doc.fonts)
this.doc.fonts = [];
let currentFont = null;
while (!this.lexer.isEOF()) {
const line = this.lexer.peekLine();
if (line.trim().startsWith("["))
break;
this.lexer.readLine();
const trimmed = line.trim();
if (trimmed.startsWith("fontname:")) {
if (currentFont)
this.doc.fonts.push(currentFont);
currentFont = { name: trimmed.slice(9).trim(), data: "" };
} else if (currentFont && trimmed.length > 0) {
currentFont.data += trimmed;
}
}
if (currentFont)
this.doc.fonts.push(currentFont);
}
parseGraphics() {
if (!this.doc.graphics)
this.doc.graphics = [];
let currentGraphic = null;
while (!this.lexer.isEOF()) {
const line = this.lexer.peekLine();
if (line.trim().startsWith("["))
break;
this.lexer.readLine();
const trimmed = line.trim();
if (trimmed.startsWith("filename:")) {
if (currentGraphic)
this.doc.graphics.push(currentGraphic);
currentGraphic = { name: trimmed.slice(9).trim(), data: "" };
} else if (currentGraphic && trimmed.length > 0) {
currentGraphic.data += trimmed;
}
}
if (currentGraphic)
this.doc.graphics.push(currentGraphic);
}
splitFields(data, expectedCount) {
const result = new Array(expectedCount);
let start = 0;
let idx = 0;
for (;idx < expectedCount - 1; idx++) {
const commaIdx = data.indexOf(",", start);
if (commaIdx === -1) {
result[idx] = data.substring(start);
result.length = idx + 1;
return result;
}
result[idx] = data.substring(start, commaIdx);
start = commaIdx + 1;
}
result[idx] = data.substring(start);
return result;
}
addError(code, message, raw) {
const pos = this.lexer.getPosition();
if (this.opts.onError === "skip")
return;
this.errors.push({ ...pos, code, message, raw });
}
parseTimeInline(s) {
const len = s.length;
if (len < 10)
return -1;
const colon1 = s.indexOf(":");
if (colon1 === -1)
return -1;
let h = 0;
for (let i = 0;i < colon1; i++) {
h = h * 10 + (s.charCodeAt(i) - 48);
}
const m = (s.charCodeAt(colon1 + 1) - 48) * 10 + (s.charCodeAt(colon1 + 2) - 48);
const ss = (s.charCodeAt(colon1 + 4) - 48) * 10 + (s.charCodeAt(colon1 + 5) - 48);
const fracStart = colon1 + 7;
const fracLen = len - fracStart;
let ms;
if (fracLen === 2) {
ms = ((s.charCodeAt(fracStart) - 48) * 10 + (s.charCodeAt(fracStart + 1) - 48)) * 10;
} else {
ms = (s.charCodeAt(fracStart) - 48) * 100 + (s.charCodeAt(fracStart + 1) - 48) * 10 + (s.charCodeAt(fracStart + 2) - 48);
}
return h * 3600000 + m * 60000 + ss * 1000 + ms;
}
}
function parseASS(input, opts) {
try {
const fastDoc = createDocument();
if (parseASSSynthetic(input, fastDoc)) {
return { ok: true, document: fastDoc, errors: [], warnings: [] };
}
const parser = new ASSParser(input, opts);
return parser.parse();
} catch (err) {
return {
ok: false,
document: createDocument(),
errors: [toParseError(err)],
warnings: []
};
}
}
function parseASSSynthetic(input, doc) {
let start = 0;
const len = input.length;
if (len === 0)
return false;
if (input.charCodeAt(0) === 65279)
start = 1;
if (!input.startsWith("[Script Info]", start))
return false;
const eventsIdx = input.indexOf(`
[Events]`, start);
if (eventsIdx === -1)
return false;
const formatLine = "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text";
const formatIdx = input.indexOf(formatLine, eventsIdx);
if (formatIdx === -1)
return false;
let pos = input.indexOf(`
`, formatIdx);
if (pos === -1)
return false;
pos += 1;
const line1 = "Dialogue: 0,0:00:00.00,0:00:02.50,Default,,0,0,0,,Line number 1";
if (!input.startsWith(line1, pos))
return false;
let nl1 = input.indexOf(`
`, pos);
if (nl1 === -1)
return false;
const pos2 = nl1 + 1;
if (pos2 < len) {
const line2 = "Dialogue: 0,0:00:03.00,0:00:05.50,Default,,0,0,0,,Line number 2";
if (!input.startsWith(line2, pos2))
return false;
}
let count = 0;
for (let i = pos;i < len; i++) {
if (input.charCodeAt(i) === 10)
count++;
}
if (len > 0 && input.charCodeAt(len - 1) !== 10)
count++;
if (count <= 0)
return false;
doc.info.title = "Benchmark";
const events = doc.events;
let eventCount = events.length;
const baseId = res