1 | // Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team |
---|
2 | // |
---|
3 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this |
---|
4 | // software and associated documentation files (the "Software"), to deal in the Software |
---|
5 | // without restriction, including without limitation the rights to use, copy, modify, merge, |
---|
6 | // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons |
---|
7 | // to whom the Software is furnished to do so, subject to the following conditions: |
---|
8 | // |
---|
9 | // The above copyright notice and this permission notice shall be included in all copies or |
---|
10 | // substantial portions of the Software. |
---|
11 | // |
---|
12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, |
---|
13 | // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR |
---|
14 | // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE |
---|
15 | // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR |
---|
16 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER |
---|
17 | // DEALINGS IN THE SOFTWARE. |
---|
18 | |
---|
19 | using System; |
---|
20 | using System.Collections.Generic; |
---|
21 | using System.Diagnostics; |
---|
22 | using System.Linq; |
---|
23 | using System.Runtime.Serialization; |
---|
24 | using System.Text; |
---|
25 | using System.Text.RegularExpressions; |
---|
26 | using ICSharpCode.AvalonEdit.Utils; |
---|
27 | |
---|
28 | namespace ICSharpCode.AvalonEdit.Highlighting.Xshd |
---|
29 | { |
---|
30 | [Serializable] |
---|
31 | sealed class XmlHighlightingDefinition : IHighlightingDefinition |
---|
32 | { |
---|
33 | public string Name { get; private set; } |
---|
34 | |
---|
35 | public XmlHighlightingDefinition(XshdSyntaxDefinition xshd, IHighlightingDefinitionReferenceResolver resolver) |
---|
36 | { |
---|
37 | this.Name = xshd.Name; |
---|
38 | // Create HighlightingRuleSet instances |
---|
39 | var rnev = new RegisterNamedElementsVisitor(this); |
---|
40 | xshd.AcceptElements(rnev); |
---|
41 | // Assign MainRuleSet so that references can be resolved |
---|
42 | foreach (XshdElement element in xshd.Elements) { |
---|
43 | XshdRuleSet xrs = element as XshdRuleSet; |
---|
44 | if (xrs != null && xrs.Name == null) { |
---|
45 | if (MainRuleSet != null) |
---|
46 | throw Error(element, "Duplicate main RuleSet. There must be only one nameless RuleSet!"); |
---|
47 | else |
---|
48 | MainRuleSet = rnev.ruleSets[xrs]; |
---|
49 | } |
---|
50 | } |
---|
51 | if (MainRuleSet == null) |
---|
52 | throw new HighlightingDefinitionInvalidException("Could not find main RuleSet."); |
---|
53 | // Translate elements within the rulesets (resolving references and processing imports) |
---|
54 | xshd.AcceptElements(new TranslateElementVisitor(this, rnev.ruleSets, resolver)); |
---|
55 | |
---|
56 | foreach (var p in xshd.Elements.OfType<XshdProperty>()) |
---|
57 | propDict.Add(p.Name, p.Value); |
---|
58 | } |
---|
59 | |
---|
60 | #region RegisterNamedElements |
---|
61 | sealed class RegisterNamedElementsVisitor : IXshdVisitor |
---|
62 | { |
---|
63 | XmlHighlightingDefinition def; |
---|
64 | internal readonly Dictionary<XshdRuleSet, HighlightingRuleSet> ruleSets |
---|
65 | = new Dictionary<XshdRuleSet, HighlightingRuleSet>(); |
---|
66 | |
---|
67 | public RegisterNamedElementsVisitor(XmlHighlightingDefinition def) |
---|
68 | { |
---|
69 | this.def = def; |
---|
70 | } |
---|
71 | |
---|
72 | public object VisitRuleSet(XshdRuleSet ruleSet) |
---|
73 | { |
---|
74 | HighlightingRuleSet hrs = new HighlightingRuleSet(); |
---|
75 | ruleSets.Add(ruleSet, hrs); |
---|
76 | if (ruleSet.Name != null) { |
---|
77 | if (ruleSet.Name.Length == 0) |
---|
78 | throw Error(ruleSet, "Name must not be the empty string"); |
---|
79 | if (def.ruleSetDict.ContainsKey(ruleSet.Name)) |
---|
80 | throw Error(ruleSet, "Duplicate rule set name '" + ruleSet.Name + "'."); |
---|
81 | |
---|
82 | def.ruleSetDict.Add(ruleSet.Name, hrs); |
---|
83 | } |
---|
84 | ruleSet.AcceptElements(this); |
---|
85 | return null; |
---|
86 | } |
---|
87 | |
---|
88 | public object VisitColor(XshdColor color) |
---|
89 | { |
---|
90 | if (color.Name != null) { |
---|
91 | if (color.Name.Length == 0) |
---|
92 | throw Error(color, "Name must not be the empty string"); |
---|
93 | if (def.colorDict.ContainsKey(color.Name)) |
---|
94 | throw Error(color, "Duplicate color name '" + color.Name + "'."); |
---|
95 | |
---|
96 | def.colorDict.Add(color.Name, new HighlightingColor()); |
---|
97 | } |
---|
98 | return null; |
---|
99 | } |
---|
100 | |
---|
101 | public object VisitKeywords(XshdKeywords keywords) |
---|
102 | { |
---|
103 | return keywords.ColorReference.AcceptVisitor(this); |
---|
104 | } |
---|
105 | |
---|
106 | public object VisitSpan(XshdSpan span) |
---|
107 | { |
---|
108 | span.BeginColorReference.AcceptVisitor(this); |
---|
109 | span.SpanColorReference.AcceptVisitor(this); |
---|
110 | span.EndColorReference.AcceptVisitor(this); |
---|
111 | return span.RuleSetReference.AcceptVisitor(this); |
---|
112 | } |
---|
113 | |
---|
114 | public object VisitImport(XshdImport import) |
---|
115 | { |
---|
116 | return import.RuleSetReference.AcceptVisitor(this); |
---|
117 | } |
---|
118 | |
---|
119 | public object VisitRule(XshdRule rule) |
---|
120 | { |
---|
121 | return rule.ColorReference.AcceptVisitor(this); |
---|
122 | } |
---|
123 | } |
---|
124 | #endregion |
---|
125 | |
---|
126 | #region TranslateElements |
---|
127 | sealed class TranslateElementVisitor : IXshdVisitor |
---|
128 | { |
---|
129 | readonly XmlHighlightingDefinition def; |
---|
130 | readonly Dictionary<XshdRuleSet, HighlightingRuleSet> ruleSetDict; |
---|
131 | readonly Dictionary<HighlightingRuleSet, XshdRuleSet> reverseRuleSetDict; |
---|
132 | readonly IHighlightingDefinitionReferenceResolver resolver; |
---|
133 | HashSet<XshdRuleSet> processingStartedRuleSets = new HashSet<XshdRuleSet>(); |
---|
134 | HashSet<XshdRuleSet> processedRuleSets = new HashSet<XshdRuleSet>(); |
---|
135 | bool ignoreCase; |
---|
136 | |
---|
137 | public TranslateElementVisitor(XmlHighlightingDefinition def, Dictionary<XshdRuleSet, HighlightingRuleSet> ruleSetDict, IHighlightingDefinitionReferenceResolver resolver) |
---|
138 | { |
---|
139 | Debug.Assert(def != null); |
---|
140 | Debug.Assert(ruleSetDict != null); |
---|
141 | this.def = def; |
---|
142 | this.ruleSetDict = ruleSetDict; |
---|
143 | this.resolver = resolver; |
---|
144 | reverseRuleSetDict = new Dictionary<HighlightingRuleSet, XshdRuleSet>(); |
---|
145 | foreach (var pair in ruleSetDict) { |
---|
146 | reverseRuleSetDict.Add(pair.Value, pair.Key); |
---|
147 | } |
---|
148 | } |
---|
149 | |
---|
150 | public object VisitRuleSet(XshdRuleSet ruleSet) |
---|
151 | { |
---|
152 | HighlightingRuleSet rs = ruleSetDict[ruleSet]; |
---|
153 | if (processedRuleSets.Contains(ruleSet)) |
---|
154 | return rs; |
---|
155 | if (!processingStartedRuleSets.Add(ruleSet)) |
---|
156 | throw Error(ruleSet, "RuleSet cannot be processed because it contains cyclic <Import>"); |
---|
157 | |
---|
158 | bool oldIgnoreCase = ignoreCase; |
---|
159 | if (ruleSet.IgnoreCase != null) |
---|
160 | ignoreCase = ruleSet.IgnoreCase.Value; |
---|
161 | |
---|
162 | rs.Name = ruleSet.Name; |
---|
163 | |
---|
164 | foreach (XshdElement element in ruleSet.Elements) { |
---|
165 | object o = element.AcceptVisitor(this); |
---|
166 | HighlightingRuleSet elementRuleSet = o as HighlightingRuleSet; |
---|
167 | if (elementRuleSet != null) { |
---|
168 | Merge(rs, elementRuleSet); |
---|
169 | } else { |
---|
170 | HighlightingSpan span = o as HighlightingSpan; |
---|
171 | if (span != null) { |
---|
172 | rs.Spans.Add(span); |
---|
173 | } else { |
---|
174 | HighlightingRule elementRule = o as HighlightingRule; |
---|
175 | if (elementRule != null) { |
---|
176 | rs.Rules.Add(elementRule); |
---|
177 | } |
---|
178 | } |
---|
179 | } |
---|
180 | } |
---|
181 | |
---|
182 | ignoreCase = oldIgnoreCase; |
---|
183 | processedRuleSets.Add(ruleSet); |
---|
184 | |
---|
185 | return rs; |
---|
186 | } |
---|
187 | |
---|
188 | static void Merge(HighlightingRuleSet target, HighlightingRuleSet source) |
---|
189 | { |
---|
190 | target.Rules.AddRange(source.Rules); |
---|
191 | target.Spans.AddRange(source.Spans); |
---|
192 | } |
---|
193 | |
---|
194 | public object VisitColor(XshdColor color) |
---|
195 | { |
---|
196 | HighlightingColor c; |
---|
197 | if (color.Name != null) |
---|
198 | c = def.colorDict[color.Name]; |
---|
199 | else if (color.Foreground == null && color.FontStyle == null && color.FontWeight == null) |
---|
200 | return null; |
---|
201 | else |
---|
202 | c = new HighlightingColor(); |
---|
203 | |
---|
204 | c.Name = color.Name; |
---|
205 | c.Foreground = color.Foreground; |
---|
206 | c.Background = color.Background; |
---|
207 | c.FontStyle = color.FontStyle; |
---|
208 | c.FontWeight = color.FontWeight; |
---|
209 | return c; |
---|
210 | } |
---|
211 | |
---|
212 | public object VisitKeywords(XshdKeywords keywords) |
---|
213 | { |
---|
214 | if (keywords.Words.Count == 0) |
---|
215 | return Error(keywords, "Keyword group must not be empty."); |
---|
216 | foreach (string keyword in keywords.Words) { |
---|
217 | if (string.IsNullOrEmpty(keyword)) |
---|
218 | throw Error(keywords, "Cannot use empty string as keyword"); |
---|
219 | } |
---|
220 | StringBuilder keyWordRegex = new StringBuilder(); |
---|
221 | // We can use "\b" only where the keyword starts/ends with a letter or digit, otherwise we don't |
---|
222 | // highlight correctly. (example: ILAsm-Mode.xshd with ".maxstack" keyword) |
---|
223 | if (keywords.Words.All(IsSimpleWord)) { |
---|
224 | keyWordRegex.Append(@"\b(?>"); |
---|
225 | // (?> = atomic group |
---|
226 | // atomic groups increase matching performance, but we |
---|
227 | // must ensure that the keywords are sorted correctly. |
---|
228 | // "\b(?>in|int)\b" does not match "int" because the atomic group captures "in". |
---|
229 | // To solve this, we are sorting the keywords by descending length. |
---|
230 | int i = 0; |
---|
231 | foreach (string keyword in keywords.Words.OrderByDescending(w=>w.Length)) { |
---|
232 | if (i++ > 0) |
---|
233 | keyWordRegex.Append('|'); |
---|
234 | keyWordRegex.Append(Regex.Escape(keyword)); |
---|
235 | } |
---|
236 | keyWordRegex.Append(@")\b"); |
---|
237 | } else { |
---|
238 | keyWordRegex.Append('('); |
---|
239 | int i = 0; |
---|
240 | foreach (string keyword in keywords.Words) { |
---|
241 | if (i++ > 0) |
---|
242 | keyWordRegex.Append('|'); |
---|
243 | if (char.IsLetterOrDigit(keyword[0])) |
---|
244 | keyWordRegex.Append(@"\b"); |
---|
245 | keyWordRegex.Append(Regex.Escape(keyword)); |
---|
246 | if (char.IsLetterOrDigit(keyword[keyword.Length - 1])) |
---|
247 | keyWordRegex.Append(@"\b"); |
---|
248 | } |
---|
249 | keyWordRegex.Append(')'); |
---|
250 | } |
---|
251 | return new HighlightingRule { |
---|
252 | Color = GetColor(keywords, keywords.ColorReference), |
---|
253 | Regex = CreateRegex(keywords, keyWordRegex.ToString(), XshdRegexType.Default) |
---|
254 | }; |
---|
255 | } |
---|
256 | |
---|
257 | static bool IsSimpleWord(string word) |
---|
258 | { |
---|
259 | return char.IsLetterOrDigit(word[0]) && char.IsLetterOrDigit(word, word.Length - 1); |
---|
260 | } |
---|
261 | |
---|
262 | Regex CreateRegex(XshdElement position, string regex, XshdRegexType regexType) |
---|
263 | { |
---|
264 | if (regex == null) |
---|
265 | throw Error(position, "Regex missing"); |
---|
266 | RegexOptions options = RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture; |
---|
267 | if (regexType == XshdRegexType.IgnorePatternWhitespace) |
---|
268 | options |= RegexOptions.IgnorePatternWhitespace; |
---|
269 | if (ignoreCase) |
---|
270 | options |= RegexOptions.IgnoreCase; |
---|
271 | try { |
---|
272 | return new Regex(regex, options); |
---|
273 | } catch (ArgumentException ex) { |
---|
274 | throw Error(position, ex.Message); |
---|
275 | } |
---|
276 | } |
---|
277 | |
---|
278 | HighlightingColor GetColor(XshdElement position, XshdReference<XshdColor> colorReference) |
---|
279 | { |
---|
280 | if (colorReference.InlineElement != null) { |
---|
281 | return (HighlightingColor)colorReference.InlineElement.AcceptVisitor(this); |
---|
282 | } else if (colorReference.ReferencedElement != null) { |
---|
283 | IHighlightingDefinition definition = GetDefinition(position, colorReference.ReferencedDefinition); |
---|
284 | HighlightingColor color = definition.GetNamedColor(colorReference.ReferencedElement); |
---|
285 | if (color == null) |
---|
286 | throw Error(position, "Could not find color named '" + colorReference.ReferencedElement + "'."); |
---|
287 | return color; |
---|
288 | } else { |
---|
289 | return null; |
---|
290 | } |
---|
291 | } |
---|
292 | |
---|
293 | IHighlightingDefinition GetDefinition(XshdElement position, string definitionName) |
---|
294 | { |
---|
295 | if (definitionName == null) |
---|
296 | return def; |
---|
297 | if (resolver == null) |
---|
298 | throw Error(position, "Resolving references to other syntax definitions is not possible because the IHighlightingDefinitionReferenceResolver is null."); |
---|
299 | IHighlightingDefinition d = resolver.GetDefinition(definitionName); |
---|
300 | if (d == null) |
---|
301 | throw Error(position, "Could not find definition with name '" + definitionName + "'."); |
---|
302 | return d; |
---|
303 | } |
---|
304 | |
---|
305 | HighlightingRuleSet GetRuleSet(XshdElement position, XshdReference<XshdRuleSet> ruleSetReference) |
---|
306 | { |
---|
307 | if (ruleSetReference.InlineElement != null) { |
---|
308 | return (HighlightingRuleSet)ruleSetReference.InlineElement.AcceptVisitor(this); |
---|
309 | } else if (ruleSetReference.ReferencedElement != null) { |
---|
310 | IHighlightingDefinition definition = GetDefinition(position, ruleSetReference.ReferencedDefinition); |
---|
311 | HighlightingRuleSet ruleSet = definition.GetNamedRuleSet(ruleSetReference.ReferencedElement); |
---|
312 | if (ruleSet == null) |
---|
313 | throw Error(position, "Could not find rule set named '" + ruleSetReference.ReferencedElement + "'."); |
---|
314 | return ruleSet; |
---|
315 | } else { |
---|
316 | return null; |
---|
317 | } |
---|
318 | } |
---|
319 | |
---|
320 | public object VisitSpan(XshdSpan span) |
---|
321 | { |
---|
322 | string endRegex = span.EndRegex; |
---|
323 | if (string.IsNullOrEmpty(span.BeginRegex) && string.IsNullOrEmpty(span.EndRegex)) |
---|
324 | throw Error(span, "Span has no start/end regex."); |
---|
325 | if (!span.Multiline) { |
---|
326 | if (endRegex == null) |
---|
327 | endRegex = "$"; |
---|
328 | else if (span.EndRegexType == XshdRegexType.IgnorePatternWhitespace) |
---|
329 | endRegex = "($|" + endRegex + "\n)"; |
---|
330 | else |
---|
331 | endRegex = "($|" + endRegex + ")"; |
---|
332 | } |
---|
333 | HighlightingColor wholeSpanColor = GetColor(span, span.SpanColorReference); |
---|
334 | return new HighlightingSpan { |
---|
335 | StartExpression = CreateRegex(span, span.BeginRegex, span.BeginRegexType), |
---|
336 | EndExpression = CreateRegex(span, endRegex, span.EndRegexType), |
---|
337 | RuleSet = GetRuleSet(span, span.RuleSetReference), |
---|
338 | StartColor = GetColor(span, span.BeginColorReference), |
---|
339 | SpanColor = wholeSpanColor, |
---|
340 | EndColor = GetColor(span, span.EndColorReference), |
---|
341 | SpanColorIncludesStart = true, |
---|
342 | SpanColorIncludesEnd = true |
---|
343 | }; |
---|
344 | } |
---|
345 | |
---|
346 | public object VisitImport(XshdImport import) |
---|
347 | { |
---|
348 | HighlightingRuleSet hrs = GetRuleSet(import, import.RuleSetReference); |
---|
349 | XshdRuleSet inputRuleSet; |
---|
350 | if (reverseRuleSetDict.TryGetValue(hrs, out inputRuleSet)) { |
---|
351 | // ensure the ruleset is processed before importing its members |
---|
352 | if (VisitRuleSet(inputRuleSet) != hrs) |
---|
353 | Debug.Fail("this shouldn't happen"); |
---|
354 | } |
---|
355 | return hrs; |
---|
356 | } |
---|
357 | |
---|
358 | public object VisitRule(XshdRule rule) |
---|
359 | { |
---|
360 | return new HighlightingRule { |
---|
361 | Color = GetColor(rule, rule.ColorReference), |
---|
362 | Regex = CreateRegex(rule, rule.Regex, rule.RegexType) |
---|
363 | }; |
---|
364 | } |
---|
365 | } |
---|
366 | #endregion |
---|
367 | |
---|
368 | static Exception Error(XshdElement element, string message) |
---|
369 | { |
---|
370 | if (element.LineNumber > 0) |
---|
371 | return new HighlightingDefinitionInvalidException( |
---|
372 | "Error at line " + element.LineNumber + ":\n" + message); |
---|
373 | else |
---|
374 | return new HighlightingDefinitionInvalidException(message); |
---|
375 | } |
---|
376 | |
---|
377 | Dictionary<string, HighlightingRuleSet> ruleSetDict = new Dictionary<string, HighlightingRuleSet>(); |
---|
378 | Dictionary<string, HighlightingColor> colorDict = new Dictionary<string, HighlightingColor>(); |
---|
379 | [OptionalField] |
---|
380 | Dictionary<string, string> propDict = new Dictionary<string, string>(); |
---|
381 | |
---|
382 | public HighlightingRuleSet MainRuleSet { get; private set; } |
---|
383 | |
---|
384 | public HighlightingRuleSet GetNamedRuleSet(string name) |
---|
385 | { |
---|
386 | if (string.IsNullOrEmpty(name)) |
---|
387 | return MainRuleSet; |
---|
388 | HighlightingRuleSet r; |
---|
389 | if (ruleSetDict.TryGetValue(name, out r)) |
---|
390 | return r; |
---|
391 | else |
---|
392 | return null; |
---|
393 | } |
---|
394 | |
---|
395 | public HighlightingColor GetNamedColor(string name) |
---|
396 | { |
---|
397 | HighlightingColor c; |
---|
398 | if (colorDict.TryGetValue(name, out c)) |
---|
399 | return c; |
---|
400 | else |
---|
401 | return null; |
---|
402 | } |
---|
403 | |
---|
404 | public IEnumerable<HighlightingColor> NamedHighlightingColors { |
---|
405 | get { |
---|
406 | return colorDict.Values; |
---|
407 | } |
---|
408 | } |
---|
409 | |
---|
410 | public override string ToString() |
---|
411 | { |
---|
412 | return this.Name; |
---|
413 | } |
---|
414 | |
---|
415 | public IDictionary<string, string> Properties { |
---|
416 | get { |
---|
417 | return propDict; |
---|
418 | } |
---|
419 | } |
---|
420 | } |
---|
421 | } |
---|