Private/Parsers/Invoke-AssParser.ps1
|
function Invoke-AssParser { <# .SYNOPSIS Parses ASS/SSA text content into a SubtitleFile object. .OUTPUTS SubtitleFile #> [OutputType('SubtitleFile')] param( [Parameter(Mandatory)] [string] $Content, [hashtable] $Warnings = @{} ) $normalized = ConvertTo-NormalizedText -Text $Content $lines = $normalized -split '\n' $file = [SubtitleFile]::new() $file.Format = 'ASS' $file.Header = [AssHeader]::new() $currentSection = '' $eventColumns = @() $dialogueIndex = 1 foreach ($line in $lines) { $trimmed = $line.Trim() # Section headers if ($trimmed -match '^\[(.+)\]$') { $currentSection = $Matches[1].ToLower() if ($currentSection -eq 'v4 styles' -or $currentSection -eq 'v4+ styles') { $file.Header.ScriptType = if ($currentSection -like '*+*') { 'v4.00+' } else { 'v4.00' } $file.Format = if ($currentSection -like '*+*') { 'ASS' } else { 'SSA' } } continue } if ([string]::IsNullOrWhiteSpace($trimmed) -or $trimmed.StartsWith(';')) { continue } switch -Regex ($currentSection) { 'script info' { if ($trimmed -match '^(.+?):\s*(.*)$') { $key = $Matches[1].Trim() $value = $Matches[2].Trim() switch ($key) { 'ScriptType' { $file.Header.ScriptType = $value } 'Title' { $file.Header.Title = $value } 'Original Script' { $file.Header.OriginalScript = $value } 'PlayResX' { $file.Header.PlayResX = $value } 'PlayResY' { $file.Header.PlayResY = $value } 'YCbCr Matrix' { $file.Header.YCbCrMatrix = $value } default { $file.Header.ExtraFields[$key] = $value } } } } '(v4\+? styles|v4 styles)' { if ($trimmed -match '^Format:\s*(.+)$') { # Store style column order (not used for parsing but preserved for output) } elseif ($trimmed -match '^Style:\s*(.+)$') { $styleLine = $Matches[1] $parts = $styleLine -split ',' if ($parts.Count -ge 23) { $style = [AssStyle]::new() $style.Name = $parts[0].Trim() $style.Fontname = $parts[1].Trim() $style.Fontsize = [int]($parts[2].Trim() -replace '[^\d]', '') $style.PrimaryColour = $parts[3].Trim() $style.SecondaryColour = $parts[4].Trim() $style.OutlineColour = $parts[5].Trim() $style.BackColour = $parts[6].Trim() $style.Bold = $parts[7].Trim() -eq '-1' $style.Italic = $parts[8].Trim() -eq '-1' $style.Underline = $parts[9].Trim() -eq '-1' $style.StrikeOut = $parts[10].Trim() -eq '-1' $style.ScaleX = [decimal]($parts[11].Trim()) $style.ScaleY = [decimal]($parts[12].Trim()) $style.Spacing = [decimal]($parts[13].Trim()) $style.Angle = [decimal]($parts[14].Trim()) $style.BorderStyle = [int]($parts[15].Trim()) $style.Outline = [decimal]($parts[16].Trim()) $style.Shadow = [decimal]($parts[17].Trim()) $style.Alignment = [int]($parts[18].Trim()) $style.MarginL = [int]($parts[19].Trim()) $style.MarginR = [int]($parts[20].Trim()) $style.MarginV = [int]($parts[21].Trim()) $style.Encoding = [int]($parts[22].Trim()) $file.Header.Styles += $style } else { $Warnings[$dialogueIndex] = "Style line has fewer than 23 fields: '$trimmed'" } } } 'events' { if ($trimmed -match '^Format:\s*(.+)$') { $eventColumns = ($Matches[1] -split ',') | ForEach-Object { $_.Trim() } $file.Header.EventColumnOrder = $eventColumns } elseif ($trimmed -match '^(Dialogue|Comment):\s*(.+)$') { $eventType = $Matches[1] $eventData = $Matches[2] # Split on commas but only up to the number of columns minus 1 # (Text field may contain commas) $maxSplit = $eventColumns.Count $parts = $eventData -split ',', $maxSplit if ($parts.Count -lt $maxSplit) { $Warnings[$dialogueIndex] = "Event line has fewer fields than Format line: '$trimmed'" continue } # Map columns by position $colMap = @{} for ($i = 0; $i -lt $eventColumns.Count; $i++) { $colMap[$eventColumns[$i]] = if ($i -lt $parts.Count) { $parts[$i].Trim() } else { '' } } # Text column may have trailing parts if we had extra commas — join them back $textColIndex = $eventColumns.IndexOf('Text') if ($textColIndex -ge 0 -and $parts.Count -gt $textColIndex) { $colMap['Text'] = ($parts[$textColIndex..($parts.Count - 1)]) -join ',' } $entry = [AssEntry]::new() $entry.Index = $dialogueIndex $entry.EventType = $eventType try { $entry.Start = ConvertFrom-AssTimestamp -Timestamp $colMap['Start'] $entry.End = ConvertFrom-AssTimestamp -Timestamp $colMap['End'] } catch { $Warnings[$dialogueIndex] = "Entry $dialogueIndex timestamp parse failed: $($_.Exception.Message)" $dialogueIndex++ continue } $entry.Layer = $colMap['Layer'] $entry.Style = $colMap['Style'] $entry.Name = $colMap['Name'] $entry.MarginL = $colMap['MarginL'] $entry.MarginR = $colMap['MarginR'] $entry.MarginV = $colMap['MarginV'] $entry.Effect = $colMap['Effect'] $rawText = $colMap['Text'] $entry.RawText = $rawText # Extract override tags, then get plain lines $plainText = $rawText -replace '\{[^}]*\}', '' $overrideTags = [regex]::Matches($rawText, '\{[^}]*\}') | ForEach-Object { $_.Value } $entry.OverrideTags = $overrideTags $entry.Lines = ($plainText -replace '\\N', "`n") -split "`n" # Only add Dialogue entries to the entries array # Comment entries are stored in metadata if ($eventType -eq 'Dialogue') { $file.Entries += $entry } $dialogueIndex++ } } } } $file.ParserWarnings = $Warnings return $file } |