/// <summary> /// Renders the HTML buildable object /// </summary> /// <returns>HTML encoded string</returns> /// <remarks> /// This method should render the start tag, and the nested content of the HTML buildable, /// optionally may render the closing tag. /// </remarks> public override List <RenderedSegment> Render() { // --- Initialize the start tag of the element var segments = new List <RenderedSegment>(); if (!string.IsNullOrEmpty(Tag)) { var builder = new TagBuilder(Tag); builder.MergeAttributes(HtmlAttributes); var mode = VoidElements.Contains(Tag) ? TagRenderMode.SelfClosing : TagRenderMode.StartTag; segments.Add(new RenderedSegment(builder.ToString(mode), Depth)); } // --- Build all nested elements foreach (var child in _nestedElements) { segments.AddRange(child.Render()); segments.AddRange(child.Complete()); } // --- Now, we're ready return(segments); }
/// <summary> /// Renders the (optional) closing tag of the buildable object /// </summary> /// <returns>HTML encoded string</returns> /// <remarks> /// This tag should render the closing tag if and only if that Build() have not /// already done it. /// </remarks> public override List <RenderedSegment> Complete() { if (string.IsNullOrEmpty(Tag) || VoidElements.Contains(Tag)) { return(new List <RenderedSegment>()); } return(new List <RenderedSegment> { new RenderedSegment(string.Format("</{0}>", Tag), Depth) }); }
private void RewriteChildren(RazorSourceDocument source, IntermediateNode node) { // We expect all of the immediate children of a node (together) to comprise // a well-formed tree of elements and components. var stack = new Stack <IntermediateNode>(); stack.Push(node); // Make a copy, we will clear and rebuild the child collection of this node. var children = node.Children.ToArray(); node.Children.Clear(); // Due to the way Anglesharp parses HTML (tags at a time) we need to keep track of some state. // This handles cases like: // // <foo bar="17" baz="@baz" /> // // This will lower like: // // HtmlContent <foo bar="17" // HtmlAttribute baz=" - " // CSharpAttributeValue baz // HtmlContent /> // // We need to consume HTML until we see the 'end tag' for <foo /> and then we can // the attributes from the parsed HTML and the CSharpAttribute value. var parser = new HtmlParser(source); var attributes = new List <HtmlAttributeIntermediateNode>(); for (var i = 0; i < children.Length; i++) { if (children[i] is HtmlContentIntermediateNode htmlNode) { parser.Push(htmlNode); var tokens = parser.Get(); foreach (var token in tokens) { // We have to call this before get. Anglesharp doesn't return the start position // of tokens. var start = parser.GetCurrentLocation(); // We have to set the Location explicitly otherwise we would need to include // the token in every call to the parser. parser.SetLocation(token); var end = parser.GetCurrentLocation(); if (token.Type == HtmlTokenType.EndOfFile) { break; } switch (token.Type) { case HtmlTokenType.Doctype: { // DocType isn't meaningful in Blazor. We don't process them in the runtime // it wouldn't really mean much anyway since we build a DOM directly rather // than letting the user-agent parse the document. // // For now, <!DOCTYPE html> and similar things will just be skipped by the compiler // unless we come up with something more meaningful to do. break; } case HtmlTokenType.Character: { // Text content var span = new SourceSpan(start, end.AbsoluteIndex - start.AbsoluteIndex); stack.Peek().Children.Add(new HtmlContentIntermediateNode() { Children = { new IntermediateToken() { Content = token.Data, Kind = TokenKind.Html, Source = span, } }, Source = span, }); break; } case HtmlTokenType.StartTag: case HtmlTokenType.EndTag: { var tag = token.AsTag(); if (token.Type == HtmlTokenType.StartTag) { var elementNode = new HtmlElementIntermediateNode() { TagName = parser.GetTagNameOriginalCasing(tag), Source = new SourceSpan(start, end.AbsoluteIndex - start.AbsoluteIndex), }; stack.Peek().Children.Add(elementNode); stack.Push(elementNode); for (var j = 0; j < tag.Attributes.Count; j++) { // Unfortunately Anglesharp doesn't provide positions for attributes // so we can't record the spans here. var attribute = tag.Attributes[j]; stack.Peek().Children.Add(CreateAttributeNode(attribute)); } for (var j = 0; j < attributes.Count; j++) { stack.Peek().Children.Add(attributes[j]); } attributes.Clear(); } if (tag.IsSelfClosing || VoidElements.Contains(tag.Data)) { // We can't possibly hit an error here since we just added an element node. stack.Pop(); } if (token.Type == HtmlTokenType.EndTag) { var popped = stack.Pop(); if (stack.Count == 0) { // If we managed to 'bottom out' the stack then we have an unbalanced end tag. // Put back the current node so we don't crash. stack.Push(popped); var tagName = parser.GetTagNameOriginalCasing(token.AsTag()); var span = new SourceSpan(start, end.AbsoluteIndex - start.AbsoluteIndex); var diagnostic = BlazorDiagnosticFactory.Create_UnexpectedClosingTag(span, tagName); popped.Children.Add(new HtmlElementIntermediateNode() { Diagnostics = { diagnostic, }, TagName = tagName, Source = span, }); } else if (!string.Equals(tag.Name, ((HtmlElementIntermediateNode)popped).TagName, StringComparison.OrdinalIgnoreCase)) { var span = new SourceSpan(start, end.AbsoluteIndex - start.AbsoluteIndex); var diagnostic = BlazorDiagnosticFactory.Create_MismatchedClosingTag(span, ((HtmlElementIntermediateNode)popped).TagName, token.Data); popped.Diagnostics.Add(diagnostic); } else { // Happy path. // // We need to compute a new source span because when we found the start tag before we knew // the end poosition of the tag. var length = end.AbsoluteIndex - popped.Source.Value.AbsoluteIndex; popped.Source = new SourceSpan( popped.Source.Value.FilePath, popped.Source.Value.AbsoluteIndex, popped.Source.Value.LineIndex, popped.Source.Value.CharacterIndex, length); } } break; } case HtmlTokenType.Comment: break; default: throw new InvalidCastException($"Unsupported token type: {token.Type.ToString()}"); } } } else if (children[i] is HtmlAttributeIntermediateNode htmlAttribute) { // Buffer the attribute for now, it will get written out as part of a tag. attributes.Add(htmlAttribute); } else { // not HTML, or already rewritten. stack.Peek().Children.Add(children[i]); } } var extraContent = parser.GetUnparsedContent(); if (!string.IsNullOrEmpty(extraContent)) { // extra HTML - almost certainly invalid because it couldn't be parsed. var start = parser.GetCurrentLocation(); var end = parser.GetCurrentLocation(extraContent.Length); var span = new SourceSpan(start, end.AbsoluteIndex - start.AbsoluteIndex); stack.Peek().Children.Add(new HtmlContentIntermediateNode() { Children = { new IntermediateToken() { Content = extraContent, Kind = TokenKind.Html, Source = span, } }, Diagnostics = { BlazorDiagnosticFactory.Create_InvalidHtmlContent(span, extraContent), }, Source = span, }); } while (stack.Count > 1) { // not balanced var popped = (HtmlElementIntermediateNode)stack.Pop(); var diagnostic = BlazorDiagnosticFactory.Create_UnclosedTag(popped.Source, popped.TagName); popped.Diagnostics.Add(diagnostic); } }
private bool RestOfTag(Tuple <HtmlSymbol, SourceLocation> tag, Stack <Tuple <HtmlSymbol, SourceLocation> > tags) { TagContent(); // We are now at a possible end of the tag // Found '<', so we just abort this tag. if (At(HtmlSymbolType.OpenAngle)) { return(false); } bool isEmpty = At(HtmlSymbolType.Solidus); // Found a solidus, so don't accept it but DON'T push the tag to the stack if (isEmpty) { AcceptAndMoveNext(); } // Check for the '>' to determine if the tag is finished bool seenClose = Optional(HtmlSymbolType.CloseAngle); if (!seenClose) { Context.OnError(tag.Item2, RazorResources.ParseError_UnfinishedTag, tag.Item1.Content); } else { if (!isEmpty) { // Is this a void element? string tagName = tag.Item1.Content.Trim(); if (VoidElements.Contains(tagName)) { // Technically, void elements like "meta" are not allowed to have end tags. Just in case they do, // we need to look ahead at the next set of tokens. If we see "<", "/", tag name, accept it and the ">" following it // Place a bookmark int bookmark = CurrentLocation.AbsoluteIndex; // Skip whitespace IEnumerable <HtmlSymbol> ws = ReadWhile(IsSpacingToken(includeNewLines: true)); // Open Angle if (At(HtmlSymbolType.OpenAngle) && NextIs(HtmlSymbolType.Solidus)) { HtmlSymbol openAngle = CurrentSymbol; NextToken(); Assert(HtmlSymbolType.Solidus); HtmlSymbol solidus = CurrentSymbol; NextToken(); if (At(HtmlSymbolType.Text) && String.Equals(CurrentSymbol.Content, tagName, StringComparison.OrdinalIgnoreCase)) { // Accept up to here Accept(ws); Accept(openAngle); Accept(solidus); AcceptAndMoveNext(); // Accept to '>', '<' or EOF AcceptUntil(HtmlSymbolType.CloseAngle, HtmlSymbolType.OpenAngle); // Accept the '>' if we saw it. And if we do see it, we're complete return(Optional(HtmlSymbolType.CloseAngle)); } // At(HtmlSymbolType.Text) && String.Equals(CurrentSymbol.Content, tagName, StringComparison.OrdinalIgnoreCase) } // At(HtmlSymbolType.OpenAngle) && NextIs(HtmlSymbolType.Solidus) // Go back to the bookmark and just finish this tag at the close angle Context.Source.Position = bookmark; NextToken(); } else if (String.Equals(tagName, "script", StringComparison.OrdinalIgnoreCase)) { SkipToEndScriptAndParseCode(); } else { // Push the tag on to the stack tags.Push(tag); } } } return(seenClose); }
private bool RestOfTag(Tuple <HtmlSymbol, SourceLocation> tag, Stack <Tuple <HtmlSymbol, SourceLocation> > tags, IDisposable tagBlockWrapper) { TagContent(); // We are now at a possible end of the tag // Found '<', so we just abort this tag. if (At(HtmlSymbolType.OpenAngle)) { return(false); } var isEmpty = At(HtmlSymbolType.ForwardSlash); // Found a solidus, so don't accept it but DON'T push the tag to the stack if (isEmpty) { AcceptAndMoveNext(); } // Check for the '>' to determine if the tag is finished var seenClose = Optional(HtmlSymbolType.CloseAngle); if (!seenClose) { Context.OnError(tag.Item2, RazorResources.FormatParseError_UnfinishedTag(tag.Item1.Content)); } else { if (!isEmpty) { // Is this a void element? var tagName = tag.Item1.Content.Trim(); if (VoidElements.Contains(tagName)) { CompleteTagBlockWithSpan(tagBlockWrapper, AcceptedCharacters.None, SpanKind.Markup); // Technically, void elements like "meta" are not allowed to have end tags. Just in case they do, // we need to look ahead at the next set of tokens. If we see "<", "/", tag name, accept it and the ">" following it // Place a bookmark var bookmark = CurrentLocation.AbsoluteIndex; // Skip whitespace IEnumerable <HtmlSymbol> whiteSpace = ReadWhile(IsSpacingToken(includeNewLines: true)); // Open Angle if (At(HtmlSymbolType.OpenAngle) && NextIs(HtmlSymbolType.ForwardSlash)) { var openAngle = CurrentSymbol; NextToken(); Assert(HtmlSymbolType.ForwardSlash); var solidus = CurrentSymbol; NextToken(); if (At(HtmlSymbolType.Text) && string.Equals(CurrentSymbol.Content, tagName, StringComparison.OrdinalIgnoreCase)) { // Accept up to here Accept(whiteSpace); Output(SpanKind.Markup); // Output the whitespace using (Context.StartBlock(BlockType.Tag)) { Accept(openAngle); Accept(solidus); AcceptAndMoveNext(); // Accept to '>', '<' or EOF AcceptUntil(HtmlSymbolType.CloseAngle, HtmlSymbolType.OpenAngle); // Accept the '>' if we saw it. And if we do see it, we're complete var complete = Optional(HtmlSymbolType.CloseAngle); if (complete) { Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; } // Output the closing void element Output(SpanKind.Markup); return(complete); } } } // Go back to the bookmark and just finish this tag at the close angle Context.Source.Position = bookmark; NextToken(); } else if (string.Equals(tagName, "script", StringComparison.OrdinalIgnoreCase)) { CompleteTagBlockWithSpan(tagBlockWrapper, AcceptedCharacters.None, SpanKind.Markup); SkipToEndScriptAndParseCode(endTagAcceptedCharacters: AcceptedCharacters.None); } else { // Push the tag on to the stack tags.Push(tag); } } } return(seenClose); }