diff --git a/QuickLook.Plugin/QuickLook.Plugin.TextViewer/Themes/HighlightingDefinitions/Dark/SubStationAlphaHighlightingDefinition.cs b/QuickLook.Plugin/QuickLook.Plugin.TextViewer/Themes/HighlightingDefinitions/Dark/SubStationAlphaHighlightingDefinition.cs
new file mode 100644
index 0000000..ef39b7f
--- /dev/null
+++ b/QuickLook.Plugin/QuickLook.Plugin.TextViewer/Themes/HighlightingDefinitions/Dark/SubStationAlphaHighlightingDefinition.cs
@@ -0,0 +1,685 @@
+// Copyright © 2017-2025 QL-Win Contributors
+//
+// This file is part of QuickLook program.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+using ICSharpCode.AvalonEdit.Document;
+using ICSharpCode.AvalonEdit.Highlighting;
+using ICSharpCode.AvalonEdit.Rendering;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text.RegularExpressions;
+
+namespace QuickLook.Plugin.TextViewer.Themes.HighlightingDefinitions.Dark;
+
+public class SubStationAlphaHighlightingDefinition : DarkHighlightingDefinition
+{
+ public override string Name => "SubStation Alpha";
+
+ public override string Extension => ".ass;.ssa";
+
+ public override HighlightingRuleSet MainRuleSet => new()
+ {
+ Rules =
+ {
+ new HighlightingRule
+ {
+ Regex = new Regex(@";.*", RegexOptions.Compiled),
+ Color = GetNamedColor("Comment")
+ },
+ new HighlightingRule
+ {
+ Regex = new Regex(@"!:.*", RegexOptions.Compiled),
+ Color = GetNamedColor("Comment")
+ },
+ new HighlightingRule
+ {
+ Regex = new Regex(@"Comment:.*", RegexOptions.Compiled),
+ Color = GetNamedColor("Comment")
+ },
+ }
+ };
+
+ public override HighlightingColor GetNamedColor(string name)
+ {
+ return name switch
+ {
+ "Comment" => new HighlightingColor
+ {
+ Name = "Comment",
+ Foreground = new SimpleHighlightingBrush("#6A9949".ToColor()),
+ },
+ _ => null,
+ };
+ }
+
+ public override IEnumerable NamedHighlightingColors =>
+ [
+ GetNamedColor("Comment"),
+ ];
+
+ public override DocumentColorizingTransformer[] LineTransformers { get; } = [new KeyHighlighter()];
+
+ public class KeyHighlighter : DocumentColorizingTransformer
+ {
+ public Regex SessionRegex { get; } = new(@"\[[^\[\]]*\]", RegexOptions.Compiled);
+
+ public SessionStore Sessions { get; } = [];
+
+ protected override void ColorizeLine(DocumentLine line)
+ {
+ var text = CurrentContext.Document.GetText(line);
+ var textTrimmed = text.TrimStart('\uFEFF').Trim(); // Skip UTF8-BOM (U+FEFF)
+
+ if (string.IsNullOrWhiteSpace(text) || textTrimmed.StartsWith(";") || textTrimmed.StartsWith("Comment:") || textTrimmed.StartsWith("!:"))
+ return;
+
+ if (textTrimmed.StartsWith("[") && SessionRegex.IsMatch(textTrimmed))
+ {
+ var match = Regex.Match(textTrimmed, @"\[(.*?)\]");
+
+ if (match.Success)
+ {
+ string sessionName = match.Groups[1].Value;
+
+ var idxStart = text.IndexOf('[');
+ var idxEnd = text.IndexOf(']');
+
+ // Session
+ ChangeLinePart(line.Offset + idxStart, line.Offset + idxStart + 1, el =>
+ {
+ el.TextRunProperties.SetForegroundBrush("#FFD705".ToBrush());
+ });
+ ChangeLinePart(line.Offset + idxEnd, line.Offset + idxEnd + 1, el =>
+ {
+ el.TextRunProperties.SetForegroundBrush("#FFD705".ToBrush());
+ });
+
+ if (!Sessions.ContainsKey(sessionName))
+ Sessions.Add(sessionName, new Session
+ {
+ Name = sessionName,
+ Formats = [],
+ LineNumber = line.LineNumber,
+ });
+ return;
+ }
+ }
+
+ // Events
+ // e.g. Comment: 0,0:00:19.41,0:00:21.87,Style1,,0,0,0,,ABCDEF
+ // e.g. Dialogue: 0,0:00:19.41,0:00:21.87,Style2,,0,0,0,,ABCDEF
+ if (Sessions.IsCurrentSession("Events", line.LineNumber))
+ {
+ if (text.StartsWith("Format:"))
+ {
+ Sessions["Events"].Formats = [.. text.Substring(text.IndexOf(':')).Split(',').Select(f => f.Trim())];
+ SetFormatForegroundBrush();
+ }
+ else if (text.StartsWith("Dialogue:"))
+ {
+ if (Sessions.ContainsKey("Events"))
+ {
+ SetEventForegroundBrush("Events");
+ }
+ }
+ }
+ // V4 Styles
+ // Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, TertiaryColour, BackColour, Bold, Italic, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, AlphaLevel, Encoding
+ else if (Sessions.IsCurrentSession("V4 Styles", line.LineNumber))
+ {
+ if (text.StartsWith("Format:"))
+ {
+ Sessions["V4 Styles"].Formats = [.. text.Substring("Format:".Length).Split(',').Select(f => f.Trim())];
+ SetFormatForegroundBrush();
+ }
+ else if (text.StartsWith("Style:"))
+ {
+ if (Sessions.ContainsKey("V4 Styles"))
+ {
+ SetStyleForegroundBrush("V4 Styles");
+ }
+ }
+ }
+ // V4+ Styles
+ // Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding, Layer
+ else if (Sessions.IsCurrentSession("V4+ Styles", line.LineNumber))
+ {
+ if (text.StartsWith("Format:"))
+ {
+ Sessions["V4+ Styles"].Formats = [.. text.Substring("Format:".Length).Split(',').Select(f => f.Trim())];
+ SetFormatForegroundBrush();
+ }
+ else if (text.StartsWith("Style:"))
+ {
+ if (Sessions.ContainsKey("V4+ Styles"))
+ {
+ SetStyleForegroundBrush("V4+ Styles");
+ }
+ }
+ }
+ // Script Info
+ // Aegisub Project Garbage
+ else
+ {
+ SetInfoForegroundBrush();
+ }
+
+ // Info
+ void SetInfoForegroundBrush()
+ {
+ int idx = text.IndexOf(':');
+
+ if (idx <= 0)
+ return;
+
+ var val = text.Substring(idx + 1);
+ var valTrimmed = val.Trim();
+ var type = DetecteType(val);
+
+ // Key
+ ChangeLinePart(line.Offset, line.Offset + idx, el =>
+ {
+ el.TextRunProperties.SetForegroundBrush("#3F9CD6".ToBrush());
+ });
+
+ // Value
+ ChangeLinePart(line.Offset + idx + 1, line.Offset + text.Length, el =>
+ {
+ el.TextRunProperties.SetForegroundBrush(GetValueColor(type).ToBrush());
+ });
+ }
+
+ // Style
+ void SetStyleForegroundBrush(string sessionName)
+ {
+ var sessionFormats = Sessions[sessionName].Formats;
+
+ int[] idxes = SpliteIndexes(text, ',');
+ int idxPrev = text.IndexOf(':');
+
+ ChangeLinePart(line.Offset, line.Offset + idxPrev, el =>
+ {
+ el.TextRunProperties.SetForegroundBrush("#3F9CD6".ToBrush());
+ });
+
+ for (int i = default; i < Math.Min(idxes.Length, sessionFormats.Length); i++)
+ {
+ int idxStart = idxPrev + 1;
+ int idxEnd = idxPrev = idxes[i];
+
+ var val = text.Substring(idxStart, idxEnd - idxStart);
+ var valTrimmed = val.Trim();
+ var type = DetecteType(val);
+
+ if (sessionFormats[i].EndsWith("Name", StringComparison.OrdinalIgnoreCase))
+ {
+ ChangeLinePart(line.Offset + idxStart, line.Offset + idxEnd, el =>
+ {
+ el.TextRunProperties.SetForegroundBrush(GetValueColor(ValueType.String).ToBrush());
+ });
+ }
+ else if (sessionFormats[i].EndsWith("Colour"))
+ {
+ ChangeLinePart(line.Offset + idxStart, line.Offset + idxEnd, el =>
+ {
+ el.TextRunProperties.SetForegroundBrush("#DCDCAA".ToBrush());
+ });
+ }
+ else
+ {
+ ChangeLinePart(line.Offset + idxStart, line.Offset + idxEnd, el =>
+ {
+ el.TextRunProperties.SetForegroundBrush(GetValueColor(type).ToBrush());
+ });
+ }
+ }
+ }
+
+ // Events
+ void SetEventForegroundBrush(string sessionName)
+ {
+ var sessionFormats = Sessions[sessionName].Formats;
+
+ int[] idxes = SpliteIndexes(text, ',');
+ int idxPrev = text.IndexOf(':');
+
+ ChangeLinePart(line.Offset, line.Offset + idxPrev, el =>
+ {
+ el.TextRunProperties.SetForegroundBrush("#3F9CD6".ToBrush());
+ });
+
+ for (int i = default; i < Math.Min(idxes.Length, sessionFormats.Length); i++)
+ {
+ int idxStart = idxPrev + 1;
+ int idxEnd = idxPrev = idxes[i];
+
+ var val = text.Substring(idxStart, idxEnd - idxStart);
+ var valTrimmed = val.Trim();
+ var type = DetecteType(val);
+
+ if (sessionFormats[i] == "Name" || sessionFormats[i] == "Style")
+ {
+ ChangeLinePart(line.Offset + idxStart, line.Offset + idxEnd, el =>
+ {
+ el.TextRunProperties.SetForegroundBrush(GetValueColor(ValueType.String).ToBrush());
+ });
+ }
+ else if (sessionFormats[i] == "Start" || sessionFormats[i] == "End")
+ {
+ ChangeLinePart(line.Offset + idxStart, line.Offset + idxEnd, el =>
+ {
+ el.TextRunProperties.SetForegroundBrush("#DCDCAA".ToBrush());
+ });
+ }
+ else if (sessionFormats[i] == "Text")
+ {
+ var valFixed = text.Substring(idxes[i - 1] + 1);
+ SubtitleLine subtitleLine = SubtitleEffectParser.Parse(valFixed);
+
+ foreach (var item in subtitleLine)
+ {
+ var itemVal = item.Value;
+
+ if (!string.IsNullOrEmpty(itemVal.Effect))
+ {
+ // Effect All
+ ChangeLinePart(line.Offset + idxStart + itemVal.StartIndex, line.Offset + idxStart + itemVal.EndIndex, el =>
+ {
+ el.TextRunProperties.SetForegroundBrush("#4EC9A2".ToBrush());
+ });
+
+ var words = SplitWords(itemVal.Effect);
+
+ // Effect Text
+ foreach (var word in words)
+ {
+ var typeEffect = DetecteType(word.Text);
+
+ if (typeEffect == ValueType.Numeric)
+ {
+ ChangeLinePart(line.Offset + idxStart + itemVal.StartIndex + word.StartIndex, line.Offset + idxStart + itemVal.StartIndex + word.EndIndex, el =>
+ {
+ el.TextRunProperties.SetForegroundBrush(GetValueColor(typeEffect).ToBrush());
+ });
+ }
+ else if (typeEffect == ValueType.String)
+ {
+ if (word.Text.StartsWith("fn")) // \fn
+ {
+ ChangeLinePart(line.Offset + idxStart + itemVal.StartIndex + word.StartIndex + 2, line.Offset + idxStart + itemVal.StartIndex + word.EndIndex, el =>
+ {
+ el.TextRunProperties.SetForegroundBrush(GetValueColor(typeEffect).ToBrush());
+ });
+ }
+ else if (word.Text.StartsWith("r")) // \r
+ {
+ ChangeLinePart(line.Offset + idxStart + itemVal.StartIndex + word.StartIndex + 1, line.Offset + idxStart + itemVal.StartIndex + word.EndIndex, el =>
+ {
+ el.TextRunProperties.SetForegroundBrush(GetValueColor(typeEffect).ToBrush());
+ });
+ }
+ else if (word.Text.StartsWith("alpha") // \alpha[&][H][&]
+ || word.Text.StartsWith("1a") // \1a[&][H][&]
+ || word.Text.StartsWith("2a") // \2a[&][H][&]
+ || word.Text.StartsWith("3a") // \3a[&][H][&]
+ || word.Text.StartsWith("4a") // \4a[&][H][&]
+ || word.Text.StartsWith("c") // \c[&][H][&] or \c
+ || word.Text.StartsWith("1c") // \1c[&][H][&]
+ || word.Text.StartsWith("2c") // \2c[&][H][&]
+ || word.Text.StartsWith("3c") // \3c[&][H][&]
+ || word.Text.StartsWith("4c") // \4c[&][H][&]
+ )
+ {
+ var idxAnd = word.Text.IndexOf('&');
+
+ if (idxAnd > 0)
+ {
+ ChangeLinePart(line.Offset + idxStart + itemVal.StartIndex + word.StartIndex + idxAnd, line.Offset + idxStart + itemVal.StartIndex + word.EndIndex, el =>
+ {
+ el.TextRunProperties.SetForegroundBrush("#DCDCAA".ToBrush());
+ });
+ }
+ else if (word.Text.StartsWith("alpha"))
+ {
+ var idxNum = IndexOfFirstDigit(word.Text);
+
+ if (idxNum > 0)
+ {
+ ChangeLinePart(line.Offset + idxStart + itemVal.StartIndex + word.StartIndex + idxNum, line.Offset + idxStart + itemVal.StartIndex + word.EndIndex, el =>
+ {
+ el.TextRunProperties.SetForegroundBrush(GetValueColor(ValueType.Numeric).ToBrush());
+ });
+ }
+ }
+ }
+ else
+ {
+ var idxNum = IndexOfFirstDigit(word.Text);
+
+ if (idxNum > 0)
+ {
+ ChangeLinePart(line.Offset + idxStart + itemVal.StartIndex + word.StartIndex + idxNum, line.Offset + idxStart + itemVal.StartIndex + word.EndIndex, el =>
+ {
+ el.TextRunProperties.SetForegroundBrush(GetValueColor(ValueType.Numeric).ToBrush());
+ });
+ }
+ }
+ }
+ }
+
+ // Effect Braces
+ for (int j = default; j < itemVal.Effect.Length; j++)
+ {
+ var ch = itemVal.Effect[j];
+
+ if (ch == '{' || ch == '}')
+ {
+ ChangeLinePart(line.Offset + idxStart + itemVal.StartIndex + j, line.Offset + idxStart + itemVal.StartIndex + j + 1, el =>
+ {
+ el.TextRunProperties.SetForegroundBrush("#F1D700".ToBrush());
+ });
+ }
+ else if (ch == '(' || ch == ')' || ch == ',')
+ {
+ ChangeLinePart(line.Offset + idxStart + itemVal.StartIndex + j, line.Offset + idxStart + itemVal.StartIndex + j + 1, el =>
+ {
+ el.TextRunProperties.SetForegroundBrush("#DA70D6".ToBrush());
+ });
+ }
+ }
+ }
+ else
+ {
+#if flase // Keep text color as default
+ // Text
+ ChangeLinePart(line.Offset + idxStart + itemVal.StartIndex, line.Offset + idxStart + itemVal.EndIndex, el =>
+ {
+ el.TextRunProperties.SetForegroundBrush(GetValueColor(ValueType.String).ToBrush());
+ });
+#endif
+
+ // Line Breaks (\N or \n) and Space (\h)
+ for (int j = default; j < itemVal.Text.Length; j++)
+ {
+ var ch = itemVal.Text[j];
+
+ if (ch == '\\' && j + 1 < itemVal.Text.Length)
+ {
+ var chNext = itemVal.Text[j + 1];
+
+ if (chNext == 'N' || chNext == 'n' || chNext == 'h')
+ {
+ ChangeLinePart(line.Offset + idxStart + itemVal.StartIndex + j, line.Offset + idxStart + itemVal.StartIndex + j + 2, el =>
+ {
+ el.TextRunProperties.SetForegroundBrush("#A0A0A0".ToBrush());
+ });
+ }
+ }
+ }
+ }
+ }
+ }
+ else
+ {
+ ChangeLinePart(line.Offset + idxStart, line.Offset + idxEnd, el =>
+ {
+ el.TextRunProperties.SetForegroundBrush(GetValueColor(type).ToBrush());
+ });
+ }
+ }
+ }
+
+ // Format
+ void SetFormatForegroundBrush()
+ {
+ int[] idxes = SpliteIndexes(text, ',');
+ int idxPrev = text.IndexOf(':');
+
+ ChangeLinePart(line.Offset, line.Offset + idxPrev, el =>
+ {
+ el.TextRunProperties.SetForegroundBrush("#3F9CD6".ToBrush());
+ });
+
+ for (int i = default; i < idxes.Length; i++)
+ {
+ int idxStart = idxPrev + 1;
+ int idxEnd = idxPrev = idxes[i];
+
+ if (Debugger.IsAttached)
+ {
+ _ = text.Substring(idxStart, idxEnd - idxStart);
+ }
+
+ ChangeLinePart(line.Offset + idxStart, line.Offset + idxEnd, el =>
+ {
+ el.TextRunProperties.SetForegroundBrush("#3F9CD6".ToBrush());
+ });
+ }
+ }
+ }
+
+ private ValueType DetecteType(string input)
+ {
+ if (double.TryParse(input, out _))
+ return ValueType.Numeric;
+
+ if (bool.TryParse(input, out _))
+ return ValueType.Boolean;
+
+ return ValueType.String;
+ }
+
+ private string GetValueColor(ValueType type)
+ {
+ return type switch
+ {
+ ValueType.Numeric => "#B5CEA8",
+ ValueType.Boolean => "#719BD1",
+ ValueType.String or _ => "#CE9178",
+ };
+ }
+
+ private static int[] SpliteIndexes(string text, char target)
+ {
+ if (string.IsNullOrEmpty(text))
+ return [];
+
+ var indexes = new List();
+
+ for (int i = default; i < text.Length; i++)
+ {
+ if (target == text[i])
+ {
+ indexes.Add(i);
+ }
+ }
+
+ // Add the last index to ensure the last segment is captured
+ indexes.Add(text.Length);
+ return [.. indexes];
+ }
+
+ public static InlineText[] SplitWords(string input)
+ {
+ if (string.IsNullOrEmpty(input))
+ return [];
+
+ var result = new List();
+ // 定义正则表达式:匹配非分隔符的连续字符段
+ // 分隔符包括:空格 \ { } ( ) , (注意 \\ 需要双转义)
+ var matches = Regex.Matches(input, @"[^{}\(\)\\,\s]+");
+ foreach (Match match in matches)
+ {
+ result.Add(new InlineText
+ {
+ Text = match.Value,
+ StartIndex = match.Index,
+ EndIndex = match.Index + match.Length,
+ });
+ }
+
+ return [.. result];
+ }
+
+ public static int IndexOfFirstDigit(string input)
+ {
+ if (string.IsNullOrEmpty(input))
+ return -1;
+
+ for (int i = 0; i < input.Length; i++)
+ {
+ if (char.IsDigit(input[i]))
+ {
+ if (i > 0 && input[i - 1] == '-')
+ return i - 1;
+ else
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private enum ValueType
+ {
+ String,
+ Numeric,
+ Boolean,
+ }
+
+ public sealed class SessionStore : Dictionary
+ {
+ public bool IsCurrentSession(string sessionName, int currentLineNumber)
+ {
+ return ContainsKey(sessionName) && currentLineNumber > this[sessionName].LineNumber;
+ }
+ }
+
+ [DebuggerDisplay("{ToString()}")]
+ public sealed class Session
+ {
+ public string Name { get; set; }
+
+ public string[] Formats { get; set; }
+
+ public int LineNumber { get; set; }
+
+ public override string ToString()
+ {
+ return $"[{Name}] {string.Join(", ", Formats)} in line {LineNumber}";
+ }
+ }
+
+ public static class SubtitleEffectParser
+ {
+ ///
+ /// 解析ASS字幕行,把特效和正文分块输出
+ ///
+ /// ASS原始字符串
+ /// SubtitleLine对象
+ public static SubtitleLine Parse(string line)
+ {
+ var result = new SubtitleLine();
+ int segIdx = 0;
+ var regex = new Regex(@"\{.*?\}");
+
+ int lastIndex = 0;
+ foreach (Match match in regex.Matches(line))
+ {
+ // 处理特效前的文本
+ if (match.Index > lastIndex)
+ {
+ string text = line.Substring(lastIndex, match.Index - lastIndex);
+ if (!string.IsNullOrEmpty(text))
+ {
+ result[segIdx.ToString()] = new SubtitleText
+ {
+ Effect = null,
+ Text = text,
+ StartIndex = lastIndex,
+ EndIndex = match.Index
+ };
+ segIdx++;
+ }
+ }
+ // 处理特效
+ result[segIdx.ToString()] = new SubtitleText
+ {
+ Effect = match.Value,
+ Text = null,
+ StartIndex = match.Index,
+ EndIndex = match.Index + match.Length
+ };
+ segIdx++;
+ lastIndex = match.Index + match.Length;
+ }
+ // 处理最后的文本
+ if (lastIndex < line.Length)
+ {
+ string text = line.Substring(lastIndex);
+ if (!string.IsNullOrEmpty(text))
+ {
+ result[segIdx.ToString()] = new SubtitleText
+ {
+ Effect = null,
+ Text = text,
+ StartIndex = lastIndex,
+ EndIndex = line.Length
+ };
+ }
+ }
+ return result;
+ }
+ }
+
+ public class SubtitleLine : Dictionary
+ {
+ }
+
+ [DebuggerDisplay("{ToString()}")]
+ public class SubtitleText
+ {
+ public string Effect { get; set; }
+
+ public string Text { get; set; }
+
+ public int StartIndex { get; set; }
+
+ public int EndIndex { get; set; }
+
+ public override string ToString()
+ {
+ return $"{Effect} {Text} [{StartIndex}, {EndIndex}]";
+ }
+ }
+
+ [DebuggerDisplay("{ToString()}")]
+ public class InlineText
+ {
+ public string Text { get; set; }
+
+ public int StartIndex { get; set; }
+
+ public int EndIndex { get; set; }
+
+ public override string ToString()
+ {
+ return $"{Text} [{StartIndex}, {EndIndex}]";
+ }
+ }
+ }
+}