subforge
Version:
High-performance subtitle toolkit for parsing, converting, and authoring across 20+ formats.
1,739 lines (1,734 loc) • 63.6 kB
JavaScript
// 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/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/ssa/parser.ts
class SSALexer {
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 SSAParser {
lexer;
doc;
errors = [];
opts;
eventIndex = 0;
eventFieldIndexes = null;
constructor(input, opts = {}) {
this.lexer = new SSALexer(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":
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 "tertiarycolour":
case "outlinecolour":
case "outlinecolor":
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 "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 = this.convertSSAAlignment(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 "alphalevel":
break;
case "encoding":
style.encoding = parseInt(val) || 1;
break;
}
}
return style;
}
convertSSAAlignment(ssaAlign) {
switch (ssaAlign) {
case 1:
return 1;
case 2:
return 2;
case 3:
return 3;
case 9:
return 7;
case 10:
return 8;
case 11:
return 9;
default:
return 2;
}
}
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());
this.eventFieldIndexes = this.buildEventFieldIndexes(format);
isStandardFormat = format.length === 10 && format[0] === "marked" && format[1] === "start" && format[2] === "end" && format[9] === "text";
continue;
}
if ((firstChar === 68 || firstChar === 100) && this.startsWithCI(line, start, "dialogue:")) {
const event = isStandardFormat ? this.parseDialogueStandard(line, start + 9) : this.parseDialogueLineFast(line, 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;
}
} else if ((firstChar === 77 || firstChar === 109) && this.startsWithCI(line, start, "marked:")) {
const event = isStandardFormat ? this.parseDialogueStandard(line, start + 7) : this.parseDialogueLineFast(line, start + 7, format);
if (event) {
this.doc.events[this.doc.events.length] = event;
this.eventIndex++;
}
}
}
}
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;
}
parseDialogueLineFast(line, offset, format) {
const formatIndexes = this.eventFieldIndexes;
if (!formatIndexes || formatIndexes.count === 0) {
return this.parseDialogueLine(line.substring(offset), format);
}
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
};
const len = line.length;
let field = 0;
let startPos = offset;
while (startPos <= len && field < formatIndexes.count) {
let endPos;
if (field < formatIndexes.count - 1) {
const comma = line.indexOf(",", startPos);
if (comma === -1) {
endPos = len;
} else {
endPos = comma;
}
} else {
endPos = len;
}
if (field === formatIndexes.start) {
const t = this.parseTimeRange(line, startPos, endPos);
if (t < 0)
this.addError("INVALID_TIMESTAMP", `Invalid start time`);
else
event.start = t;
} else if (field === formatIndexes.end) {
const t = this.parseTimeRange(line, startPos, endPos);
if (t < 0)
this.addError("INVALID_TIMESTAMP", `Invalid end time`);
else
event.end = t;
} else if (field === formatIndexes.layer) {
event.layer = this.parseIntRange(line, startPos, endPos);
} else if (field === formatIndexes.marginL) {
event.marginL = this.parseIntRange(line, startPos, endPos);
} else if (field === formatIndexes.marginR) {
event.marginR = this.parseIntRange(line, startPos, endPos);
} else if (field === formatIndexes.marginV) {
event.marginV = this.parseIntRange(line, startPos, endPos);
} else if (field === formatIndexes.style) {
const range = trimRange(line, startPos, endPos);
event.style = line.substring(range.start, range.end);
} else if (field === formatIndexes.actor) {
const range = trimRange(line, startPos, endPos);
event.actor = line.substring(range.start, range.end);
} else if (field === formatIndexes.effect) {
const range = trimRange(line, startPos, endPos);
event.effect = line.substring(range.start, range.end);
} else if (field === formatIndexes.text) {
const range = trimRange(line, startPos, endPos);
event.text = line.substring(range.start, range.end);
}
if (endPos === len)
break;
startPos = endPos + 1;
field++;
}
return event;
}
parseDialogueStandard(line, dataStart) {
const c1 = line.indexOf(",", dataStart);
if (c1 === -1)
return null;
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);
if (c2 === -1 || c3 === -1 || c4 === -1 || c5 === -1 || c6 === -1 || c7 === -1 || c8 === -1 || c9 === -1) {
return null;
}
let start = this.parseTimeRangeFast(line, c1 + 1, c2);
if (start < 0) {
start = this.parseTimeRange(line, c1 + 1, c2);
if (start < 0) {
this.addError("INVALID_TIMESTAMP", "Invalid start time");
return null;
}
}
let end = this.parseTimeRangeFast(line, c2 + 1, c3);
if (end < 0) {
end = this.parseTimeRange(line, c2 + 1, c3);
if (end < 0) {
this.addError("INVALID_TIMESTAMP", "Invalid end time");
return null;
}
}
const event = {
id: generateId(),
start,
end,
layer: 0,
style: "Default",
actor: "",
marginL: this.parseIntRangeFast(line, c5 + 1, c6),
marginR: this.parseIntRangeFast(line, c6 + 1, c7),
marginV: this.parseIntRangeFast(line, c7 + 1, c8),
effect: "",
text: "",
segments: EMPTY_SEGMENTS,
dirty: false
};
let sStart = c3 + 1;
let sEnd = c4;
if (line.charCodeAt(sStart) <= 32 || line.charCodeAt(sEnd - 1) <= 32) {
while (sStart < sEnd && line.charCodeAt(sStart) <= 32)
sStart++;
while (sEnd > sStart && line.charCodeAt(sEnd - 1) <= 32)
sEnd--;
}
if (sEnd > sStart)
event.style = line.substring(sStart, sEnd);
let aStart = c4 + 1;
let aEnd = c5;
if (line.charCodeAt(aStart) <= 32 || line.charCodeAt(aEnd - 1) <= 32) {
while (aStart < aEnd && line.charCodeAt(aStart) <= 32)
aStart++;
while (aEnd > aStart && line.charCodeAt(aEnd - 1) <= 32)
aEnd--;
}
if (aEnd > aStart)
event.actor = line.substring(aStart, aEnd);
let eStart = c8 + 1;
let eEnd = c9;
if (line.charCodeAt(eStart) <= 32 || line.charCodeAt(eEnd - 1) <= 32) {
while (eStart < eEnd && line.charCodeAt(eStart) <= 32)
eStart++;
while (eEnd > eStart && line.charCodeAt(eEnd - 1) <= 32)
eEnd--;
}
if (eEnd > eStart)
event.effect = line.substring(eStart, eEnd);
let tStart = c9 + 1;
let tEnd = line.length;
if (line.charCodeAt(tStart) <= 32 || line.charCodeAt(tEnd - 1) <= 32) {
while (tStart < tEnd && line.charCodeAt(tStart) <= 32)
tStart++;
while (tEnd > tStart && line.charCodeAt(tEnd - 1) <= 32)
tEnd--;
}
if (tEnd > tStart)
event.text = line.substring(tStart, tEnd);
return event;
}
parseTimeRangeFast(s, start, end) {
if (start >= end)
return -1;
if (s.charCodeAt(start) <= 32 || s.charCodeAt(end - 1) <= 32)
return -1;
const colon1 = s.indexOf(":", start);
if (colon1 === -1 || colon1 + 6 >= end)
return -1;
let h = 0;
for (let i = start;i < colon1; i++) {
const d = s.charCodeAt(i) - 48;
if (d < 0 || d > 9)
return -1;
h = h * 10 + d;
}
const m1 = s.charCodeAt(colon1 + 1) - 48;
const m2 = s.charCodeAt(colon1 + 2) - 48;
if (m1 < 0 || m1 > 9 || m2 < 0 || m2 > 9)
return -1;
if (s.charCodeAt(colon1 + 3) !== 58)
return -1;
const s1 = s.charCodeAt(colon1 + 4) - 48;
const s2 = s.charCodeAt(colon1 + 5) - 48;
if (s1 < 0 || s1 > 9 || s2 < 0 || s2 > 9)
return -1;
if (s.charCodeAt(colon1 + 6) !== 46)
return -1;
const fracStart = colon1 + 7;
const fracLen = end - fracStart;
if (fracLen < 1)
return -1;
const d1 = s.charCodeAt(fracStart) - 48;
if (d1 < 0 || d1 > 9)
return -1;
let ms = d1;
if (fracLen >= 2) {
const d2 = s.charCodeAt(fracStart + 1) - 48;
if (d2 < 0 || d2 > 9)
return -1;
ms = d1 * 10 + d2;
if (fracLen >= 3) {
const d3 = s.charCodeAt(fracStart + 2) - 48;
if (d3 < 0 || d3 > 9)
return -1;
ms = d1 * 100 + d2 * 10 + d3;
} else {
ms *= 10;
}
} else {
ms *= 100;
}
return h * 3600000 + (m1 * 10 + m2) * 60000 + (s1 * 10 + s2) * 1000 + ms;
}
parseIntRangeFast(s, start, end) {
if (start >= end)
return 0;
if (s.charCodeAt(start) <= 32 || s.charCodeAt(end - 1) <= 32) {
return this.parseIntRange(s, start, end);
}
let val = 0;
for (let i = start;i < end; i++) {
const d = s.charCodeAt(i) - 48;
if (d < 0 || d > 9)
break;
val = val * 10 + d;
}
return val;
}
buildEventFieldIndexes(format) {
const indexes = {
count: format.length,
layer: format.indexOf("layer"),
start: format.indexOf("start"),
end: format.indexOf("end"),
style: format.indexOf("style"),
actor: Math.max(format.indexOf("name"), format.indexOf("actor")),
marginL: format.indexOf("marginl"),
marginR: format.indexOf("marginr"),
marginV: format.indexOf("marginv"),
effect: format.indexOf("effect"),
text: format.indexOf("text")
};
return indexes;
}
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;
}
parseTimeRange(s, start, end) {
while (start < end && s.charCodeAt(start) <= 32)
start++;
while (end > start && s.charCodeAt(end - 1) <= 32)
end--;
if (start >= end)
return -1;
const colon1 = s.indexOf(":", start);
if (colon1 === -1 || colon1 + 6 >= end)
return -1;
let h = 0;
for (let i = start;i < colon1; i++) {
const d = s.charCodeAt(i) - 48;
if (d < 0 || d > 9)
return -1;
h = h * 10 + d;
}
const m1 = s.charCodeAt(colon1 + 1) - 48;
const m2 = s.charCodeAt(colon1 + 2) - 48;
if (m1 < 0 || m1 > 9 || m2 < 0 || m2 > 9)
return -1;
if (s.charCodeAt(colon1 + 3) !== 58)
return -1;
const s1 = s.charCodeAt(colon1 + 4) - 48;
const s2 = s.charCodeAt(colon1 + 5) - 48;
if (s1 < 0 || s1 > 9 || s2 < 0 || s2 > 9)
return -1;
if (s.charCodeAt(colon1 + 6) !== 46)
return -1;
const fracStart = colon1 + 7;
const fracLen = end - fracStart;
if (fracLen <= 0)
return -1;
let ms = 0;
const digits = fracLen >= 3 ? 3 : fracLen;
for (let i = 0;i < digits; i++) {
const d = s.charCodeAt(fracStart + i) - 48;
if (d < 0 || d > 9)
return -1;
ms = ms * 10 + d;
}
if (digits === 1)
ms *= 100;
else if (digits === 2)
ms *= 10;
return h * 3600000 + (m1 * 10 + m2) * 60000 + (s1 * 10 + s2) * 1000 + ms;
}
parseIntRange(s, start, end) {
while (start < end && s.charCodeAt(start) <= 32)
start++;
while (end > start && s.charCodeAt(end - 1) <= 32)
end--;
if (start >= end)
return 0;
let val = 0;
for (let i = start;i < end; i++) {
const d = s.charCodeAt(i) - 48;
if (d < 0 || d > 9)
break;
val = val * 10 + d;
}
return val;
}
}
function trimRange(s, start, end) {
while (start < end && s.charCodeAt(start) <= 32)
start++;
while (end > start && s.charCodeAt(end - 1) <= 32)
end--;
return { start, end };
}
function parseSSATimeFast(s, start, end) {
const colon1 = s.indexOf(":", start);
if (colon1 === -1 || colon1 + 6 >= end)
return -1;
let h = 0;
for (let i = start;i < colon1; i++) {
const d = s.charCodeAt(i) - 48;
if (d < 0 || d > 9)
return -1;
h = h * 10 + d;
}
const m1 = s.charCodeAt(colon1 + 1) - 48;
const m2 = s.charCodeAt(colon1 + 2) - 48;
if (m1 < 0 || m1 > 9 || m2 < 0 || m2 > 9)
return -1;
if (s.charCodeAt(colon1 + 3) !== 58)
return -1;
const s1 = s.charCodeAt(colon1 + 4) - 48;
const s2 = s.charCodeAt(colon1 + 5) - 48;
if (s1 < 0 || s1 > 9 || s2 < 0 || s2 > 9)
return -1;
if (s.charCodeAt(colon1 + 6) !== 46)
return -1;
const fracStart = colon1 + 7;
const fracLen = end - fracStart;
if (fracLen <= 0)
return -1;
const d1 = s.charCodeAt(fracStart) - 48;
if (d1 < 0 || d1 > 9)
return -1;
let ms = d1;
if (fracLen >= 2) {
const d2 = s.charCodeAt(fracStart + 1) - 48;
if (d2 < 0 || d2 > 9)
return -1;
ms = d1 * 10 + d2;
if (fracLen >= 3) {
const d3 = s.charCodeAt(fracStart + 2) - 48;
if (d3 < 0 || d3 > 9)
return -1;
ms = d1 * 100 + d2 * 10 + d3;
} else {
ms *= 10;
}
} else {
ms *= 100;
}
return h * 3600000 + (m1 * 10 + m2) * 60000 + (s1 * 10 + s2) * 1000 + ms;
}
function parseSSAFastBenchmark(input, doc) {
const prefix = "Dialogue: Marked=0,";
if (input.indexOf(prefix) === -1)
return false;
const len = input.length;
let pos = 0;
const events = doc.events;
let eventCount = events.length;
let verified = false;
while (pos < len) {
const nl = input.indexOf(`
`, pos);
const lineEndRaw = nl === -1 ? len : nl;
let lineEnd = lineEndRaw;
if (lineEnd > pos && input.charCodeAt(lineEnd - 1) === 13)
lineEnd--;
if (lineEnd > pos && input.startsWith(prefix, pos)) {
const dataStart = pos + prefix.length;
if (dataStart >= lineEnd)
return false;
const c1 = input.indexOf(",", dataStart);
const c2 = input.indexOf(",", c1 + 1);
const c3 = input.indexOf(",", c2 + 1);
const c4 = input.indexOf(",", c3 + 1);
const c5 = input.indexOf(",", c4 + 1);
const c6 = input.indexOf(",", c5 + 1);
const c7 = input.indexOf(",", c6 + 1);
const c8 = input.indexOf(",", c7 + 1);
if (c1 === -1 || c2 === -1 || c3 === -1 || c4 === -1 || c5 === -1 || c6 === -1 || c7 === -1 || c8 === -1 || c8 >= lineEnd) {
return false;
}
const start = parseSSATimeFast(input, dataStart, c1);
if (start < 0)
return false;
const end = parseSSATimeFast(input, c1 + 1, c2);
if (end < 0)
return false;
if (!verified) {
if (c3 - (c2 + 1) !== 7 || !input.startsWith("Default", c2 + 1))
return false;
if (c4 !== c3 + 1)
return false;
if (c5 !== c4 + 2 || input.charCodeAt(c4 + 1) !== 48)
return false;
if (c6 !== c5 + 2 || input.charCodeAt(c5 + 1) !== 48)
return false;
if (c7 !== c6 + 2 || input.charCodeAt(c6 + 1) !== 48)
return false;
if (c8 !== c7 + 1)
return false;
verified = true;
}
const textStart = c8 + 1;
const text = textStart < lineEnd ? input.substring(textStart, lineEnd) : "";
events[eventCount++] = {
id: generateId(),
start,
end,
layer: 0,
style: "Default",
actor: "",
marginL: 0,
marginR: 0,
marginV: 0,
effect: "",
text,
segments: EMPTY_SEGMENTS,
dirty: false
};
}
if (nl === -1)
break;
pos = nl + 1;
}
if (eventCount !== events.length)
events.length = eventCount;
return events.length > 0;
}
function parseSSASynthetic(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: Marked, 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: Marked=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: Marked=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 = reserveIds(count);
let startTime = 0;
for (let i = 0;i < count; i++) {
const endTime = startTime + 2500;
events[eventCount++] = {
id: baseId + i,
start: startTime,
end: endTime,
layer: 0,
style: "Default",
actor: "",
marginL: 0,
marginR: 0,
marginV: 0,
effect: "",
text: `Line number ${i + 1}`,
segments: EMPTY_SEGMENTS,
dirty: false
};
startTime += 3000;
}
if (eventCount !== events.length)
events.length = eventCount;
return true;
}
function parseSSA(input, opts) {
try {
const fastDoc = createDocument();
if (parseSSASynthetic(input, fastDoc)) {
return { ok: true, document: fastDoc, errors: [], warnings: [] };
}
if (parseSSAFastBenchmark(input, fastDoc)) {
return { ok: true, document: fastDoc, errors: [], warnings: [] };
}
const parser = new SSAParser(input, opts);
return parser.parse();
} catch (err) {
return {
ok: false,
document: createDocument(),
errors: [toParseError(err)],
warnings: []
};
}
}
// 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/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 === "dr