// Formats the code (content) protected virtual string HighlightSource(string content) { if (string.IsNullOrEmpty(CommentCssClass)) { Debug.LogError("The CommentCssClass should not be null or empty"); } if (string.IsNullOrEmpty(QuotesCssClass)) { Debug.LogError("The CommentCssClass should not be null or empty"); } if (string.IsNullOrEmpty(TypeCssClass)) { Debug.LogError("The TypeCssClass should not be null or empty"); } // Some fairly secure token placeholders const string COMMENTS_TOKEN = "`````"; const string MULTILINECOMMENTS_TOKEN = "~~~~~"; const string QUOTES_TOKEN = "¬¬¬¬¬"; // Remove /* */ quotes, taken from ostermiller.org Regex regex = new Regex(@"/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/", RegexOptions.Singleline); List <string> multiLineComments = new List <string>(); if (regex.IsMatch(content)) { foreach (Match item in regex.Matches(content)) { if (!multiLineComments.Contains(item.Value)) { multiLineComments.Add(item.Value); } } } for (int i = 0; i < multiLineComments.Count; i++) { content = content.ReplaceToken(multiLineComments[i], MULTILINECOMMENTS_TOKEN, i); } // Remove the quotes first, so they don't get highlighted List <string> quotes = new List <string>(); bool onEscape = false; bool onComment1 = false; bool onComment2 = false; bool inQuotes = false; int start = -1; for (int i = 0; i < content.Length; i++) { if (content[i] == '/' && !inQuotes && !onComment1) { onComment1 = true; } else if (content[i] == '/' && !inQuotes && onComment1) { onComment2 = true; } else if (content[i] == '"' && !onEscape && !onComment2) { inQuotes = true; // stops cases of: var s = "// I'm a comment"; if (start > -1) { string quote = content.Substring(start, i - start + 1); if (!quotes.Contains(quote)) { quotes.Add(quote); } start = -1; inQuotes = false; } else { start = i; } } else if (content[i] == '\\' || content[i] == '\'') { onEscape = true; } else if (content[i] == '\n') { onComment1 = false; onComment2 = false; } else { onEscape = false; } } for (int i = 0; i < quotes.Count; i++) { content = content.ReplaceToken(quotes[i], QUOTES_TOKEN, i); } // Remove the comments next, so they don't get highlighted regex = new Regex("(/{2,3}.+)", RegexOptions.Multiline); List <string> comments = new List <string>(); if (regex.IsMatch(content)) { foreach (Match item in regex.Matches(content)) { if (!comments.Contains(item.Value + "\n")) { comments.Add(item.Value); } } } for (int i = 0; i < comments.Count; i++) { content = content.ReplaceToken(comments[i], COMMENTS_TOKEN, i); } // Highlight single quotes content = Regex.Replace(content, "('.{1,2}')", CssExtensions.GetCssReplacement(QuotesCssClass), RegexOptions.Singleline); // Highlight class names based on the logic: {space OR start of line OR >}{1 capital){alphanumeric}{space} regex = new Regex(@"((?:\s|^)[A-Z]\w+(?:\s))", RegexOptions.Singleline); List <string> highlightedClasses = new List <string>(); if (regex.IsMatch(content)) { foreach (Match item in regex.Matches(content)) { string val = item.Groups[1].Value; if (!highlightedClasses.Contains(val)) { highlightedClasses.Add(val); } } } for (int i = 0; i < highlightedClasses.Count; i++) { content = content.ReplaceWithCss(highlightedClasses[i].Trim(), TypeCssClass); } // Pass 2. Doing it in N passes due to my inferior regex knowledge of back/forwardtracking. // This does {space or [}{1 capital){alphanumeric}{]} regex = new Regex(@"(?:\s|\[)([A-Z]\w+)(?:\])", RegexOptions.Singleline); highlightedClasses = new List <string>(); if (regex.IsMatch(content)) { foreach (Match item in regex.Matches(content)) { string val = item.Groups[1].Value; if (!highlightedClasses.Contains(val)) { highlightedClasses.Add(val); } } } for (int i = 0; i < highlightedClasses.Count; i++) { content = content.ReplaceWithCss(highlightedClasses[i], TypeCssClass); } // Pass 3. Generics regex = new Regex(@"(?:\s|\[|\()([A-Z]\w+(?:<|<))", RegexOptions.Singleline); highlightedClasses = new List <string>(); if (regex.IsMatch(content)) { foreach (Match item in regex.Matches(content)) { string val = item.Groups[1].Value; if (!highlightedClasses.Contains(val)) { highlightedClasses.Add(val); } } } for (int i = 0; i < highlightedClasses.Count; i++) { string val = highlightedClasses[i]; val = val.Replace("<", "").Replace("<", ""); content = content.ReplaceWithCss(highlightedClasses[i], val, "<", TypeCssClass); } // Pass 4. new keyword with a type regex = new Regex(@"new\s+([A-Z]\w+)(?:\()", RegexOptions.Singleline); highlightedClasses = new List <string>(); if (regex.IsMatch(content)) { foreach (Match item in regex.Matches(content)) { string val = item.Groups[1].Value; if (!highlightedClasses.Contains(val)) { highlightedClasses.Add(val); } } } // Replace the highlighted classes for (int i = 0; i < highlightedClasses.Count; i++) { content = content.ReplaceWithCss(highlightedClasses[i], TypeCssClass); } // Highlight keywords foreach (KeywordStruct keyword in _keywords) { Regex regexKeyword = new Regex(@"(\W" + keyword.Word + "|^" + keyword.Word + @")(>|>|\s|\n|;|<|)", RegexOptions.Singleline); content = regexKeyword.Replace(content, CssExtensions.GetCssReplacement(keyword.Color) + "$2"); } // Shove the multiline comments back in for (int i = 0; i < multiLineComments.Count; i++) { content = content.ReplaceTokenWithCss(multiLineComments[i], MULTILINECOMMENTS_TOKEN, i, CommentCssClass); } // Shove the quotes back in for (int i = 0; i < quotes.Count; i++) { content = content.ReplaceTokenWithCss(quotes[i], QUOTES_TOKEN, i, QuotesCssClass); } // Shove the single line comments back in for (int i = 0; i < comments.Count; i++) { string comment = comments[i]; // Add quotes back in for (int n = 0; n < quotes.Count; n++) { comment = comment.Replace(string.Format("{0}{1}{0}", QUOTES_TOKEN, n), quotes[n]); } content = content.ReplaceTokenWithCss(comment, COMMENTS_TOKEN, i, CommentCssClass); } return(content); }
public async Task SelectorExpansionTest() { #region LESS sources var testSources = new[] { @" @media all { a { @media all { @media all { b { color: goldenrod; em { color: goldenrod; } } } } } } ", @" // Taken from http://blog.slaks.net/2013-09-29/less-css-secrets-of-the-ampersand/ a { color: blue; &:hover { color: green; } } form a { color: purple; body.QuietMode & { color: black; } } .quoted-source { background: #fcc; blockquote& { background: #fdc; } } .btn.btn-primary.btn-lg[disabled] { & + & + & { margin-left: 10px; } } p, blockquote, ul, li { border-top: 1px solid gray; & + & { border-top: 0; } } ", @" // Taken from https://github.com/less/less.js/blob/master/test/less/selectors.less h1, h2, h3 { a, p { &:hover { color: red; } } } #all { color: blue; } #the { color: blue; } #same { color: blue; } ul, li, div, q, blockquote, textarea { margin: 0; } td { margin: 0; padding: 0; } td, input { line-height: 1em; } a { color: red; &:hover { color: blue; } div & { color: green; } p & span { color: yellow; } } .foo { .bar, .baz { & .qux { display: block; } .qux & { display: inline; } .qux& { display: inline-block; } .qux & .biz { display: none; } } } .b { &.c { .a& { color: red; } } } .b { .c & { &.a { color: red; } } } .p { .foo &.bar { color: red; } } .p { .foo&.bar { color: red; } } .foo { .foo + & { background: amber; } & + & { background: amber; } } .foo, .bar { & + & { background: amber; } } .foo, .bar { a, b { & > & { background: amber; } } } .other ::fnord { color: red } .other::fnord { color: red } .other { ::bnord {color: red } &::bnord {color: red } } ", @"// Taken from https://github.com/less/less.js/blob/master/test/less/rulesets.less #first > .one { > #second .two > #deux { width: 50%; #third { &:focus { color: black; #fifth { > #sixth { .seventh #eighth { + #ninth { color: purple; } } } } } height: 100%; } #fourth, #five, #six { color: #110000; .seven, .eight > #nine { border: 1px solid black; } #ten { color: red; } } } font-size: 2em; } " }; #endregion var lessFactory = CssParserLocator.FindComponent(ContentTypeManager.GetContentType(LessContentTypeDefinition.LessContentType)); foreach (var lessCode in testSources) { var cssCode = await new LessCompiler().CompileSourceAsync(lessCode, ".less"); var lessDoc = lessFactory.CreateParser().Parse(lessCode, false); var cssDoc = new CssParser().Parse(cssCode, false); var cssSelectors = new CssItemAggregator <string>(false) { (Selector s) => CssExtensions.SelectorText(s) }.Crawl(cssDoc); var lessSelectors = new CssItemAggregator <RuleSet>(true) { (RuleSet rs) => rs }.Crawl(lessDoc) .Where(rs => rs.Block.Declarations.Any()) // Skip selectors that don't have any rules; these won't end up in the CSS .SelectMany(rs => LessDocument.GetSelectorNames(rs, LessMixinAction.Literal)) .ToList(); lessSelectors.Should().Equal(cssSelectors); } }