Commits

Tatham Oddie committed b8d4047

Now rendering the expression itself as nested HTML markup with node data to support hover effects.

Comments (0)

Files changed (6)

Web.Test/Controllers/AnalysisControllerTests.cs

             // Assert
             StringAssert.Contains(nodeClass, "ast-parse-failure-node");
         }
+
+        [TestMethod]
+        public void AnalysisController_RenderExpressionAsHtml_ShouldRenderSingleFlatNode()
+        {
+            // Arrange
+            var nodes = new Node[]
+            {
+                new LiteralNode("abc", 0) { NodeId = 1 }
+            };
+
+            // Act
+            var result = AnalysisController.RenderExpressionAsHtml(nodes).ToHtmlString();
+
+            // Assert
+            Assert.AreEqual("<span class=\"ast-node ast-node-1\">abc</span>", result);
+        }
+
+        [TestMethod]
+        public void AnalysisController_RenderExpressionAsHtml_ShouldRenderSequentialFlatNodes()
+        {
+            // Arrange
+            var nodes = new Node[]
+            {
+                new LiteralNode("abc", 0) { NodeId = 1 },
+                new LiteralNode("def", 3) { NodeId = 2 }
+            };
+
+            // Act
+            var result = AnalysisController.RenderExpressionAsHtml(nodes).ToHtmlString();
+
+            // Assert
+            Assert.AreEqual("<span class=\"ast-node ast-node-1\">abc</span><span class=\"ast-node ast-node-2\">def</span>", result);
+        }
+
+        [TestMethod]
+        public void AnalysisController_RenderExpressionAsHtml_ShouldRenderGroupNodeWithDataBeforeChildNode()
+        {
+            // Arrange
+            var nodes = new Node[]
+            {
+                new GroupNode("(abc", 0,
+                    new LiteralNode("abc", 1) { NodeId = 2 })
+                { NodeId = 1 }
+            };
+
+            // Act
+            var result = AnalysisController.RenderExpressionAsHtml(nodes).ToHtmlString();
+
+            // Assert
+            Assert.AreEqual("<span class=\"ast-node ast-node-1\">(<span class=\"ast-node ast-node-2\">abc</span></span>", result);
+        }
+
+        [TestMethod]
+        public void AnalysisController_RenderExpressionAsHtml_ShouldRenderGroupNodeWithDataAfterChildNode()
+        {
+            // Arrange
+            var nodes = new Node[]
+            {
+                new GroupNode("abc)", 0,
+                    new LiteralNode("abc", 0) { NodeId = 2 })
+                { NodeId = 1 }
+            };
+
+            // Act
+            var result = AnalysisController.RenderExpressionAsHtml(nodes).ToHtmlString();
+
+            // Assert
+            Assert.AreEqual("<span class=\"ast-node ast-node-1\"><span class=\"ast-node ast-node-2\">abc</span>)</span>", result);
+        }
+
+        [TestMethod]
+        public void AnalysisController_RenderExpressionAsHtml_ShouldRenderGroupNodeWithDataBeforeAndAfterChildNode()
+        {
+            // Arrange
+            var nodes = new Node[]
+            {
+                new GroupNode("(abc)", 0,
+                    new LiteralNode("abc", 1) { NodeId = 2 })
+                { NodeId = 1 }
+            };
+
+            // Act
+            var result = AnalysisController.RenderExpressionAsHtml(nodes).ToHtmlString();
+
+            // Assert
+            Assert.AreEqual("<span class=\"ast-node ast-node-1\">(<span class=\"ast-node ast-node-2\">abc</span>)</span>", result);
+        }
+
+        [TestMethod]
+        public void AnalysisController_RenderExpressionAsHtml_ShouldRenderGroupNodeWithDataBetweenChildNodes()
+        {
+            // Arrange
+            var nodes = new Node[]
+            {
+                new GroupNode("abc|def", 0,
+                    new LiteralNode("abc", 0) { NodeId = 2 },
+                    new LiteralNode("def", 4) { NodeId = 3 })
+                { NodeId = 1 }
+            };
+
+            // Act
+            var result = AnalysisController.RenderExpressionAsHtml(nodes).ToHtmlString();
+
+            // Assert
+            Assert.AreEqual("<span class=\"ast-node ast-node-1\"><span class=\"ast-node ast-node-2\">abc</span>|<span class=\"ast-node ast-node-3\">def</span></span>", result);
+        }
+
+        [TestMethod]
+        public void AnalysisController_RenderExpressionAsHtml_ShouldRenderGroupNodeWithDataBeforeAndAfterAndBetweenChildNodes()
+        {
+            // Arrange
+            var nodes = new Node[]
+            {
+                new GroupNode("(abc|def)", 0,
+                    new LiteralNode("abc", 1) { NodeId = 2 },
+                    new LiteralNode("def", 5) { NodeId = 3 })
+                { NodeId = 1 }
+            };
+
+            // Act
+            var result = AnalysisController.RenderExpressionAsHtml(nodes).ToHtmlString();
+
+            // Assert
+            Assert.AreEqual("<span class=\"ast-node ast-node-1\">(<span class=\"ast-node ast-node-2\">abc</span>|<span class=\"ast-node ast-node-3\">def</span>)</span>", result);
+        }
+
+        [TestMethod]
+        public void AnalysisController_RenderExpressionAsHtml_ShouldRenderNestedGroupNodesWithDataBeforeAndAfterAndBetweenChildNodes()
+        {
+            // Arrange
+            // abc(def(ghi)jkl(mno)pqr)stu
+            var nodes = new Node[]
+            {
+                new LiteralNode("abc", 0),
+                new GroupNode("(def(ghi)jkl(mno)pqr)", 3,
+                    new LiteralNode("def", 4),
+                    new GroupNode("(ghi)", 7,
+                        new LiteralNode("ghi", 8)),
+                    new LiteralNode("jkl", 12),
+                    new GroupNode("(mno)", 15,
+                        new LiteralNode("mno", 16)),
+                    new LiteralNode("pqr", 20)),
+                new LiteralNode("stu", 24)
+            };
+
+            // Act
+            var result = AnalysisController.RenderExpressionAsHtml(nodes).ToHtmlString();
+
+            // Assert
+            const string expected =
+                "<span class=\"ast-node ast-node-0\">abc</span>" +
+                "<span class=\"ast-node ast-node-0\">(" +
+                    "<span class=\"ast-node ast-node-0\">def</span>" +
+                    "<span class=\"ast-node ast-node-0\">(" +
+                        "<span class=\"ast-node ast-node-0\">ghi</span>" +
+                    ")</span>" +
+                    "<span class=\"ast-node ast-node-0\">jkl</span>" +
+                    "<span class=\"ast-node ast-node-0\">(" +
+                        "<span class=\"ast-node ast-node-0\">mno</span>" +
+                    ")</span>" +
+                    "<span class=\"ast-node ast-node-0\">pqr</span>" +
+                ")</span>" +
+                "<span class=\"ast-node ast-node-0\">stu</span>";
+            Assert.AreEqual(expected, result);
+        }
     }
 }

Web.Test/Web.Test.csproj

     <Reference Include="System.Core">
       <RequiredTargetFramework>3.5</RequiredTargetFramework>
     </Reference>
+    <Reference Include="System.Web" />
     <Reference Include="System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL" />
   </ItemGroup>
   <ItemGroup>

Web/Assets/Styles.less

 @box-color: #aaa;
 @box-inner-border: #ddd;
 @box-background-color: #f7f7f7;
-@highlight-background-color: #ffb;
+@highlight-background-color: #ff9;
 @error-highlight-background-color: #fcc;
 
 * { margin: 0; padding: 0; }
 
 p { margin-bottom: 8px; }
 
+.ast-node-hover-candidate ()
+{
+    -webkit-transition-property: background-color;
+    -webkit-transition-duration: 0.5s;
+    -moz-transition-property: background-color;
+    -moz-transition-duration: 0.5s;
+    -o-transition-property: background-color;
+    -o-transition-duration: 0.5s;
+}
+
+.ast-node-hover-effect (@existing-background-color: #fff)
+{
+    background-color: @highlight-background-color;
+    .ast-node
+    {
+        background-color: @existing-background-color;
+    }
+}
+
 code
 {
+    font-weight: bold;
     white-space: pre-wrap;
     word-wrap: break-word;
     overflow: hidden;
     border: solid 1px @box-color;
     line-height: 140%;
     margin-bottom: 8px;
-}
-
-.ast-node-hover-candidate ()
-{
-    -webkit-transition-property: background-color;
-    -webkit-transition-duration: 1s;
-    -moz-transition-property: background-color;
-    -moz-transition-duration: 1s;
-    -o-transition-property: background-color;
-    -o-transition-duration: 1s;
-}
-
-.ast-node-hover ()
-{
-    background-color: @highlight-background-color;
+    .ast-node { display: inline-block; .ast-node-hover-candidate; }
+    .ast-parse-failure-node { background-color: @error-highlight-background-color; }
+    .ast-node-hover { .ast-node-hover-effect(@box-background-color); }
 }
 
 table
         tr { border-top: solid 1px @box-color; }
         tr.ast-node { .ast-node-hover-candidate; }
         tr.ast-parse-failure-node { background-color: @error-highlight-background-color; }
-        tr.ast-node:hover,
-        tr.ast-node-hover { .ast-node-hover; }
+        tr.ast-node-hover { .ast-node-hover-effect; }
     }
 }
 
         }
         .ast-node * * { .ast-node-hover-candidate; }
         .ast-parse-failure-node * * { background-color: @error-highlight-background-color; }
-        .ast-node:hover * *,
-        .ast-node-hover * * { .ast-node-hover; }
+        .ast-node-hover * * { .ast-node-hover-effect; }
         .ast-node-data
         {
             display: block;

Web/Controllers/AnalysisController.cs

-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
 using System.ComponentModel;
 using System.Diagnostics;
 using System.Linq;
 
             ViewData["TimeTaken"] = stopwatch.Elapsed;
 
+            ViewData["ExpressionMarkup"] = RenderExpressionAsHtml(nodes);
             ViewData["NodesMarkup"] = RenderNodesAsHtml(nodes);
 
             return View("Basic");
             ViewData["TimeTaken"] = stopwatch.Elapsed;
 
             ViewData["Tokens"] = tokens;
-            
+
+            ViewData["ExpressionMarkup"] = RenderExpressionAsHtml(nodes);
             ViewData["AllNodes"] = FlattenNodes(nodes);
             ViewData["NodesMarkup"] = RenderNodesAsHtml(nodes);
 
             }
         }
 
+        internal static IHtmlString RenderExpressionAsHtml(IEnumerable<Node> nodes)
+        {
+            var markupBuilder = new StringBuilder();
+            
+            var nodesToProcess = new Stack<Node>(nodes.Reverse());
+            var layers = new Stack<int>(new[] { nodes.Count() });
+            var nodeToParentDictionary = nodes.ToDictionary(n => n, p => (Node)null);
+            while (nodesToProcess.Any())
+            {
+                var layersToClose = 0;
+                while (layers.Any() && layers.Peek() == 0)
+                {
+                    layersToClose++;
+                    layers.Pop();
+                }
+
+                layers.Push(layers.Pop() - 1);
+
+                for (var i = 0; i < layersToClose; i++)
+                    markupBuilder.Append("</span>");
+
+                var currentNode = nodesToProcess.Pop();
+
+                RenderDataBetweenThisAndPreviousNode(markupBuilder, currentNode, nodeToParentDictionary);
+
+                markupBuilder.AppendFormat(
+                    "<span class=\"{0}\">",
+                    BuildNodeClass(currentNode));
+                
+                if (currentNode.Children.Any())
+                {
+                    layers.Push(currentNode.Children.Count());
+
+                    foreach (var childNode in currentNode.Children.Reverse())
+                    {
+                        nodesToProcess.Push(childNode);
+                        nodeToParentDictionary[childNode] = currentNode;
+                    }
+
+                    var firstChild = currentNode.Children.First();
+                    var firstChildStartOffset = firstChild.StartIndex - currentNode.StartIndex;
+
+                    if (firstChildStartOffset > 0)
+                        markupBuilder.Append(HttpUtility.HtmlEncode(currentNode.Data.Substring(0, firstChildStartOffset)));
+                }
+                else
+                {
+                    markupBuilder.Append(HttpUtility.HtmlEncode(currentNode.Data));
+
+                    markupBuilder.Append("</span>");
+                }
+
+                RenderRemainingDataIfThisIsTheLastNode(markupBuilder, currentNode, nodeToParentDictionary);
+            }
+
+            for (var i = 0; i < layers.Count() - 1; i++)
+                markupBuilder.Append("</span>");
+
+            return new HtmlString(markupBuilder.ToString());
+        }
+
+        static void RenderDataBetweenThisAndPreviousNode(StringBuilder markupBuilder, Node currentNode, IDictionary<Node, Node> nodeToParentDictionary)
+        {
+            var parentNode = nodeToParentDictionary[currentNode];
+            if (parentNode == null) return;
+
+            var indexOfCurrentNodeAtThisLevel = parentNode.Children.ToList().IndexOf(currentNode);
+            if (indexOfCurrentNodeAtThisLevel <= 0) return;
+
+            var previousNodeAtThisLevel = parentNode.Children.ElementAt(indexOfCurrentNodeAtThisLevel - 1);
+            var endIndexOfPreviousNodeAtThisLevel = previousNodeAtThisLevel.StartIndex + previousNodeAtThisLevel.Data.Length;
+            var numberOfCharactersBetweenPreviousAndCurrentNode = currentNode.StartIndex - endIndexOfPreviousNodeAtThisLevel;
+            if (numberOfCharactersBetweenPreviousAndCurrentNode <= 0) return;
+
+            var charactersBetweenPreviousAndCurrentNode = parentNode.Data.Substring(endIndexOfPreviousNodeAtThisLevel, numberOfCharactersBetweenPreviousAndCurrentNode);
+
+            markupBuilder.Append(charactersBetweenPreviousAndCurrentNode);
+        }
+
+        static void RenderRemainingDataIfThisIsTheLastNode(StringBuilder markupBuilder, Node currentNode, IDictionary<Node, Node> nodeToParentDictionary)
+        {
+            var parentNode = nodeToParentDictionary[currentNode];
+            if (parentNode == null) return;
+
+            var siblings = parentNode.Children.ToList();
+
+            var indexOfCurrentNodeAtThisLevel = siblings.IndexOf(currentNode);
+            if (indexOfCurrentNodeAtThisLevel < siblings.Count() - 1) return;
+
+            var endIndexOfCurrentNode = currentNode.StartIndex + currentNode.Data.Length;
+            var numberOfRemainingCharacters = parentNode.StartIndex + parentNode.Data.Length - endIndexOfCurrentNode;
+            if (numberOfRemainingCharacters <= 0) return;
+
+            var remainingCharacters = parentNode.Data.Substring(endIndexOfCurrentNode - parentNode.StartIndex, numberOfRemainingCharacters);
+
+            markupBuilder.Append(remainingCharacters);
+        }
+
         static IHtmlString RenderNodesAsHtml(IEnumerable<Node> nodes)
         {
             var markupBuilder = new StringBuilder();

Web/Views/Analysis/Basic.cshtml

 @using System.Web.Mvc.Html;
 
 <h2>You gave us this expression:</h2>
-<code class="code-block">@View.Expression</code>
+<code class="code-block">@View.ExpressionMarkup</code>
 
 <h2>This is how it works:</h2>
 @View.NodesMarkup

Web/Views/Analysis/Verbose.cshtml

 @inherits System.Web.Mvc.WebViewPage<dynamic>
 
 <h2>We started with this expression:</h2>
-<code class="code-block">@View.Expression</code>
+<code class="code-block">@View.ExpressionMarkup</code>
 
 <h2>First up, we broke it into these tokens:</h2>
 <table title="Expression tokens">