UNPKG

vidstack

Version:

Build awesome media experiences on the web.

294 lines (291 loc) 10.4 kB
import { V as VTTCue, f as parseVTTTimestamp } from './index.js'; const FORMAT_START_RE = /^Format:[\s\t]*/, STYLE_START_RE = /^Style:[\s\t]*/, DIALOGUE_START_RE = /^Dialogue:[\s\t]*/, FORMAT_SPLIT_RE = /[\s\t]*,[\s\t]*/, STYLE_FUNCTION_RE = /\{[^}]+\}/g, NEW_LINE_RE = /\\N/g, STYLES_SECTION_START_RE = /^\[(.*)[\s\t]?Styles\]$/, EVENTS_SECTION_START_RE = /^\[(.*)[\s\t]?Events\]$/; class SSAParser { _init; _section = 0 /* None */; _cue = null; _cues = []; _errors = []; _format = null; _errorBuilder; _styles = {}; async init(init) { this._init = init; if (init.errors) this._errorBuilder = (await import('./errors.js')).ParseErrorBuilder; } parse(line, lineCount) { if (this._section) { switch (this._section) { case 1 /* Style */: if (line === "") { this._section = 0 /* None */; } else if (STYLE_START_RE.test(line)) { if (this._format) { const styles = line.replace(STYLE_START_RE, "").split(FORMAT_SPLIT_RE); this._parseStyles(styles); } else { this._handleError(this._errorBuilder?._missingFormat("Style", lineCount)); } } else if (FORMAT_START_RE.test(line)) { this._format = line.replace(FORMAT_START_RE, "").split(FORMAT_SPLIT_RE); } else if (EVENTS_SECTION_START_RE.test(line)) { this._format = null; this._section = 2 /* Event */; } break; case 2 /* Event */: if (line === "") { this._commitCue(); } else if (DIALOGUE_START_RE.test(line)) { this._commitCue(); if (this._format) { const dialogue = line.replace(DIALOGUE_START_RE, "").split(FORMAT_SPLIT_RE), cue = this._parseDialogue(dialogue, lineCount); if (cue) this._cue = cue; } else { this._handleError(this._errorBuilder?._missingFormat("Dialogue", lineCount)); } } else if (this._cue) { this._cue.text += "\n" + line.replace(STYLE_FUNCTION_RE, "").replace(NEW_LINE_RE, "\n"); } else if (FORMAT_START_RE.test(line)) { this._format = line.replace(FORMAT_START_RE, "").split(FORMAT_SPLIT_RE); } else if (STYLES_SECTION_START_RE.test(line)) { this._format = null; this._section = 1 /* Style */; } else if (EVENTS_SECTION_START_RE.test(line)) { this._format = null; } } } else if (line === "") ; else if (STYLES_SECTION_START_RE.test(line)) { this._format = null; this._section = 1 /* Style */; } else if (EVENTS_SECTION_START_RE.test(line)) { this._format = null; this._section = 2 /* Event */; } } done() { return { metadata: {}, cues: this._cues, regions: [], errors: this._errors }; } _commitCue() { if (!this._cue) return; this._cues.push(this._cue); this._init.onCue?.(this._cue); this._cue = null; } _parseStyles(values) { let name = "Default", styles = {}, outlineX, align = "center", vertical = "bottom", marginV, outlineY = 1.2, outlineColor, bgColor, borderStyle = 3, transform = []; for (let i = 0; i < this._format.length; i++) { const field = this._format[i], value = values[i]; switch (field) { case "Name": name = value; break; case "Fontname": styles["font-family"] = value; break; case "Fontsize": styles["font-size"] = `calc(${value} / var(--overlay-height))`; break; case "PrimaryColour": const color = parseColor(value); if (color) styles["--cue-color"] = color; break; case "BorderStyle": borderStyle = parseInt(value, 10); break; case "BackColour": bgColor = parseColor(value); break; case "OutlineColour": const _outlineColor = parseColor(value); if (_outlineColor) outlineColor = _outlineColor; break; case "Bold": if (parseInt(value)) styles["font-weight"] = "bold"; break; case "Italic": if (parseInt(value)) styles["font-style"] = "italic"; break; case "Underline": if (parseInt(value)) styles["text-decoration"] = "underline"; break; case "StrikeOut": if (parseInt(value)) styles["text-decoration"] = "line-through"; break; case "Spacing": styles["letter-spacing"] = value + "px"; break; case "AlphaLevel": styles["opacity"] = parseFloat(value); break; case "ScaleX": transform.push(`scaleX(${parseFloat(value) / 100})`); break; case "ScaleY": transform.push(`scaleY(${parseFloat(value) / 100})`); break; case "Angle": transform.push(`rotate(${value}deg)`); break; case "Shadow": outlineY = parseInt(value, 10) * 1.2; break; case "MarginL": styles["--cue-width"] = "auto"; styles["--cue-left"] = parseFloat(value) + "px"; break; case "MarginR": styles["--cue-width"] = "auto"; styles["--cue-right"] = parseFloat(value) + "px"; break; case "MarginV": marginV = parseFloat(value); break; case "Outline": outlineX = parseInt(value, 10); break; case "Alignment": const alignment = parseInt(value, 10); if (alignment >= 4) vertical = alignment >= 7 ? "top" : "center"; switch (alignment % 3) { case 1: align = "start"; break; case 2: align = "center"; break; case 3: align = "end"; break; } } } styles._vertical = vertical; styles["--cue-white-space"] = "normal"; styles["--cue-line-height"] = "normal"; styles["--cue-text-align"] = align; if (vertical === "center") { styles[`--cue-top`] = "50%"; transform.push("translateY(-50%)"); } else { styles[`--cue-${vertical}`] = (marginV || 0) + "px"; } if (borderStyle === 1) { styles["--cue-padding-y"] = "0"; } if (borderStyle === 1 || bgColor) { styles["--cue-bg-color"] = borderStyle === 1 ? "none" : bgColor; } if (borderStyle === 3 && outlineColor) { styles["--cue-outline"] = `${outlineX}px solid ${outlineColor}`; } if (borderStyle === 1 && typeof outlineX === "number") { const color = bgColor ?? "#000"; styles["--cue-text-shadow"] = [ outlineColor && buildTextShadow(outlineX * 1.2, outlineY * 1.2, outlineColor), outlineColor ? buildTextShadow(outlineX * (outlineX / 2), outlineY * (outlineX / 2), color) : buildTextShadow(outlineX, outlineY, color) ].filter(Boolean).join(", "); } if (transform.length) styles["--cue-transform"] = transform.join(" "); this._styles[name] = styles; } _parseDialogue(values, lineCount) { const fields = this._buildFields(values); const timestamp = this._parseTimestamp(fields.Start, fields.End, lineCount); if (!timestamp) return; const cue = new VTTCue(timestamp[0], timestamp[1], ""), styles = { ...this._styles[fields.Style] || {} }, voice = fields.Name ? `<v ${fields.Name}>` : ""; const vertical = styles._vertical, marginLeft = fields.MarginL && parseFloat(fields.MarginL), marginRight = fields.MarginR && parseFloat(fields.MarginR), marginV = fields.MarginV && parseFloat(fields.MarginV); if (marginLeft) { styles["--cue-width"] = "auto"; styles["--cue-left"] = marginLeft + "px"; } if (marginRight) { styles["--cue-width"] = "auto"; styles["--cue-right"] = marginRight + "px"; } if (marginV && vertical !== "center") { styles[`--cue-${vertical}`] = marginV + "px"; } cue.text = voice + values.slice(this._format.length - 1).join(", ").replace(STYLE_FUNCTION_RE, "").replace(NEW_LINE_RE, "\n"); delete styles._vertical; if (Object.keys(styles).length) cue.style = styles; return cue; } _buildFields(values) { const fields = {}; for (let i = 0; i < this._format.length; i++) { fields[this._format[i]] = values[i]; } return fields; } _parseTimestamp(startTimeText, endTimeText, lineCount) { const startTime = parseVTTTimestamp(startTimeText), endTime = parseVTTTimestamp(endTimeText); if (startTime !== null && endTime !== null && endTime > startTime) { return [startTime, endTime]; } else { if (startTime === null) { this._handleError(this._errorBuilder?._badStartTimestamp(startTimeText, lineCount)); } if (endTime === null) { this._handleError(this._errorBuilder?._badEndTimestamp(endTimeText, lineCount)); } if (startTime != null && endTime !== null && endTime > startTime) { this._handleError(this._errorBuilder?._badRangeTimestamp(startTime, endTime, lineCount)); } } } _handleError(error) { if (!error) return; this._errors.push(error); if (this._init.strict) { this._init.cancel(); throw error; } else { this._init.onError?.(error); } } } function parseColor(color) { const abgr = parseInt(color.replace("&H", ""), 16); if (abgr >= 0) { const a = abgr >> 24 & 255 ^ 255; const alpha = a / 255; const b = abgr >> 16 & 255; const g = abgr >> 8 & 255; const r = abgr & 255; return "rgba(" + [r, g, b, alpha].join(",") + ")"; } return null; } function buildTextShadow(x, y, color) { const noOfShadows = Math.ceil(2 * Math.PI * x); let textShadow = ""; for (let i = 0; i < noOfShadows; i++) { const theta = 2 * Math.PI * i / noOfShadows; textShadow += x * Math.cos(theta) + "px " + y * Math.sin(theta) + "px 0 " + color + (i == noOfShadows - 1 ? "" : ","); } return textShadow; } function createSSAParser() { return new SSAParser(); } export { SSAParser, createSSAParser as default };