// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
// This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Text.RegularExpressions;
using ICSharpCode.AvalonEdit.Utils;
namespace ICSharpCode.AvalonEdit.Highlighting.Xshd
{
[Serializable]
sealed class XmlHighlightingDefinition : IHighlightingDefinition2
{
public string Name { get; private set; }
public XmlHighlightingDefinition(XshdSyntaxDefinition xshd, IHighlightingDefinitionReferenceResolver resolver)
{
this.Name = xshd.Name;
// Create HighlightingRuleSet instances
var rnev = new RegisterNamedElementsVisitor(this);
xshd.AcceptElements(rnev);
// Assign MainRuleSet so that references can be resolved
foreach (XshdElement element in xshd.Elements) {
XshdRuleSet xrs = element as XshdRuleSet;
if (xrs != null && xrs.Name == null) {
if (MainRuleSet != null)
throw Error(element, "Duplicate main RuleSet. There must be only one nameless RuleSet!");
else
MainRuleSet = rnev.ruleSets[xrs];
}
}
if (MainRuleSet == null)
throw new HighlightingDefinitionInvalidException("Could not find main RuleSet.");
// Translate elements within the rulesets (resolving references and processing imports)
xshd.AcceptElements(new TranslateElementVisitor(this, rnev.ruleSets, resolver));
foreach (var p in xshd.Elements.OfType<XshdProperty>())
propDict.Add(p.Name, p.Value);
}
#region RegisterNamedElements
sealed class RegisterNamedElementsVisitor : IXshdVisitor
{
XmlHighlightingDefinition def;
internal readonly Dictionary<XshdRuleSet, HighlightingRuleSet> ruleSets
= new Dictionary<XshdRuleSet, HighlightingRuleSet>();
public RegisterNamedElementsVisitor(XmlHighlightingDefinition def)
{
this.def = def;
}
public object VisitRuleSet(XshdRuleSet ruleSet)
{
HighlightingRuleSet hrs = new HighlightingRuleSet();
ruleSets.Add(ruleSet, hrs);
if (ruleSet.Name != null) {
if (ruleSet.Name.Length == 0)
throw Error(ruleSet, "Name must not be the empty string");
if (def.ruleSetDict.ContainsKey(ruleSet.Name))
throw Error(ruleSet, "Duplicate rule set name '" + ruleSet.Name + "'.");
def.ruleSetDict.Add(ruleSet.Name, hrs);
}
ruleSet.AcceptElements(this);
return null;
}
public object VisitColor(XshdColor color)
{
if (color.Name != null) {
if (color.Name.Length == 0)
throw Error(color, "Name must not be the empty string");
if (def.colorDict.ContainsKey(color.Name))
throw Error(color, "Duplicate color name '" + color.Name + "'.");
def.colorDict.Add(color.Name, new HighlightingColor());
}
return null;
}
public object VisitKeywords(XshdKeywords keywords)
{
return keywords.ColorReference.AcceptVisitor(this);
}
public object VisitSpan(XshdSpan span)
{
span.BeginColorReference.AcceptVisitor(this);
span.SpanColorReference.AcceptVisitor(this);
span.EndColorReference.AcceptVisitor(this);
return span.RuleSetReference.AcceptVisitor(this);
}
public object VisitImport(XshdImport import)
{
return import.RuleSetReference.AcceptVisitor(this);
}
public object VisitRule(XshdRule rule)
{
return rule.ColorReference.AcceptVisitor(this);
}
}
#endregion
#region TranslateElements
sealed class TranslateElementVisitor : IXshdVisitor
{
readonly XmlHighlightingDefinition def;
readonly Dictionary<XshdRuleSet, HighlightingRuleSet> ruleSetDict;
readonly Dictionary<HighlightingRuleSet, XshdRuleSet> reverseRuleSetDict;
readonly IHighlightingDefinitionReferenceResolver resolver;
HashSet<XshdRuleSet> processingStartedRuleSets = new HashSet<XshdRuleSet>();
HashSet<XshdRuleSet> processedRuleSets = new HashSet<XshdRuleSet>();
bool ignoreCase;
public TranslateElementVisitor(XmlHighlightingDefinition def, Dictionary<XshdRuleSet, HighlightingRuleSet> ruleSetDict, IHighlightingDefinitionReferenceResolver resolver)
{
Debug.Assert(def != null);
Debug.Assert(ruleSetDict != null);
this.def = def;
this.ruleSetDict = ruleSetDict;
this.resolver = resolver;
reverseRuleSetDict = new Dictionary<HighlightingRuleSet, XshdRuleSet>();
foreach (var pair in ruleSetDict) {
reverseRuleSetDict.Add(pair.Value, pair.Key);
}
}
public object VisitRuleSet(XshdRuleSet ruleSet)
{
HighlightingRuleSet rs = ruleSetDict[ruleSet];
if (processedRuleSets.Contains(ruleSet))
return rs;
if (!processingStartedRuleSets.Add(ruleSet))
throw Error(ruleSet, "RuleSet cannot be processed because it contains cyclic <Import>");
bool oldIgnoreCase = ignoreCase;
if (ruleSet.IgnoreCase != null)
ignoreCase = ruleSet.IgnoreCase.Value;
rs.Name = ruleSet.Name;
foreach (XshdElement element in ruleSet.Elements) {
object o = element.AcceptVisitor(this);
HighlightingRuleSet elementRuleSet = o as HighlightingRuleSet;
if (elementRuleSet != null) {
Merge(rs, elementRuleSet);
} else {
HighlightingSpan span = o as HighlightingSpan;
if (span != null) {
rs.Spans.Add(span);
} else {
HighlightingRule elementRule = o as HighlightingRule;
if (elementRule != null) {
rs.Rules.Add(elementRule);
}
}
}
}
ignoreCase = oldIgnoreCase;
processedRuleSets.Add(ruleSet);
return rs;
}
static void Merge(HighlightingRuleSet target, HighlightingRuleSet source)
{
target.Rules.AddRange(source.Rules);
target.Spans.AddRange(source.Spans);
}
public object VisitColor(XshdColor color)
{
HighlightingColor c;
if (color.Name != null)
c = def.colorDict[color.Name];
else if (color.Foreground == null && color.FontStyle == null && color.FontWeight == null)
return null;
else
c = new HighlightingColor();
c.Name = color.Name;
c.Foreground = color.Foreground;
c.Background = color.Background;
c.FontStyle = color.FontStyle;
c.FontWeight = color.FontWeight;
return c;
}
public object VisitKeywords(XshdKeywords keywords)
{
if (keywords.Words.Count == 0)
return Error(keywords, "Keyword group must not be empty.");
foreach (string keyword in keywords.Words) {
if (string.IsNullOrEmpty(keyword))
throw Error(keywords, "Cannot use empty string as keyword");
}
StringBuilder keyWordRegex = new StringBuilder();
// We can use "\b" only where the keyword starts/ends with a letter or digit, otherwise we don't
// highlight correctly. (example: ILAsm-Mode.xshd with ".maxstack" keyword)
if (keywords.Words.All(IsSimpleWord)) {
keyWordRegex.Append(@"\b(?>");
// (?> = atomic group
// atomic groups increase matching performance, but we
// must ensure that the keywords are sorted correctly.
// "\b(?>in|int)\b" does not match "int" because the atomic group captures "in".
// To solve this, we are sorting the keywords by descending length.
int i = 0;
foreach (string keyword in keywords.Words.OrderByDescending(w=>w.Length)) {
if (i++ > 0)
keyWordRegex.Append('|');
keyWordRegex.Append(Regex.Escape(keyword));
}
keyWordRegex.Append(@")\b");
} else {
keyWordRegex.Append('(');
int i = 0;
foreach (string keyword in keywords.Words) {
if (i++ > 0)
keyWordRegex.Append('|');
if (char.IsLetterOrDigit(keyword[0]))
keyWordRegex.Append(@"\b");
keyWordRegex.Append(Regex.Escape(keyword));
if (char.IsLetterOrDigit(keyword[keyword.Length - 1]))
keyWordRegex.Append(@"\b");
}
keyWordRegex.Append(')');
}
return new HighlightingRule {
Color = GetColor(keywords, keywords.ColorReference),
Regex = CreateRegex(keywords, keyWordRegex.ToString(), XshdRegexType.Default)
};
}
static bool IsSimpleWord(string word)
{
return char.IsLetterOrDigit(word[0]) && char.IsLetterOrDigit(word, word.Length - 1);
}
Regex CreateRegex(XshdElement position, string regex, XshdRegexType regexType)
{
if (regex == null)
throw Error(position, "Regex missing");
RegexOptions options = RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture;
if (regexType == XshdRegexType.IgnorePatternWhitespace)
options |= RegexOptions.IgnorePatternWhitespace;
if (ignoreCase)
options |= RegexOptions.IgnoreCase;
try {
return new Regex(regex, options);
} catch (ArgumentException ex) {
throw Error(position, ex.Message);
}
}
HighlightingColor GetColor(XshdElement position, XshdReference<XshdColor> colorReference)
{
if (colorReference.InlineElement != null) {
return (HighlightingColor)colorReference.InlineElement.AcceptVisitor(this);
} else if (colorReference.ReferencedElement != null) {
IHighlightingDefinition definition = GetDefinition(position, colorReference.ReferencedDefinition);
HighlightingColor color = definition.GetNamedColor(colorReference.ReferencedElement);
if (color == null)
throw Error(position, "Could not find color named '" + colorReference.ReferencedElement + "'.");
return color;
} else {
return null;
}
}
IHighlightingDefinition GetDefinition(XshdElement position, string definitionName)
{
if (definitionName == null)
return def;
if (resolver == null)
throw Error(position, "Resolving references to other syntax definitions is not possible because the IHighlightingDefinitionReferenceResolver is null.");
IHighlightingDefinition d = resolver.GetDefinition(definitionName);
if (d == null)
throw Error(position, "Could not find definition with name '" + definitionName + "'.");
return d;
}
HighlightingRuleSet GetRuleSet(XshdElement position, XshdReference<XshdRuleSet> ruleSetReference)
{
if (ruleSetReference.InlineElement != null) {
return (HighlightingRuleSet)ruleSetReference.InlineElement.AcceptVisitor(this);
} else if (ruleSetReference.ReferencedElement != null) {
IHighlightingDefinition definition = GetDefinition(position, ruleSetReference.ReferencedDefinition);
HighlightingRuleSet ruleSet = definition.GetNamedRuleSet(ruleSetReference.ReferencedElement);
if (ruleSet == null)
throw Error(position, "Could not find rule set named '" + ruleSetReference.ReferencedElement + "'.");
return ruleSet;
} else {
return null;
}
}
public object VisitSpan(XshdSpan span)
{
string endRegex = span.EndRegex;
if (string.IsNullOrEmpty(span.BeginRegex) && string.IsNullOrEmpty(span.EndRegex))
throw Error(span, "Span has no start/end regex.");
if (!span.Multiline) {
if (endRegex == null)
endRegex = "$";
else if (span.EndRegexType == XshdRegexType.IgnorePatternWhitespace)
endRegex = "($|" + endRegex + "\n)";
else
endRegex = "($|" + endRegex + ")";
}
HighlightingColor wholeSpanColor = GetColor(span, span.SpanColorReference);
return new HighlightingSpan {
StartExpression = CreateRegex(span, span.BeginRegex, span.BeginRegexType),
EndExpression = CreateRegex(span, endRegex, span.EndRegexType),
RuleSet = GetRuleSet(span, span.RuleSetReference),
StartColor = GetColor(span, span.BeginColorReference),
SpanColor = wholeSpanColor,
EndColor = GetColor(span, span.EndColorReference),
SpanColorIncludesStart = true,
SpanColorIncludesEnd = true
};
}
public object VisitImport(XshdImport import)
{
HighlightingRuleSet hrs = GetRuleSet(import, import.RuleSetReference);
XshdRuleSet inputRuleSet;
if (reverseRuleSetDict.TryGetValue(hrs, out inputRuleSet)) {
// ensure the ruleset is processed before importing its members
if (VisitRuleSet(inputRuleSet) != hrs)
Debug.Fail("this shouldn't happen");
}
return hrs;
}
public object VisitRule(XshdRule rule)
{
return new HighlightingRule {
Color = GetColor(rule, rule.ColorReference),
Regex = CreateRegex(rule, rule.Regex, rule.RegexType)
};
}
}
#endregion
static Exception Error(XshdElement element, string message)
{
if (element.LineNumber > 0)
return new HighlightingDefinitionInvalidException(
"Error at line " + element.LineNumber + ":\n" + message);
else
return new HighlightingDefinitionInvalidException(message);
}
Dictionary<string, HighlightingRuleSet> ruleSetDict = new Dictionary<string, HighlightingRuleSet>();
Dictionary<string, HighlightingColor> colorDict = new Dictionary<string, HighlightingColor>();
[OptionalField]
Dictionary<string, string> propDict = new Dictionary<string, string>();
public HighlightingRuleSet MainRuleSet { get; private set; }
public HighlightingRuleSet GetNamedRuleSet(string name)
{
if (string.IsNullOrEmpty(name))
return MainRuleSet;
HighlightingRuleSet r;
if (ruleSetDict.TryGetValue(name, out r))
return r;
else
return null;
}
public HighlightingColor GetNamedColor(string name)
{
HighlightingColor c;
if (colorDict.TryGetValue(name, out c))
return c;
else
return null;
}
public IEnumerable<HighlightingColor> NamedHighlightingColors {
get {
return colorDict.Values;
}
}
public override string ToString()
{
return this.Name;
}
public IDictionary<string, string> Properties {
get {
return propDict;
}
}
}
}