Add YAML syntax highlighting

This commit is contained in:
ema
2025-06-29 09:56:18 +08:00
parent e16a89f979
commit ac7a08cbf3
2 changed files with 860 additions and 0 deletions

View File

@@ -0,0 +1,430 @@
// 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 <http://www.gnu.org/licenses/>.
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Highlighting;
using ICSharpCode.AvalonEdit.Rendering;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;
namespace QuickLook.Plugin.TextViewer.Themes.HighlightingDefinitions.Dark;
public class YAMLHighlightingDefinition : DarkHighlightingDefinition
{
public override string Name => "YAML";
public override string Extension => ".yaml;.yml";
public override HighlightingRuleSet MainRuleSet => new()
{
Rules =
{
new HighlightingRule
{
Regex = new Regex(@"#.*", 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<HighlightingColor> NamedHighlightingColors =>
[
GetNamedColor("Comment"),
];
public override DocumentColorizingTransformer[] LineTransformers { get; } = [new KeyHighlighter()];
public class KeyHighlighter : DocumentColorizingTransformer
{
protected override void ColorizeLine(DocumentLine line)
{
var text = CurrentContext.Document.GetText(line);
// Skip empty lines and comments
if (string.IsNullOrWhiteSpace(text) || text.TrimStart().StartsWith("#"))
return;
// Detect as array or object notation
if (text.TrimStart().StartsWith("-"))
{
int idx = text.IndexOf('-');
var val = text.Substring(idx + 1); // Here +1 to skip symbol '-'
var valTrimmed = val.Trim();
var type = DetecteType(val);
if (valTrimmed.Contains("#") && !(valTrimmed.StartsWith("\"") && !valTrimmed.StartsWith("'")))
{
int idxSharp = text.IndexOf('#');
var valSharp = text.Substring(idxSharp).Trim();
// Value
ChangeLinePart(line.Offset, line.Offset + text.Length - valSharp.Length, el =>
{
el.TextRunProperties.SetForegroundBrush(GetValueColor(type).ToBrush());
});
// Comment
ChangeLinePart(line.Offset + text.Length - valSharp.Length, line.Offset + text.Length, el =>
{
el.TextRunProperties.SetForegroundBrush("#6A9949".ToBrush());
});
}
else
{
// Value
ChangeLinePart(line.Offset + idx + 1, line.Offset + text.Length, el =>
{
el.TextRunProperties.SetForegroundBrush(GetValueColor(type).ToBrush());
});
}
return;
}
// Detect as normal key-value pair
else
{
int idx = text.IndexOf(':');
var val = text.Substring(idx + 1);
var valTrimmed = val.Trim();
if (idx <= 0)
{
// If no marker is found, it is considered a multi-line string
if (valTrimmed.Contains("#") && !(valTrimmed.StartsWith("\"") && !valTrimmed.StartsWith("'")))
{
int idxSharp = text.IndexOf('#');
var valSharp = text.Substring(idxSharp).Trim();
// Value
ChangeLinePart(line.Offset, line.Offset + text.Length - valSharp.Length, el =>
{
el.TextRunProperties.SetForegroundBrush(GetValueColor(ValueType.String).ToBrush());
});
// Comment
ChangeLinePart(line.Offset + text.Length - valSharp.Length, line.Offset + text.Length, el =>
{
el.TextRunProperties.SetForegroundBrush("#6A9949".ToBrush());
});
}
else
{
// Value
ChangeLinePart(line.Offset, line.Offset + text.Length, el =>
{
el.TextRunProperties.SetForegroundBrush(GetValueColor(ValueType.String).ToBrush());
});
}
return;
}
// Key
ChangeLinePart(line.Offset, line.Offset + idx, el =>
{
el.TextRunProperties.SetForegroundBrush("#719BD1".ToBrush());
});
// Detect as Literal Block Scalar / Folded Block Scalar
if (valTrimmed.StartsWith(">") || valTrimmed.StartsWith("|"))
{
// Detect as value with comment
if (valTrimmed.Contains("#") && !(valTrimmed.StartsWith("\"") && !valTrimmed.StartsWith("'")))
{
int idxSharp = text.IndexOf('#');
var valSharp = text.Substring(idxSharp).Trim();
// Scalar
ChangeLinePart(line.Offset + idx + 1, line.Offset + idxSharp, el =>
{
el.TextRunProperties.SetForegroundBrush("#C586C0".ToBrush());
});
// Comment
ChangeLinePart(line.Offset + text.Length - valSharp.Length, line.Offset + text.Length, el =>
{
el.TextRunProperties.SetForegroundBrush("#6A9949".ToBrush());
});
}
else
{
// Scalar
ChangeLinePart(line.Offset + idx + 1, line.Offset + text.Length, el =>
{
el.TextRunProperties.SetForegroundBrush("#C586C0".ToBrush());
});
}
}
// Detect as others
else
{
// Detect as value with comment
if (valTrimmed.Contains("#") && !(valTrimmed.StartsWith("\"") && !valTrimmed.StartsWith("'")))
{
var valWithoutSharp = valTrimmed.Substring(0, valTrimmed.IndexOf('#')).Trim();
int idxSharp = text.IndexOf('#');
var valSharp = text.Substring(idxSharp).Trim();
var type = DetecteType(valWithoutSharp);
// If value is a string, check for array or object notation
if (type == ValueType.String)
{
// Detect as array or object notation
if ((valWithoutSharp.StartsWith("[") && valWithoutSharp.EndsWith("]"))
|| (valWithoutSharp.StartsWith("{") && valWithoutSharp.EndsWith("}")))
{
var parsed = InlineTextParser.ParseInlineText(val);
// Value
foreach (var item in parsed)
{
if (item.Type == InlineType.None) continue;
var startIndex = (idx + 1) + item.StartIndex;
var count = item.EndIndex - item.StartIndex;
var dd = text.Substring(startIndex, count);
ChangeLinePart(line.Offset + startIndex, line.Offset + startIndex + count, el =>
{
el.TextRunProperties.SetForegroundBrush(item.GetValueColor().ToBrush());
});
}
}
// Detect as normal value
else
{
// Value
ChangeLinePart(line.Offset + idx + 1, line.Offset + text.Length, el =>
{
el.TextRunProperties.SetForegroundBrush(GetValueColor(type).ToBrush());
});
}
}
else
{
// Value
ChangeLinePart(line.Offset + idx + 1, line.Offset + text.Length, el =>
{
el.TextRunProperties.SetForegroundBrush(GetValueColor(type).ToBrush());
});
}
// Comment
ChangeLinePart(line.Offset + text.Length - valSharp.Length, line.Offset + text.Length, el =>
{
el.TextRunProperties.SetForegroundBrush("#6A9949".ToBrush());
});
}
// Detect as value without comment
else
{
var type = DetecteType(val);
// If value is a string, check for array or object notation
if (type == ValueType.String)
{
// Detect as array or object notation
if ((valTrimmed.StartsWith("[") && valTrimmed.EndsWith("]"))
|| (valTrimmed.StartsWith("{") && valTrimmed.EndsWith("}")))
{
var parsed = InlineTextParser.ParseInlineText(val);
// Value
foreach (var item in parsed)
{
if (item.Type == InlineType.None) continue;
var startIndex = (idx + 1) + item.StartIndex;
var count = item.EndIndex - item.StartIndex;
var dd = text.Substring(startIndex, count);
ChangeLinePart(line.Offset + startIndex, line.Offset + startIndex + count, el =>
{
el.TextRunProperties.SetForegroundBrush(item.GetValueColor().ToBrush());
});
}
}
else
{
ChangeLinePart(line.Offset + idx + 1, line.Offset + text.Length, el =>
{
el.TextRunProperties.SetForegroundBrush(GetValueColor(type).ToBrush());
});
}
}
else
{
ChangeLinePart(line.Offset + idx + 1, line.Offset + text.Length, el =>
{
el.TextRunProperties.SetForegroundBrush(GetValueColor(type).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 enum ValueType
{
String,
Numeric,
Boolean,
}
private static class InlineTextParser
{
public static InlineText[] ParseInlineText(string input)
{
var result = new List<InlineText>();
int index = 0;
var aoSymbol = default(char); // Array/Object symbol
var prevSymbol = default(char);
while (index < input.Length)
{
char c = input[index];
// Symbol token
if ("{}[]:,".Contains(c))
{
if (c == '[' || c == '{')
{
aoSymbol = c;
}
prevSymbol = c;
result.Add(new InlineText(c.ToString(), c switch
{
'[' or ']' or '{' or '}' => InlineType.Symbol,
':' or ',' or _ => InlineType.None,
}, index, index + 1));
index++;
}
// Quoted string
else if (c == '\'' || c == '"')
{
int start = index;
index++;
while (index < input.Length && input[index] != c)
index++;
index++; // include closing quote
result.Add(new InlineText(input.Substring(start, index - start), InlineType.ValueString, start, index));
}
// Key or value (word or number)
else if (char.IsLetterOrDigit(c) || c == '-' || c == '>' || c == '<' || c == '=')
{
int start = index;
while (index < input.Length && (char.IsLetterOrDigit(input[index]) || "><=-.".Contains(input[index])))
index++;
string token = input.Substring(start, index - start);
result.Add(new InlineText(token, aoSymbol switch
{
'[' => double.TryParse(token, out _) ? InlineType.ValueNumeric : InlineType.ValueString,
'{' or _ => prevSymbol switch
{
':' => InlineType.ValueNumeric,
',' or _ => InlineType.Key,
}
}, start, index));
}
// Whitespace, skip
else if (char.IsWhiteSpace(c))
{
result.Add(new InlineText(c.ToString(), InlineType.None, index, index + 1));
index++;
}
else
{
// Unknown single char token
result.Add(new InlineText(c.ToString(), InlineType.None, index, index + 1));
index++;
}
}
return [.. result];
}
}
[DebuggerDisplay("{Text}: {Type} [{StartIndex}, {EndIndex}]")]
private sealed class InlineText(string text, InlineType type, int start, int end)
{
public string Text { get; set; } = text;
public InlineType Type { get; set; } = type;
public int StartIndex { get; set; } = start;
public int EndIndex { get; set; } = end;
public string GetValueColor()
{
return Type switch
{
InlineType.Symbol => "#FFD700",
InlineType.ValueNumeric => "#B5CEA8",
InlineType.Key or InlineType.ValueBoolean => "#719BD1",
InlineType.ValueString or _ => "#CE9178",
};
}
}
public enum InlineType
{
None,
Key,
ValueString,
ValueNumeric,
ValueBoolean,
Symbol, // e.g. [ ] { } etc.
}
}
}

View File

@@ -0,0 +1,430 @@
// 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 <http://www.gnu.org/licenses/>.
using ICSharpCode.AvalonEdit.Document;
using ICSharpCode.AvalonEdit.Highlighting;
using ICSharpCode.AvalonEdit.Rendering;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;
namespace QuickLook.Plugin.TextViewer.Themes.HighlightingDefinitions.Light;
public class YAMLHighlightingDefinition : LightHighlightingDefinition
{
public override string Name => "YAML";
public override string Extension => ".yaml;.yml";
public override HighlightingRuleSet MainRuleSet => new()
{
Rules =
{
new HighlightingRule
{
Regex = new Regex(@"#.*", RegexOptions.Compiled),
Color = GetNamedColor("Comment")
}
}
};
public override HighlightingColor GetNamedColor(string name)
{
return name switch
{
"Comment" => new HighlightingColor
{
Name = "Comment",
Foreground = new SimpleHighlightingBrush("#008000".ToColor()),
},
_ => null
};
}
public override IEnumerable<HighlightingColor> NamedHighlightingColors =>
[
GetNamedColor("Comment"),
];
public override DocumentColorizingTransformer[] LineTransformers { get; } = [new KeyHighlighter()];
public class KeyHighlighter : DocumentColorizingTransformer
{
protected override void ColorizeLine(DocumentLine line)
{
var text = CurrentContext.Document.GetText(line);
// Skip empty lines and comments
if (string.IsNullOrWhiteSpace(text) || text.TrimStart().StartsWith("#"))
return;
// Detect as array or object notation
if (text.TrimStart().StartsWith("-"))
{
int idx = text.IndexOf('-');
var val = text.Substring(idx + 1); // Here +1 to skip symbol '-'
var valTrimmed = val.Trim();
var type = DetecteType(val);
if (valTrimmed.Contains("#") && !(valTrimmed.StartsWith("\"") && !valTrimmed.StartsWith("'")))
{
int idxSharp = text.IndexOf('#');
var valSharp = text.Substring(idxSharp).Trim();
// Value
ChangeLinePart(line.Offset, line.Offset + text.Length - valSharp.Length, el =>
{
el.TextRunProperties.SetForegroundBrush(GetValueColor(type).ToBrush());
});
// Comment
ChangeLinePart(line.Offset + text.Length - valSharp.Length, line.Offset + text.Length, el =>
{
el.TextRunProperties.SetForegroundBrush("#008000".ToBrush());
});
}
else
{
// Value
ChangeLinePart(line.Offset + idx + 1, line.Offset + text.Length, el =>
{
el.TextRunProperties.SetForegroundBrush(GetValueColor(type).ToBrush());
});
}
return;
}
// Detect as normal key-value pair
else
{
int idx = text.IndexOf(':');
var val = text.Substring(idx + 1);
var valTrimmed = val.Trim();
if (idx <= 0)
{
// If no marker is found, it is considered a multi-line string
if (valTrimmed.Contains("#") && !(valTrimmed.StartsWith("\"") && !valTrimmed.StartsWith("'")))
{
int idxSharp = text.IndexOf('#');
var valSharp = text.Substring(idxSharp).Trim();
// Value
ChangeLinePart(line.Offset, line.Offset + text.Length - valSharp.Length, el =>
{
el.TextRunProperties.SetForegroundBrush(GetValueColor(ValueType.String).ToBrush());
});
// Comment
ChangeLinePart(line.Offset + text.Length - valSharp.Length, line.Offset + text.Length, el =>
{
el.TextRunProperties.SetForegroundBrush("#008000".ToBrush());
});
}
else
{
// Value
ChangeLinePart(line.Offset, line.Offset + text.Length, el =>
{
el.TextRunProperties.SetForegroundBrush(GetValueColor(ValueType.String).ToBrush());
});
}
return;
}
// Key
ChangeLinePart(line.Offset, line.Offset + idx, el =>
{
el.TextRunProperties.SetForegroundBrush("#795E26".ToBrush());
});
// Detect as Literal Block Scalar / Folded Block Scalar
if (valTrimmed.StartsWith(">") || valTrimmed.StartsWith("|"))
{
// Detect as value with comment
if (valTrimmed.Contains("#") && !(valTrimmed.StartsWith("\"") && !valTrimmed.StartsWith("'")))
{
int idxSharp = text.IndexOf('#');
var valSharp = text.Substring(idxSharp).Trim();
// Scalar
ChangeLinePart(line.Offset + idx + 1, line.Offset + idxSharp, el =>
{
el.TextRunProperties.SetForegroundBrush("#267f99".ToBrush());
});
// Comment
ChangeLinePart(line.Offset + text.Length - valSharp.Length, line.Offset + text.Length, el =>
{
el.TextRunProperties.SetForegroundBrush("#008000".ToBrush());
});
}
else
{
// Scalar
ChangeLinePart(line.Offset + idx + 1, line.Offset + text.Length, el =>
{
el.TextRunProperties.SetForegroundBrush("#267f99".ToBrush());
});
}
}
// Detect as others
else
{
// Detect as value with comment
if (valTrimmed.Contains("#") && !(valTrimmed.StartsWith("\"") && !valTrimmed.StartsWith("'")))
{
var valWithoutSharp = valTrimmed.Substring(0, valTrimmed.IndexOf('#')).Trim();
int idxSharp = text.IndexOf('#');
var valSharp = text.Substring(idxSharp).Trim();
var type = DetecteType(valWithoutSharp);
// If value is a string, check for array or object notation
if (type == ValueType.String)
{
// Detect as array or object notation
if ((valWithoutSharp.StartsWith("[") && valWithoutSharp.EndsWith("]"))
|| (valWithoutSharp.StartsWith("{") && valWithoutSharp.EndsWith("}")))
{
var parsed = InlineTextParser.ParseInlineText(val);
// Value
foreach (var item in parsed)
{
if (item.Type == InlineType.None) continue;
var startIndex = (idx + 1) + item.StartIndex;
var count = item.EndIndex - item.StartIndex;
var dd = text.Substring(startIndex, count);
ChangeLinePart(line.Offset + startIndex, line.Offset + startIndex + count, el =>
{
el.TextRunProperties.SetForegroundBrush(item.GetValueColor().ToBrush());
});
}
}
// Detect as normal value
else
{
// Value
ChangeLinePart(line.Offset + idx + 1, line.Offset + text.Length, el =>
{
el.TextRunProperties.SetForegroundBrush(GetValueColor(type).ToBrush());
});
}
}
else
{
// Value
ChangeLinePart(line.Offset + idx + 1, line.Offset + text.Length, el =>
{
el.TextRunProperties.SetForegroundBrush(GetValueColor(type).ToBrush());
});
}
// Comment
ChangeLinePart(line.Offset + text.Length - valSharp.Length, line.Offset + text.Length, el =>
{
el.TextRunProperties.SetForegroundBrush("#008000".ToBrush());
});
}
// Detect as value without comment
else
{
var type = DetecteType(val);
// If value is a string, check for array or object notation
if (type == ValueType.String)
{
// Detect as array or object notation
if ((valTrimmed.StartsWith("[") && valTrimmed.EndsWith("]"))
|| (valTrimmed.StartsWith("{") && valTrimmed.EndsWith("}")))
{
var parsed = InlineTextParser.ParseInlineText(val);
// Value
foreach (var item in parsed)
{
if (item.Type == InlineType.None) continue;
var startIndex = (idx + 1) + item.StartIndex;
var count = item.EndIndex - item.StartIndex;
var dd = text.Substring(startIndex, count);
ChangeLinePart(line.Offset + startIndex, line.Offset + startIndex + count, el =>
{
el.TextRunProperties.SetForegroundBrush(item.GetValueColor().ToBrush());
});
}
}
else
{
ChangeLinePart(line.Offset + idx + 1, line.Offset + text.Length, el =>
{
el.TextRunProperties.SetForegroundBrush(GetValueColor(type).ToBrush());
});
}
}
else
{
ChangeLinePart(line.Offset + idx + 1, line.Offset + text.Length, el =>
{
el.TextRunProperties.SetForegroundBrush(GetValueColor(type).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 => "#098658",
ValueType.Boolean => "#0000FF",
ValueType.String or _ => "#A31515",
};
}
private enum ValueType
{
String,
Numeric,
Boolean,
}
private static class InlineTextParser
{
public static InlineText[] ParseInlineText(string input)
{
var result = new List<InlineText>();
int index = 0;
var aoSymbol = default(char); // Array/Object symbol
var prevSymbol = default(char);
while (index < input.Length)
{
char c = input[index];
// Symbol token
if ("{}[]:,".Contains(c))
{
if (c == '[' || c == '{')
{
aoSymbol = c;
}
prevSymbol = c;
result.Add(new InlineText(c.ToString(), c switch
{
'[' or ']' or '{' or '}' => InlineType.Symbol,
':' or ',' or _ => InlineType.None,
}, index, index + 1));
index++;
}
// Quoted string
else if (c == '\'' || c == '"')
{
int start = index;
index++;
while (index < input.Length && input[index] != c)
index++;
index++; // include closing quote
result.Add(new InlineText(input.Substring(start, index - start), InlineType.ValueString, start, index));
}
// Key or value (word or number)
else if (char.IsLetterOrDigit(c) || c == '-' || c == '>' || c == '<' || c == '=')
{
int start = index;
while (index < input.Length && (char.IsLetterOrDigit(input[index]) || "><=-.".Contains(input[index])))
index++;
string token = input.Substring(start, index - start);
result.Add(new InlineText(token, aoSymbol switch
{
'[' => double.TryParse(token, out _) ? InlineType.ValueNumeric : InlineType.ValueString,
'{' or _ => prevSymbol switch
{
':' => InlineType.ValueNumeric,
',' or _ => InlineType.Key,
}
}, start, index));
}
// Whitespace, skip
else if (char.IsWhiteSpace(c))
{
result.Add(new InlineText(c.ToString(), InlineType.None, index, index + 1));
index++;
}
else
{
// Unknown single char token
result.Add(new InlineText(c.ToString(), InlineType.None, index, index + 1));
index++;
}
}
return [.. result];
}
}
[DebuggerDisplay("{Text}: {Type} [{StartIndex}, {EndIndex}]")]
private sealed class InlineText(string text, InlineType type, int start, int end)
{
public string Text { get; set; } = text;
public InlineType Type { get; set; } = type;
public int StartIndex { get; set; } = start;
public int EndIndex { get; set; } = end;
public string GetValueColor()
{
return Type switch
{
InlineType.Symbol => "#795E26",
InlineType.ValueNumeric => "#098658",
InlineType.Key or InlineType.ValueBoolean => "#0000FF",
InlineType.ValueString or _ => "#A31515",
};
}
}
public enum InlineType
{
None,
Key,
ValueString,
ValueNumeric,
ValueBoolean,
Symbol, // e.g. [ ] { } etc.
}
}
}