// 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.Highlighting;
using ICSharpCode.AvalonEdit.Highlighting.Xshd;
using QuickLook.Common.Helpers;
using QuickLook.Plugin.TextViewer.Detectors;
using QuickLook.Plugin.TextViewer.Themes.HighlightingDefinitions;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Xml;
namespace QuickLook.Plugin.TextViewer.Themes;
public class HighlightingThemeManager
{
public static HighlightingManager Light { get; internal set; }
public static HighlightingManager Dark { get; internal set; }
public static void Initialize()
{
InitHighlightingManager();
InitCustomHighlighting();
}
public static HighlightingTheme GetHighlightingByExtensionOrDetector(string path, string extension, string text = null)
{
if (Light is null || Dark is null) return HighlightingTheme.Default;
var useFormatDetector = SettingHelper.Get("UseFormatDetector", true, "QuickLook.Plugin.TextViewer");
var highlightingTheme = GetDefinitionByExtension(nameof(Dark), extension);
if (useFormatDetector && FormatDetector.Confuse(path, text) is IFormatDetector confusedFormatDetector)
{
if (!string.IsNullOrEmpty(confusedFormatDetector.Extension))
{
highlightingTheme = GetDefinitionByExtension(nameof(Dark), confusedFormatDetector.Extension)
?? GetDefinitionByExtension(nameof(Light), confusedFormatDetector.Extension);
}
else
{
highlightingTheme = GetDefinition(nameof(Dark), confusedFormatDetector.Name)
?? GetDefinition(nameof(Light), confusedFormatDetector.Name);
}
}
if (highlightingTheme == null)
{
highlightingTheme = GetDefinitionByExtension(nameof(Light), extension);
if (highlightingTheme == null)
{
if (useFormatDetector && FormatDetector.Detect(path, text)?.Extension is string detectExtension)
{
highlightingTheme = GetDefinitionByExtension(nameof(Dark), detectExtension)
?? GetDefinitionByExtension(nameof(Light), detectExtension);
}
}
}
// The unsupported highlighting will be fallback to not highlighted text
highlightingTheme ??= GetDefinitionByExtension(nameof(Dark), ".txt")
?? GetDefinitionByExtension(nameof(Light), ".txt")
?? HighlightingTheme.Default;
var darkThemeAllowed = SettingHelper.Get("AllowDarkTheme", highlightingTheme.IsDark, "QuickLook.Plugin.TextViewer");
var isDark = darkThemeAllowed && OSThemeHelper.AppsUseDarkTheme();
// The current environment does not require dark mode so revert to light mode
if (!isDark && highlightingTheme.IsDark)
{
highlightingTheme.Theme = nameof(Light);
highlightingTheme.HighlightingManager = Light;
highlightingTheme.SyntaxHighlighting
// The extension that supports dark mode must support light mode also
= Light.GetDefinitionByExtension(highlightingTheme.Extension);
}
return highlightingTheme;
}
private static HighlightingTheme GetDefinition(string theme, string extension)
{
var highlightingManager = theme == nameof(Dark) ? Dark : Light;
var def = highlightingManager.GetDefinition(extension);
if (def != null)
{
return new HighlightingTheme()
{
Theme = theme,
HighlightingManager = highlightingManager,
SyntaxHighlighting = def,
Extension = extension,
};
}
return null;
}
private static HighlightingTheme GetDefinitionByExtension(string theme, string extension)
{
var highlightingManager = theme == nameof(Dark) ? Dark : Light;
var def = highlightingManager.GetDefinitionByExtension(extension);
if (def != null)
{
return new HighlightingTheme()
{
Theme = theme,
HighlightingManager = highlightingManager,
SyntaxHighlighting = def,
Extension = extension,
};
}
return null;
}
private static void InitHighlightingManager()
{
Light = new HighlightingManager();
Dark = new HighlightingManager();
Assembly assembly = Assembly.GetExecutingAssembly();
string[] resourceNames = assembly.GetManifestResourceNames();
foreach (var resourceName in resourceNames.Where(name => name.Contains(".Syntax.")))
{
using Stream s = assembly.GetManifestResourceStream(resourceName);
if (s == null)
continue;
Debug.WriteLine(resourceName);
try
{
var hlm = resourceName.Contains(".Syntax.Dark.") ? Dark : Light;
var name = EmbeddedResource.GetFileNameWithoutExtension(resourceName);
using var reader = new XmlTextReader(s);
var xshd = HighlightingLoader.LoadXshd(reader);
var highlightingDefinition = HighlightingLoader.Load(xshd, hlm);
if (xshd.Extensions.Count > 0)
hlm.RegisterHighlighting(name, [.. xshd.Extensions], highlightingDefinition);
}
catch (Exception e)
{
ProcessHelper.WriteLog(e.ToString());
}
}
AddHighlightingManager(Light, nameof(Light));
AddHighlightingManager(Dark, nameof(Dark));
}
private static void AddHighlightingManager(HighlightingManager hlm, string dirName)
{
var assemblyPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
if (string.IsNullOrEmpty(assemblyPath))
return;
var syntaxPath = Path.Combine(assemblyPath, "Syntax", dirName);
if (!Directory.Exists(syntaxPath))
return;
foreach (var file in Directory.EnumerateFiles(syntaxPath, "*.xshd").OrderBy(f => f))
{
try
{
Debug.WriteLine(file);
var ext = Path.GetFileNameWithoutExtension(file);
using var fileStream = new FileStream(Path.GetFullPath(file), FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
using var reader = new XmlTextReader(fileStream);
var xshd = HighlightingLoader.LoadXshd(reader);
var highlightingDefinition = HighlightingLoader.Load(xshd, hlm);
if (xshd.Extensions.Count > 0)
hlm.RegisterHighlighting(ext, [.. xshd.Extensions], highlightingDefinition);
}
catch (Exception e)
{
ProcessHelper.WriteLog(e.ToString());
}
}
}
private static void InitCustomHighlighting()
{
foreach (var definitionClass in LoadAllDefinitions())
{
var hlm = definitionClass.Theme == nameof(Dark) ? Dark : Light;
AddCustomHighlighting(hlm, definitionClass.Instance);
}
static IEnumerable LoadAllDefinitions()
{
var assembly = Assembly.GetExecutingAssembly();
var types = assembly.GetTypes()
.Where(t => t.IsClass
&& !t.IsAbstract
&& typeof(ICustomHighlightingDefinition).IsAssignableFrom(t)
&& t.GetConstructor(Type.EmptyTypes) != null);
foreach (var type in types)
{
if (type.GetCustomAttribute() is CustomHighlightingDefinitionAttribute { } attr)
{
if (Activator.CreateInstance(type) is ICustomHighlightingDefinition instance)
{
yield return new CustomHighlightingDefinitionClass(instance, attr.Theme);
}
}
}
}
}
private static void AddCustomHighlighting(HighlightingManager hlm, ICustomHighlightingDefinition definition)
{
try
{
hlm.RegisterHighlighting(definition.Name, definition.Extension.Split(';'), definition);
}
catch (Exception e)
{
ProcessHelper.WriteLog(e.ToString());
}
}
}
file static class EmbeddedResource
{
public static string GetFileNameWithoutExtension(string resourceName)
{
// Requires the embedded resource file name
// must have a file extension and have only one '.' character
int start = int.MinValue, end = int.MinValue;
for (int i = resourceName.Length - 1; i >= 0; i--)
{
if (resourceName[i] == '.')
{
if (end == int.MinValue)
{
end = i;
continue;
}
if (start == int.MinValue)
{
start = i + 1; // Exinclude '.' character
break;
}
}
}
if ((start != int.MinValue) && (end != int.MinValue))
{
return resourceName.Substring(start, end - start);
}
return resourceName;
}
}