From ff854e3e1552192a16f93e79d159449cdab646d5 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Sat, 11 Jan 2014 20:12:30 -0800 Subject: [PATCH] Initial commit. --- .gitattributes | 50 + .gitignore | 34 + Razor.sln | 22 + .../CSharpRazorCodeLanguage.cs | 49 + .../Common/CommonResources.Designer.cs | 135 +++ .../Common/CommonResources.resx | 144 +++ .../Common/HashCodeCombiner.cs | 53 + .../DocumentParseCompleteEventArgs.cs | 28 + .../Editor/AutoCompleteEditHandler.cs | 64 ++ .../Editor/BackgroundParser.cs | 475 ++++++++ .../Editor/EditResult.cs | 18 + .../Editor/EditorHints.cs | 30 + .../Editor/ImplicitExpressionEditHandler.cs | 237 ++++ .../Editor/RazorEditorTrace.cs | 53 + .../Editor/SingleLineMarkupEditHandler.cs | 25 + .../Editor/SpanEditHandler.cs | 184 +++ .../Generator/AddImportCodeGenerator.cs | 69 ++ .../Generator/AttributeBlockCodeGenerator.cs | 91 ++ .../Generator/BaseCodeWriter.cs | 77 ++ .../Generator/BlockCodeGenerator.cs | 47 + .../Generator/CSharpCodeWriter.cs | 250 ++++ .../Generator/CSharpRazorCodeGenerator.cs | 31 + .../CodeGenerationCompleteEventArgs.cs | 30 + .../Generator/CodeGeneratorContext.cs | 334 ++++++ .../Generator/CodeGeneratorPaddingHelper.cs | 204 ++++ .../Generator/CodeWriter.cs | 210 ++++ .../Generator/CodeWriterExtensions.cs | 19 + .../DynamicAttributeBlockCodeGenerator.cs | 142 +++ .../Generator/ExpressionCodeGenerator.cs | 113 ++ .../Generator/ExpressionRenderingMode.cs | 28 + .../Generator/GeneratedClassContext.cs | 177 +++ .../Generator/GeneratedCodeMapping.cs | 102 ++ .../Generator/HelperCodeGenerator.cs | 116 ++ .../Generator/HybridCodeGenerator.cs | 21 + .../Generator/IBlockCodeGenerator.cs | 12 + .../Generator/ISpanCodeGenerator.cs | 11 + .../LiteralAttributeCodeGenerator.cs | 114 ++ .../Generator/MarkupCodeGenerator.cs | 64 ++ .../Generator/RazorCodeGenerator.cs | 104 ++ .../Generator/RazorCommentCodeGenerator.cs | 21 + .../RazorDirectiveAttributeCodeGenerator.cs | 55 + .../Generator/ResolveUrlCodeGenerator.cs | 93 ++ .../Generator/SectionCodeGenerator.cs | 62 + .../Generator/SetBaseTypeCodeGenerator.cs | 65 ++ .../Generator/SetLayoutCodeGenerator.cs | 45 + .../Generator/SetVBOptionCodeGenerator.cs | 43 + .../Generator/SpanCodeGenerator.cs | 39 + .../Generator/StatementCodeGenerator.cs | 43 + .../Generator/TemplateBlockCodeGenerator.cs | 44 + .../Generator/TypeMemberCodeGenerator.cs | 46 + .../Generator/VBCodeWriter.cs | 201 ++++ .../Generator/VBRazorCodeGenerator.cs | 18 + .../GeneratorResults.cs | 55 + .../GlobalSuppressions.cs | 22 + .../Microsoft.AspNet.Razor.csproj | 216 ++++ .../Parser/BalancingModes.cs | 15 + .../Parser/CSharpCodeParser.Directives.cs | 505 +++++++++ .../Parser/CSharpCodeParser.Statements.cs | 683 +++++++++++ .../Parser/CSharpCodeParser.cs | 578 ++++++++++ .../Parser/CSharpLanguageCharacteristics.cs | 190 ++++ .../Parser/CallbackVisitor.cs | 96 ++ .../Parser/ConditionalAttributeCollapser.cs | 55 + .../Parser/HtmlLanguageCharacteristics.cs | 132 +++ .../Parser/HtmlMarkupParser.Block.cs | 832 ++++++++++++++ .../Parser/HtmlMarkupParser.Document.cs | 72 ++ .../Parser/HtmlMarkupParser.Section.cs | 197 ++++ .../Parser/HtmlMarkupParser.cs | 187 +++ .../Parser/ISyntaxTreeRewriter.cs | 11 + .../Parser/LanguageCharacteristics.cs | 113 ++ .../Parser/MarkupCollapser.cs | 38 + .../Parser/MarkupRewriter.cs | 105 ++ .../Parser/ParserBase.cs | 51 + .../Parser/ParserContext.cs | 305 +++++ .../Parser/ParserHelpers.cs | 146 +++ .../Parser/ParserVisitor.cs | 56 + .../Parser/ParserVisitorExtensions.cs | 29 + .../Parser/RazorParser.cs | 158 +++ .../Parser/SyntaxConstants.cs | 52 + .../Parser/SyntaxTree/AcceptedCharacters.cs | 24 + .../Parser/SyntaxTree/Block.cs | 201 ++++ .../Parser/SyntaxTree/BlockBuilder.cs | 44 + .../Parser/SyntaxTree/BlockType.cs | 22 + .../Parser/SyntaxTree/EquivalenceComparer.cs | 19 + .../Parser/SyntaxTree/RazorError.cs | 59 + .../Parser/SyntaxTree/Span.cs | 153 +++ .../Parser/SyntaxTree/SpanBuilder.cs | 84 ++ .../Parser/SyntaxTree/SpanKind.cs | 13 + .../Parser/SyntaxTree/SyntaxTreeNode.cs | 41 + .../Parser/TextReaderExtensions.cs | 109 ++ .../Parser/TokenizerBackedParser.Helpers.cs | 550 +++++++++ .../Parser/TokenizerBackedParser.cs | 88 ++ .../Parser/VBCodeParser.Directives.cs | 411 +++++++ .../Parser/VBCodeParser.Statements.cs | 315 ++++++ .../Parser/VBCodeParser.cs | 601 ++++++++++ .../Parser/VBLanguageCharacteristics.cs | 91 ++ .../Parser/WhitespaceRewriter.cs | 77 ++ src/Microsoft.AspNet.Razor/ParserResults.cs | 40 + .../PartialParseResult.cs | 60 + .../Properties/AssemblyInfo.cs | 39 + .../RazorCodeLanguage.cs | 61 + .../RazorDebugHelpers.cs | 200 ++++ .../RazorDirectiveAttribute.cs | 50 + .../RazorEditorParser.cs | 287 +++++ src/Microsoft.AspNet.Razor/RazorEngineHost.cs | 240 ++++ .../RazorTemplateEngine.cs | 200 ++++ .../Resources/RazorResources.Designer.cs | 1003 +++++++++++++++++ .../Resources/RazorResources.resx | 457 ++++++++ src/Microsoft.AspNet.Razor/StateMachine.cs | 105 ++ .../Text/BufferingTextReader.cs | 201 ++++ .../Text/ITextBuffer.cs | 18 + .../Text/LineTrackingStringBuffer.cs | 162 +++ .../Text/LocationTagged.cs | 93 ++ .../Text/LookaheadTextReader.cs | 14 + .../Text/LookaheadToken.cs | 35 + .../Text/SeekableTextReader.cs | 99 ++ .../Text/SourceLocation.cs | 142 +++ .../Text/SourceLocationTracker.cs | 95 ++ .../Text/TextBufferReader.cs | 106 ++ src/Microsoft.AspNet.Razor/Text/TextChange.cs | 211 ++++ .../Text/TextChangeType.cs | 10 + .../Text/TextDocumentReader.cs | 42 + .../Text/TextExtensions.cs | 46 + .../Tokenizer/CSharpHelpers.cs | 44 + .../Tokenizer/CSharpKeywordDetector.cs | 102 ++ .../Tokenizer/CSharpTokenizer.cs | 433 +++++++ .../Tokenizer/HtmlTokenizer.cs | 199 ++++ .../Tokenizer/ITokenizer.cs | 11 + .../Tokenizer/Symbols/CSharpKeyword.cs | 85 ++ .../Tokenizer/Symbols/CSharpSymbol.cs | 47 + .../Tokenizer/Symbols/CSharpSymbolType.cs | 74 ++ .../Tokenizer/Symbols/HtmlSymbol.cs | 33 + .../Tokenizer/Symbols/HtmlSymbolType.cs | 28 + .../Tokenizer/Symbols/ISymbol.cs | 15 + .../Tokenizer/Symbols/KnownSymbolType.cs | 17 + .../Tokenizer/Symbols/SymbolBase.cs | 72 ++ .../Tokenizer/Symbols/SymbolExtensions.cs | 42 + .../Symbols/SymbolTypeSuppressions.cs | 26 + .../Tokenizer/Symbols/VBKeyword.cs | 168 +++ .../Tokenizer/Symbols/VBSymbol.cs | 114 ++ .../Tokenizer/Symbols/VBSymbolType.cs | 48 + .../Tokenizer/Tokenizer.cs | 355 ++++++ .../Tokenizer/TokenizerView.cs | 57 + .../Tokenizer/VBHelpers.cs | 22 + .../Tokenizer/VBKeywordDetector.cs | 177 +++ .../Tokenizer/VBTokenizer.cs | 384 +++++++ .../Tokenizer/XmlHelpers.cs | 50 + src/Microsoft.AspNet.Razor/Utils/CharUtils.cs | 21 + .../Utils/DisposableAction.cs | 34 + src/Microsoft.AspNet.Razor/Utils/EnumUtil.cs | 23 + .../Utils/EnumeratorExtensions.cs | 15 + .../VBRazorCodeLanguage.cs | 49 + 151 files changed, 19304 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 Razor.sln create mode 100644 src/Microsoft.AspNet.Razor/CSharpRazorCodeLanguage.cs create mode 100644 src/Microsoft.AspNet.Razor/Common/CommonResources.Designer.cs create mode 100644 src/Microsoft.AspNet.Razor/Common/CommonResources.resx create mode 100644 src/Microsoft.AspNet.Razor/Common/HashCodeCombiner.cs create mode 100644 src/Microsoft.AspNet.Razor/DocumentParseCompleteEventArgs.cs create mode 100644 src/Microsoft.AspNet.Razor/Editor/AutoCompleteEditHandler.cs create mode 100644 src/Microsoft.AspNet.Razor/Editor/BackgroundParser.cs create mode 100644 src/Microsoft.AspNet.Razor/Editor/EditResult.cs create mode 100644 src/Microsoft.AspNet.Razor/Editor/EditorHints.cs create mode 100644 src/Microsoft.AspNet.Razor/Editor/ImplicitExpressionEditHandler.cs create mode 100644 src/Microsoft.AspNet.Razor/Editor/RazorEditorTrace.cs create mode 100644 src/Microsoft.AspNet.Razor/Editor/SingleLineMarkupEditHandler.cs create mode 100644 src/Microsoft.AspNet.Razor/Editor/SpanEditHandler.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/AddImportCodeGenerator.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/AttributeBlockCodeGenerator.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/BaseCodeWriter.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/BlockCodeGenerator.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/CSharpCodeWriter.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/CSharpRazorCodeGenerator.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/CodeGenerationCompleteEventArgs.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/CodeGeneratorContext.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/CodeGeneratorPaddingHelper.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/CodeWriter.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/CodeWriterExtensions.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/DynamicAttributeBlockCodeGenerator.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/ExpressionCodeGenerator.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/ExpressionRenderingMode.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/GeneratedClassContext.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/GeneratedCodeMapping.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/HelperCodeGenerator.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/HybridCodeGenerator.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/IBlockCodeGenerator.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/ISpanCodeGenerator.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/LiteralAttributeCodeGenerator.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/MarkupCodeGenerator.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/RazorCodeGenerator.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/RazorCommentCodeGenerator.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/RazorDirectiveAttributeCodeGenerator.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/ResolveUrlCodeGenerator.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/SectionCodeGenerator.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/SetBaseTypeCodeGenerator.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/SetLayoutCodeGenerator.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/SetVBOptionCodeGenerator.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/SpanCodeGenerator.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/StatementCodeGenerator.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/TemplateBlockCodeGenerator.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/TypeMemberCodeGenerator.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/VBCodeWriter.cs create mode 100644 src/Microsoft.AspNet.Razor/Generator/VBRazorCodeGenerator.cs create mode 100644 src/Microsoft.AspNet.Razor/GeneratorResults.cs create mode 100644 src/Microsoft.AspNet.Razor/GlobalSuppressions.cs create mode 100644 src/Microsoft.AspNet.Razor/Microsoft.AspNet.Razor.csproj create mode 100644 src/Microsoft.AspNet.Razor/Parser/BalancingModes.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.Directives.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.Statements.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/CSharpLanguageCharacteristics.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/CallbackVisitor.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/ConditionalAttributeCollapser.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/HtmlLanguageCharacteristics.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/HtmlMarkupParser.Block.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/HtmlMarkupParser.Document.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/HtmlMarkupParser.Section.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/HtmlMarkupParser.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/ISyntaxTreeRewriter.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/LanguageCharacteristics.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/MarkupCollapser.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/MarkupRewriter.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/ParserBase.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/ParserContext.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/ParserHelpers.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/ParserVisitor.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/ParserVisitorExtensions.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/RazorParser.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/SyntaxConstants.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/SyntaxTree/AcceptedCharacters.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/SyntaxTree/Block.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/SyntaxTree/BlockBuilder.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/SyntaxTree/BlockType.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/SyntaxTree/EquivalenceComparer.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/SyntaxTree/RazorError.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/SyntaxTree/Span.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/SyntaxTree/SpanBuilder.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/SyntaxTree/SpanKind.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/SyntaxTree/SyntaxTreeNode.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/TextReaderExtensions.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/TokenizerBackedParser.Helpers.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/TokenizerBackedParser.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/VBCodeParser.Directives.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/VBCodeParser.Statements.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/VBCodeParser.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/VBLanguageCharacteristics.cs create mode 100644 src/Microsoft.AspNet.Razor/Parser/WhitespaceRewriter.cs create mode 100644 src/Microsoft.AspNet.Razor/ParserResults.cs create mode 100644 src/Microsoft.AspNet.Razor/PartialParseResult.cs create mode 100644 src/Microsoft.AspNet.Razor/Properties/AssemblyInfo.cs create mode 100644 src/Microsoft.AspNet.Razor/RazorCodeLanguage.cs create mode 100644 src/Microsoft.AspNet.Razor/RazorDebugHelpers.cs create mode 100644 src/Microsoft.AspNet.Razor/RazorDirectiveAttribute.cs create mode 100644 src/Microsoft.AspNet.Razor/RazorEditorParser.cs create mode 100644 src/Microsoft.AspNet.Razor/RazorEngineHost.cs create mode 100644 src/Microsoft.AspNet.Razor/RazorTemplateEngine.cs create mode 100644 src/Microsoft.AspNet.Razor/Resources/RazorResources.Designer.cs create mode 100644 src/Microsoft.AspNet.Razor/Resources/RazorResources.resx create mode 100644 src/Microsoft.AspNet.Razor/StateMachine.cs create mode 100644 src/Microsoft.AspNet.Razor/Text/BufferingTextReader.cs create mode 100644 src/Microsoft.AspNet.Razor/Text/ITextBuffer.cs create mode 100644 src/Microsoft.AspNet.Razor/Text/LineTrackingStringBuffer.cs create mode 100644 src/Microsoft.AspNet.Razor/Text/LocationTagged.cs create mode 100644 src/Microsoft.AspNet.Razor/Text/LookaheadTextReader.cs create mode 100644 src/Microsoft.AspNet.Razor/Text/LookaheadToken.cs create mode 100644 src/Microsoft.AspNet.Razor/Text/SeekableTextReader.cs create mode 100644 src/Microsoft.AspNet.Razor/Text/SourceLocation.cs create mode 100644 src/Microsoft.AspNet.Razor/Text/SourceLocationTracker.cs create mode 100644 src/Microsoft.AspNet.Razor/Text/TextBufferReader.cs create mode 100644 src/Microsoft.AspNet.Razor/Text/TextChange.cs create mode 100644 src/Microsoft.AspNet.Razor/Text/TextChangeType.cs create mode 100644 src/Microsoft.AspNet.Razor/Text/TextDocumentReader.cs create mode 100644 src/Microsoft.AspNet.Razor/Text/TextExtensions.cs create mode 100644 src/Microsoft.AspNet.Razor/Tokenizer/CSharpHelpers.cs create mode 100644 src/Microsoft.AspNet.Razor/Tokenizer/CSharpKeywordDetector.cs create mode 100644 src/Microsoft.AspNet.Razor/Tokenizer/CSharpTokenizer.cs create mode 100644 src/Microsoft.AspNet.Razor/Tokenizer/HtmlTokenizer.cs create mode 100644 src/Microsoft.AspNet.Razor/Tokenizer/ITokenizer.cs create mode 100644 src/Microsoft.AspNet.Razor/Tokenizer/Symbols/CSharpKeyword.cs create mode 100644 src/Microsoft.AspNet.Razor/Tokenizer/Symbols/CSharpSymbol.cs create mode 100644 src/Microsoft.AspNet.Razor/Tokenizer/Symbols/CSharpSymbolType.cs create mode 100644 src/Microsoft.AspNet.Razor/Tokenizer/Symbols/HtmlSymbol.cs create mode 100644 src/Microsoft.AspNet.Razor/Tokenizer/Symbols/HtmlSymbolType.cs create mode 100644 src/Microsoft.AspNet.Razor/Tokenizer/Symbols/ISymbol.cs create mode 100644 src/Microsoft.AspNet.Razor/Tokenizer/Symbols/KnownSymbolType.cs create mode 100644 src/Microsoft.AspNet.Razor/Tokenizer/Symbols/SymbolBase.cs create mode 100644 src/Microsoft.AspNet.Razor/Tokenizer/Symbols/SymbolExtensions.cs create mode 100644 src/Microsoft.AspNet.Razor/Tokenizer/Symbols/SymbolTypeSuppressions.cs create mode 100644 src/Microsoft.AspNet.Razor/Tokenizer/Symbols/VBKeyword.cs create mode 100644 src/Microsoft.AspNet.Razor/Tokenizer/Symbols/VBSymbol.cs create mode 100644 src/Microsoft.AspNet.Razor/Tokenizer/Symbols/VBSymbolType.cs create mode 100644 src/Microsoft.AspNet.Razor/Tokenizer/Tokenizer.cs create mode 100644 src/Microsoft.AspNet.Razor/Tokenizer/TokenizerView.cs create mode 100644 src/Microsoft.AspNet.Razor/Tokenizer/VBHelpers.cs create mode 100644 src/Microsoft.AspNet.Razor/Tokenizer/VBKeywordDetector.cs create mode 100644 src/Microsoft.AspNet.Razor/Tokenizer/VBTokenizer.cs create mode 100644 src/Microsoft.AspNet.Razor/Tokenizer/XmlHelpers.cs create mode 100644 src/Microsoft.AspNet.Razor/Utils/CharUtils.cs create mode 100644 src/Microsoft.AspNet.Razor/Utils/DisposableAction.cs create mode 100644 src/Microsoft.AspNet.Razor/Utils/EnumUtil.cs create mode 100644 src/Microsoft.AspNet.Razor/Utils/EnumeratorExtensions.cs create mode 100644 src/Microsoft.AspNet.Razor/VBRazorCodeLanguage.cs diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..bdaa5ba982 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,50 @@ +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain + +*.jpg binary +*.png binary +*.gif binary + +*.cs text=auto diff=csharp +*.vb text=auto +*.resx text=auto +*.c text=auto +*.cpp text=auto +*.cxx text=auto +*.h text=auto +*.hxx text=auto +*.py text=auto +*.rb text=auto +*.java text=auto +*.html text=auto +*.htm text=auto +*.css text=auto +*.scss text=auto +*.sass text=auto +*.less text=auto +*.js text=auto +*.lisp text=auto +*.clj text=auto +*.sql text=auto +*.php text=auto +*.lua text=auto +*.m text=auto +*.asm text=auto +*.erl text=auto +*.fs text=auto +*.fsx text=auto +*.hs text=auto + +*.csproj text=auto +*.vbproj text=auto +*.fsproj text=auto +*.dbproj text=auto +*.sln text=auto eol=crlf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..7473e2211c --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +[Oo]bj/ +[Bb]in/ +*.xap +*.user +/TestResults +*.vspscc +*.vssscc +deploy +deploy/* +*.suo +*.cache +*.docstates +_ReSharper.* +*.csproj.user +*[Rr]e[Ss]harper.user +_ReSharper.*/ +packages/* +artifacts/* +msbuild.log +PublishProfiles/ +*.psess +*.vsp +*.pidb +*.userprefs +*DS_Store +*.ncrunchsolution +*.log +*.vspx +*.log.txt +/tests/Microsoft.AspNet.SignalR.FunctionalTests/artifacts/ +/samples/Microsoft.AspNet.SignalR.Client.WindowsStoreJavaScript.Samples/bld/ +jquery.signalR.js +jquery.signalR.min.js +xamarin/SignalRPackage/component/lib/mobile/ diff --git a/Razor.sln b/Razor.sln new file mode 100644 index 0000000000..08285df812 --- /dev/null +++ b/Razor.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2013 +VisualStudioVersion = 12.0.21005.1 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.Razor", "src\Microsoft.AspNet.Razor\Microsoft.AspNet.Razor.csproj", "{E75D8296-3BA6-4E67-AFEB-90FF77460B15}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E75D8296-3BA6-4E67-AFEB-90FF77460B15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E75D8296-3BA6-4E67-AFEB-90FF77460B15}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E75D8296-3BA6-4E67-AFEB-90FF77460B15}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E75D8296-3BA6-4E67-AFEB-90FF77460B15}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/src/Microsoft.AspNet.Razor/CSharpRazorCodeLanguage.cs b/src/Microsoft.AspNet.Razor/CSharpRazorCodeLanguage.cs new file mode 100644 index 0000000000..852c849a17 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/CSharpRazorCodeLanguage.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using Microsoft.AspNet.Razor.Generator; +using Microsoft.AspNet.Razor.Parser; +using Microsoft.CSharp; +using System; + +namespace Microsoft.AspNet.Razor +{ + /// + /// Defines the C# Code Language for Razor + /// + public class CSharpRazorCodeLanguage : RazorCodeLanguage + { + private const string CSharpLanguageName = "csharp"; + + /// + /// Returns the name of the language: "csharp" + /// + public override string LanguageName + { + get { return CSharpLanguageName; } + } + + /// + /// Returns the type of the CodeDOM provider for this language + /// + public override Type CodeDomProviderType + { + get { return typeof(CSharpCodeProvider); } + } + + /// + /// Constructs a new instance of the code parser for this language + /// + public override ParserBase CreateCodeParser() + { + return new CSharpCodeParser(); + } + + /// + /// Constructs a new instance of the code generator for this language with the specified settings + /// + public override RazorCodeGenerator CreateCodeGenerator(string className, string rootNamespaceName, string sourceFileName, RazorEngineHost host) + { + return new CSharpRazorCodeGenerator(className, rootNamespaceName, sourceFileName, host); + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Common/CommonResources.Designer.cs b/src/Microsoft.AspNet.Razor/Common/CommonResources.Designer.cs new file mode 100644 index 0000000000..38d34b2300 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Common/CommonResources.Designer.cs @@ -0,0 +1,135 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.34003 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.Internal.Web.Utils { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class CommonResources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal CommonResources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNet.Razor.Common.CommonResources", typeof(CommonResources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Value cannot be null or an empty string.. + /// + internal static string Argument_Cannot_Be_Null_Or_Empty { + get { + return ResourceManager.GetString("Argument_Cannot_Be_Null_Or_Empty", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Value must be between {0} and {1}.. + /// + internal static string Argument_Must_Be_Between { + get { + return ResourceManager.GetString("Argument_Must_Be_Between", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Value must be a value from the "{0}" enumeration.. + /// + internal static string Argument_Must_Be_Enum_Member { + get { + return ResourceManager.GetString("Argument_Must_Be_Enum_Member", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Value must be greater than {0}.. + /// + internal static string Argument_Must_Be_GreaterThan { + get { + return ResourceManager.GetString("Argument_Must_Be_GreaterThan", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Value must be greater than or equal to {0}.. + /// + internal static string Argument_Must_Be_GreaterThanOrEqualTo { + get { + return ResourceManager.GetString("Argument_Must_Be_GreaterThanOrEqualTo", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Value must be less than {0}.. + /// + internal static string Argument_Must_Be_LessThan { + get { + return ResourceManager.GetString("Argument_Must_Be_LessThan", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Value must be less than or equal to {0}.. + /// + internal static string Argument_Must_Be_LessThanOrEqualTo { + get { + return ResourceManager.GetString("Argument_Must_Be_LessThanOrEqualTo", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Value cannot be an empty string. It must either be null or a non-empty string.. + /// + internal static string Argument_Must_Be_Null_Or_Non_Empty { + get { + return ResourceManager.GetString("Argument_Must_Be_Null_Or_Non_Empty", resourceCulture); + } + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Common/CommonResources.resx b/src/Microsoft.AspNet.Razor/Common/CommonResources.resx new file mode 100644 index 0000000000..2490419364 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Common/CommonResources.resx @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Value cannot be null or an empty string. + + + Value must be between {0} and {1}. + + + Value must be a value from the "{0}" enumeration. + + + Value must be greater than {0}. + + + Value must be greater than or equal to {0}. + + + Value must be less than {0}. + + + Value must be less than or equal to {0}. + + + Value cannot be an empty string. It must either be null or a non-empty string. + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor/Common/HashCodeCombiner.cs b/src/Microsoft.AspNet.Razor/Common/HashCodeCombiner.cs new file mode 100644 index 0000000000..ba08cb2024 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Common/HashCodeCombiner.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections; + +namespace Microsoft.Internal.Web.Utils +{ + internal class HashCodeCombiner + { + private long _combinedHash64 = 0x1505L; + + public int CombinedHash + { + get { return _combinedHash64.GetHashCode(); } + } + + public HashCodeCombiner Add(IEnumerable e) + { + if (e == null) + { + Add(0); + } + else + { + int count = 0; + foreach (object o in e) + { + Add(o); + count++; + } + Add(count); + } + return this; + } + + public HashCodeCombiner Add(int i) + { + _combinedHash64 = ((_combinedHash64 << 5) + _combinedHash64) ^ i; + return this; + } + + public HashCodeCombiner Add(object o) + { + int hashCode = (o != null) ? o.GetHashCode() : 0; + Add(hashCode); + return this; + } + + public static HashCodeCombiner Start() + { + return new HashCodeCombiner(); + } + } +} diff --git a/src/Microsoft.AspNet.Razor/DocumentParseCompleteEventArgs.cs b/src/Microsoft.AspNet.Razor/DocumentParseCompleteEventArgs.cs new file mode 100644 index 0000000000..53153549cd --- /dev/null +++ b/src/Microsoft.AspNet.Razor/DocumentParseCompleteEventArgs.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Razor.Text; + +namespace Microsoft.AspNet.Razor +{ + /// + /// Arguments for the DocumentParseComplete event in RazorEditorParser + /// + public class DocumentParseCompleteEventArgs : EventArgs + { + /// + /// Indicates if the tree structure has actually changed since the previous reparse. + /// + public bool TreeStructureChanged { get; set; } + + /// + /// The results of the code generation and parsing + /// + public GeneratorResults GeneratorResults { get; set; } + + /// + /// The TextChange which triggered the reparse + /// + public TextChange SourceChange { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Razor/Editor/AutoCompleteEditHandler.cs b/src/Microsoft.AspNet.Razor/Editor/AutoCompleteEditHandler.cs new file mode 100644 index 0000000000..4032779e4a --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Editor/AutoCompleteEditHandler.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNet.Razor.Editor; +using Microsoft.AspNet.Razor.Text; +using Microsoft.AspNet.Razor.Tokenizer.Symbols; +using Microsoft.Internal.Web.Utils; +using System; + +namespace Microsoft.AspNet.Razor.Parser.SyntaxTree +{ + public class AutoCompleteEditHandler : SpanEditHandler + { + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Func is the recommended delegate type and requires this level of nesting.")] + public AutoCompleteEditHandler(Func> tokenizer) + : base(tokenizer) + { + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Func is the recommended delegate type and requires this level of nesting.")] + public AutoCompleteEditHandler(Func> tokenizer, AcceptedCharacters accepted) + : base(tokenizer, accepted) + { + } + + public bool AutoCompleteAtEndOfSpan { get; set; } + public string AutoCompleteString { get; set; } + + protected override PartialParseResult CanAcceptChange(Span target, TextChange normalizedChange) + { + if (((AutoCompleteAtEndOfSpan && IsAtEndOfSpan(target, normalizedChange)) || IsAtEndOfFirstLine(target, normalizedChange)) && + normalizedChange.IsInsert && + ParserHelpers.IsNewLine(normalizedChange.NewText) && + AutoCompleteString != null) + { + return PartialParseResult.Rejected | PartialParseResult.AutoCompleteBlock; + } + return PartialParseResult.Rejected; + } + + public override string ToString() + { + return base.ToString() + ",AutoComplete:[" + (AutoCompleteString ?? "") + "]" + (AutoCompleteAtEndOfSpan ? ";AtEnd" : ";AtEOL"); + } + + public override bool Equals(object obj) + { + AutoCompleteEditHandler other = obj as AutoCompleteEditHandler; + return base.Equals(obj) && + other != null && + String.Equals(other.AutoCompleteString, AutoCompleteString, StringComparison.Ordinal) && + AutoCompleteAtEndOfSpan == other.AutoCompleteAtEndOfSpan; + } + + public override int GetHashCode() + { + return HashCodeCombiner.Start() + .Add(base.GetHashCode()) + .Add(AutoCompleteString) + .CombinedHash; + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Editor/BackgroundParser.cs b/src/Microsoft.AspNet.Razor/Editor/BackgroundParser.cs new file mode 100644 index 0000000000..4dd588faec --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Editor/BackgroundParser.cs @@ -0,0 +1,475 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Resources; +using Microsoft.AspNet.Razor.Text; +using Microsoft.AspNet.Razor.Utils; + +namespace Microsoft.AspNet.Razor.Editor +{ + internal class BackgroundParser : IDisposable + { + private MainThreadState _main; + private BackgroundThread _bg; + + public BackgroundParser(RazorEngineHost host, string fileName) + { + _main = new MainThreadState(fileName); + _bg = new BackgroundThread(_main, host, fileName); + + _main.ResultsReady += (sender, args) => OnResultsReady(args); + } + + /// + /// Fired on the main thread. + /// + public event EventHandler ResultsReady; + + public bool IsIdle + { + get { return _main.IsIdle; } + } + + public void Start() + { + _bg.Start(); + } + + public void Cancel() + { + _main.Cancel(); + } + + public void QueueChange(TextChange change) + { + _main.QueueChange(change); + } + + [SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "_main", Justification = "MainThreadState is disposed when the background thread shuts down")] + public void Dispose() + { + _main.Cancel(); + } + + public IDisposable SynchronizeMainThreadState() + { + return _main.Lock(); + } + + protected virtual void OnResultsReady(DocumentParseCompleteEventArgs args) + { + var handler = ResultsReady; + if (handler != null) + { + handler(this, args); + } + } + + internal static bool TreesAreDifferent(Block leftTree, Block rightTree, IEnumerable changes) + { + return TreesAreDifferent(leftTree, rightTree, changes, CancellationToken.None); + } + + internal static bool TreesAreDifferent(Block leftTree, Block rightTree, IEnumerable changes, CancellationToken cancelToken) + { + // Apply all the pending changes to the original tree + // PERF: If this becomes a bottleneck, we can probably do it the other way around, + // i.e. visit the tree and find applicable changes for each node. + foreach (TextChange change in changes) + { + cancelToken.ThrowIfCancellationRequested(); + Span changeOwner = leftTree.LocateOwner(change); + + // Apply the change to the tree + if (changeOwner == null) + { + return true; + } + EditResult result = changeOwner.EditHandler.ApplyChange(changeOwner, change, force: true); + changeOwner.ReplaceWith(result.EditedSpan); + } + + // Now compare the trees + bool treesDifferent = !leftTree.EquivalentTo(rightTree); + return treesDifferent; + } + + private abstract class ThreadStateBase + { +#if DEBUG + private int _id = -1; +#endif + protected ThreadStateBase() + { + } + + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This method is only empty in Release builds. In Debug builds it contains references to instance variables")] + [Conditional("DEBUG")] + protected void SetThreadId(int id) + { +#if DEBUG + _id = id; +#endif + } + + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This method is only empty in Release builds. In Debug builds it contains references to instance variables")] + [Conditional("DEBUG")] + protected void EnsureOnThread() + { +#if DEBUG + Debug.Assert(_id != -1, "SetThreadId was never called!"); + Debug.Assert(Thread.CurrentThread.ManagedThreadId == _id, "Called from an unexpected thread!"); +#endif + } + + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This method is only empty in Release builds. In Debug builds it contains references to instance variables")] + [Conditional("DEBUG")] + protected void EnsureNotOnThread() + { +#if DEBUG + Debug.Assert(_id != -1, "SetThreadId was never called!"); + Debug.Assert(Thread.CurrentThread.ManagedThreadId != _id, "Called from an unexpected thread!"); +#endif + } + } + + private class MainThreadState : ThreadStateBase, IDisposable + { + private CancellationTokenSource _cancelSource = new CancellationTokenSource(); + private ManualResetEventSlim _hasParcel = new ManualResetEventSlim(false); + private CancellationTokenSource _currentParcelCancelSource; + + [SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields", Justification = "Field is used in debug code and may be used later")] + private string _fileName; + private object _stateLock = new object(); + private IList _changes = new List(); + + public MainThreadState(string fileName) + { + _fileName = fileName; + + SetThreadId(Thread.CurrentThread.ManagedThreadId); + } + + public event EventHandler ResultsReady; + + public CancellationToken CancelToken + { + get { return _cancelSource.Token; } + } + + public bool IsIdle + { + get + { + lock (_stateLock) + { + return _currentParcelCancelSource == null; + } + } + } + + public void Cancel() + { + EnsureOnThread(); + _cancelSource.Cancel(); + } + + public IDisposable Lock() + { + Monitor.Enter(_stateLock); + return new DisposableAction(() => Monitor.Exit(_stateLock)); + } + + public void QueueChange(TextChange change) + { + RazorEditorTrace.TraceLine(RazorResources.Trace_QueuingParse, Path.GetFileName(_fileName), change); + EnsureOnThread(); + lock (_stateLock) + { + // CurrentParcel token source is not null ==> There's a parse underway + if (_currentParcelCancelSource != null) + { + _currentParcelCancelSource.Cancel(); + } + + _changes.Add(change); + _hasParcel.Set(); + } + } + + public WorkParcel GetParcel() + { + EnsureNotOnThread(); // Only the background thread can get a parcel + _hasParcel.Wait(_cancelSource.Token); + _hasParcel.Reset(); + lock (_stateLock) + { + // Create a cancellation source for this parcel + _currentParcelCancelSource = new CancellationTokenSource(); + + var changes = _changes; + _changes = new List(); + return new WorkParcel(changes, _currentParcelCancelSource.Token); + } + } + + public void ReturnParcel(DocumentParseCompleteEventArgs args) + { + lock (_stateLock) + { + // Clear the current parcel cancellation source + if (_currentParcelCancelSource != null) + { + _currentParcelCancelSource.Dispose(); + _currentParcelCancelSource = null; + } + + // If there are things waiting to be parsed, just don't fire the event because we're already out of date + if (_changes.Any()) + { + return; + } + } + var handler = ResultsReady; + if (handler != null) + { + handler(this, args); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (_currentParcelCancelSource != null) + { + _currentParcelCancelSource.Dispose(); + _currentParcelCancelSource = null; + } + _cancelSource.Dispose(); + _hasParcel.Dispose(); + } + } + } + + private class BackgroundThread : ThreadStateBase + { + private MainThreadState _main; + private Thread _backgroundThread; + private CancellationToken _shutdownToken; + private RazorEngineHost _host; + private string _fileName; + private Block _currentParseTree; + private IList _previouslyDiscarded = new List(); + + public BackgroundThread(MainThreadState main, RazorEngineHost host, string fileName) + { + // Run on MAIN thread! + _main = main; + _backgroundThread = new Thread(WorkerLoop); + _shutdownToken = _main.CancelToken; + _host = host; + _fileName = fileName; + + SetThreadId(_backgroundThread.ManagedThreadId); + } + + // **** ANY THREAD **** + public void Start() + { + _backgroundThread.Start(); + } + + // **** BACKGROUND THREAD **** + private void WorkerLoop() + { + long? elapsedMs = null; + string fileNameOnly = Path.GetFileName(_fileName); +#if EDITOR_TRACING + Stopwatch sw = new Stopwatch(); +#endif + + try + { + RazorEditorTrace.TraceLine(RazorResources.Trace_BackgroundThreadStart, fileNameOnly); + EnsureOnThread(); + while (!_shutdownToken.IsCancellationRequested) + { + // Grab the parcel of work to do + WorkParcel parcel = _main.GetParcel(); + if (parcel.Changes.Any()) + { + RazorEditorTrace.TraceLine(RazorResources.Trace_ChangesArrived, fileNameOnly, parcel.Changes.Count); + try + { + DocumentParseCompleteEventArgs args = null; + using (var linkedCancel = CancellationTokenSource.CreateLinkedTokenSource(_shutdownToken, parcel.CancelToken)) + { + if (parcel != null && !linkedCancel.IsCancellationRequested) + { + // Collect ALL changes +#if EDITOR_TRACING + if (_previouslyDiscarded != null && _previouslyDiscarded.Any()) + { + RazorEditorTrace.TraceLine(RazorResources.Trace_CollectedDiscardedChanges, fileNameOnly, _previouslyDiscarded.Count); + } +#endif + var allChanges = Enumerable.Concat( + _previouslyDiscarded ?? Enumerable.Empty(), parcel.Changes).ToList(); + var finalChange = allChanges.LastOrDefault(); + if (finalChange != null) + { +#if EDITOR_TRACING + sw.Start(); +#endif + GeneratorResults results = ParseChange(finalChange.NewBuffer, linkedCancel.Token); +#if EDITOR_TRACING + sw.Stop(); + elapsedMs = sw.ElapsedMilliseconds; + sw.Reset(); +#endif + RazorEditorTrace.TraceLine( + RazorResources.Trace_ParseComplete, + fileNameOnly, + elapsedMs.HasValue ? elapsedMs.Value.ToString() : "?"); + + if (results != null && !linkedCancel.IsCancellationRequested) + { + // Clear discarded changes list + _previouslyDiscarded = null; + + // Take the current tree and check for differences +#if EDITOR_TRACING + sw.Start(); +#endif + bool treeStructureChanged = _currentParseTree == null || TreesAreDifferent(_currentParseTree, results.Document, allChanges, parcel.CancelToken); +#if EDITOR_TRACING + sw.Stop(); + elapsedMs = sw.ElapsedMilliseconds; + sw.Reset(); +#endif + _currentParseTree = results.Document; + RazorEditorTrace.TraceLine(RazorResources.Trace_TreesCompared, + fileNameOnly, + elapsedMs.HasValue ? elapsedMs.Value.ToString() : "?", + treeStructureChanged); + + // Build Arguments + args = new DocumentParseCompleteEventArgs() + { + GeneratorResults = results, + SourceChange = finalChange, + TreeStructureChanged = treeStructureChanged + }; + } + else + { + // Parse completed but we were cancelled in the mean time. Add these to the discarded changes set + RazorEditorTrace.TraceLine(RazorResources.Trace_ChangesDiscarded, fileNameOnly, allChanges.Count); + _previouslyDiscarded = allChanges; + } + +#if CHECK_TREE + if (args != null) + { + // Rewind the buffer and sanity check the line mappings + finalChange.NewBuffer.Position = 0; + int lineCount = finalChange.NewBuffer.ReadToEnd().Split(new string[] { Environment.NewLine, "\r", "\n" }, StringSplitOptions.None).Count(); + Debug.Assert( + !args.GeneratorResults.DesignTimeLineMappings.Any(pair => pair.Value.StartLine > lineCount), + "Found a design-time line mapping referring to a line outside the source file!"); + Debug.Assert( + !args.GeneratorResults.Document.Flatten().Any(span => span.Start.LineIndex > lineCount), + "Found a span with a line number outside the source file"); + Debug.Assert( + !args.GeneratorResults.Document.Flatten().Any(span => span.Start.AbsoluteIndex > parcel.NewBuffer.Length), + "Found a span with an absolute offset outside the source file"); + } +#endif + } + } + } + if (args != null) + { + _main.ReturnParcel(args); + } + } + catch (OperationCanceledException) + { + } + } + else + { + RazorEditorTrace.TraceLine(RazorResources.Trace_NoChangesArrived, fileNameOnly, parcel.Changes.Count); + Thread.Yield(); + } + } + } + catch (OperationCanceledException) + { + // Do nothing. Just shut down. + } + finally + { + RazorEditorTrace.TraceLine(RazorResources.Trace_BackgroundThreadShutdown, fileNameOnly); + + // Clean up main thread resources + _main.Dispose(); + } + } + + private GeneratorResults ParseChange(ITextBuffer buffer, CancellationToken token) + { + EnsureOnThread(); + + // Create a template engine + RazorTemplateEngine engine = new RazorTemplateEngine(_host); + + // Seek the buffer to the beginning + buffer.Position = 0; + + try + { + return engine.GenerateCode( + input: buffer, + className: null, + rootNamespace: null, + sourceFileName: _fileName, + cancelToken: token); + } + catch (OperationCanceledException) + { + return null; + } + } + } + + private class WorkParcel + { + public WorkParcel(IList changes, CancellationToken cancelToken) + { + Changes = changes; + CancelToken = cancelToken; + } + + public CancellationToken CancelToken { get; private set; } + public IList Changes { get; private set; } + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Editor/EditResult.cs b/src/Microsoft.AspNet.Razor/Editor/EditResult.cs new file mode 100644 index 0000000000..c731546936 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Editor/EditResult.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Editor +{ + public class EditResult + { + public EditResult(PartialParseResult result, SpanBuilder editedSpan) + { + Result = result; + EditedSpan = editedSpan; + } + + public PartialParseResult Result { get; set; } + public SpanBuilder EditedSpan { get; set; } + } +} diff --git a/src/Microsoft.AspNet.Razor/Editor/EditorHints.cs b/src/Microsoft.AspNet.Razor/Editor/EditorHints.cs new file mode 100644 index 0000000000..303b78d656 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Editor/EditorHints.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +namespace Microsoft.AspNet.Razor.Editor +{ + /// + /// Used within . + /// + [Flags] + public enum EditorHints + { + /// + /// The default (Markup or Code) editor behavior for Statement completion should be used. + /// Editors can always use the default behavior, even if the span is labeled with a different CompletionType. + /// + None = 0, // 0000 0000 + + /// + /// Indicates that Virtual Path completion should be used for this span if the editor supports it. + /// Editors need not support this mode of completion, and will use the default () behavior + /// if they do not support it. + /// + VirtualPath = 1, // 0000 0001 + + /// + /// Indicates that this span's content contains the path to the layout page for this document. + /// + LayoutPage = 2, // 0000 0010 + } +} diff --git a/src/Microsoft.AspNet.Razor/Editor/ImplicitExpressionEditHandler.cs b/src/Microsoft.AspNet.Razor/Editor/ImplicitExpressionEditHandler.cs new file mode 100644 index 0000000000..fa0ed1c1a2 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Editor/ImplicitExpressionEditHandler.cs @@ -0,0 +1,237 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Linq; +using Microsoft.AspNet.Razor.Parser; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Text; +using Microsoft.AspNet.Razor.Tokenizer.Symbols; +using Microsoft.Internal.Web.Utils; +using System; + +namespace Microsoft.AspNet.Razor.Editor +{ + public class ImplicitExpressionEditHandler : SpanEditHandler + { + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Func is the recommended delegate type and requires this level of nesting.")] + public ImplicitExpressionEditHandler(Func> tokenizer, ISet keywords, bool acceptTrailingDot) + : base(tokenizer) + { + Initialize(keywords, acceptTrailingDot); + } + + public bool AcceptTrailingDot { get; private set; } + public ISet Keywords { get; private set; } + + public override string ToString() + { + return String.Format(CultureInfo.InvariantCulture, "{0};ImplicitExpression[{1}];K{2}", base.ToString(), AcceptTrailingDot ? "ATD" : "RTD", Keywords.Count); + } + + public override bool Equals(object obj) + { + ImplicitExpressionEditHandler other = obj as ImplicitExpressionEditHandler; + return other != null && + base.Equals(other) && + Keywords.SetEquals(other.Keywords) && + AcceptTrailingDot == other.AcceptTrailingDot; + } + + public override int GetHashCode() + { + return HashCodeCombiner.Start() + .Add(base.GetHashCode()) + .Add(AcceptTrailingDot) + .Add(Keywords) + .CombinedHash; + } + + protected override PartialParseResult CanAcceptChange(Span target, TextChange normalizedChange) + { + if (AcceptedCharacters == AcceptedCharacters.Any) + { + return PartialParseResult.Rejected; + } + + if (IsAcceptableReplace(target, normalizedChange)) + { + return HandleReplacement(target, normalizedChange); + } + int changeRelativePosition = normalizedChange.OldPosition - target.Start.AbsoluteIndex; + + // Get the edit context + char? lastChar = null; + if (changeRelativePosition > 0 && target.Content.Length > 0) + { + lastChar = target.Content[changeRelativePosition - 1]; + } + + // Don't support 0->1 length edits + if (lastChar == null) + { + return PartialParseResult.Rejected; + } + + // Only support insertions at the end of the span + if (IsAcceptableInsertion(target, normalizedChange)) + { + // Handle the insertion + return HandleInsertion(target, lastChar.Value, normalizedChange); + } + + if (IsAcceptableDeletion(target, normalizedChange)) + { + return HandleDeletion(target, lastChar.Value, normalizedChange); + } + + return PartialParseResult.Rejected; + } + + private void Initialize(ISet keywords, bool acceptTrailingDot) + { + Keywords = keywords ?? new HashSet(); + AcceptTrailingDot = acceptTrailingDot; + } + + private static bool IsAcceptableReplace(Span target, TextChange change) + { + return IsEndReplace(target, change) || + (change.IsReplace && RemainingIsWhitespace(target, change)); + } + + private static bool IsAcceptableDeletion(Span target, TextChange change) + { + return IsEndDeletion(target, change) || + (change.IsDelete && RemainingIsWhitespace(target, change)); + } + + private static bool IsAcceptableInsertion(Span target, TextChange change) + { + return IsEndInsertion(target, change) || + (change.IsInsert && RemainingIsWhitespace(target, change)); + } + + private static bool RemainingIsWhitespace(Span target, TextChange change) + { + int offset = (change.OldPosition - target.Start.AbsoluteIndex) + change.OldLength; + return String.IsNullOrWhiteSpace(target.Content.Substring(offset)); + } + + private PartialParseResult HandleReplacement(Span target, TextChange change) + { + // Special Case for IntelliSense commits. + // When IntelliSense commits, we get two changes (for example user typed "Date", then committed "DateTime" by pressing ".") + // 1. Insert "." at the end of this span + // 2. Replace the "Date." at the end of the span with "DateTime." + // We need partial parsing to accept case #2. + string oldText = GetOldText(target, change); + + PartialParseResult result = PartialParseResult.Rejected; + if (EndsWithDot(oldText) && EndsWithDot(change.NewText)) + { + result = PartialParseResult.Accepted; + if (!AcceptTrailingDot) + { + result |= PartialParseResult.Provisional; + } + } + return result; + } + + private PartialParseResult HandleDeletion(Span target, char previousChar, TextChange change) + { + // What's left after deleting? + if (previousChar == '.') + { + return TryAcceptChange(target, change, PartialParseResult.Accepted | PartialParseResult.Provisional); + } + else if (ParserHelpers.IsIdentifierPart(previousChar)) + { + return TryAcceptChange(target, change); + } + else + { + return PartialParseResult.Rejected; + } + } + + private PartialParseResult HandleInsertion(Span target, char previousChar, TextChange change) + { + // What are we inserting after? + if (previousChar == '.') + { + return HandleInsertionAfterDot(target, change); + } + else if (ParserHelpers.IsIdentifierPart(previousChar) || previousChar == ')' || previousChar == ']') + { + return HandleInsertionAfterIdPart(target, change); + } + else + { + return PartialParseResult.Rejected; + } + } + + private PartialParseResult HandleInsertionAfterIdPart(Span target, TextChange change) + { + // If the insertion is a full identifier part, accept it + if (ParserHelpers.IsIdentifier(change.NewText, requireIdentifierStart: false)) + { + return TryAcceptChange(target, change); + } + else if (EndsWithDot(change.NewText)) + { + // Accept it, possibly provisionally + PartialParseResult result = PartialParseResult.Accepted; + if (!AcceptTrailingDot) + { + result |= PartialParseResult.Provisional; + } + return TryAcceptChange(target, change, result); + } + else + { + return PartialParseResult.Rejected; + } + } + + private static bool EndsWithDot(string content) + { + return (content.Length == 1 && content[0] == '.') || + (content[content.Length - 1] == '.' && + content.Take(content.Length - 1).All(ParserHelpers.IsIdentifierPart)); + } + + private PartialParseResult HandleInsertionAfterDot(Span target, TextChange change) + { + // If the insertion is a full identifier, accept it + if (ParserHelpers.IsIdentifier(change.NewText)) + { + return TryAcceptChange(target, change); + } + return PartialParseResult.Rejected; + } + + private PartialParseResult TryAcceptChange(Span target, TextChange change, PartialParseResult acceptResult = PartialParseResult.Accepted) + { + string content = change.ApplyChange(target); + if (StartsWithKeyword(content)) + { + return PartialParseResult.Rejected | PartialParseResult.SpanContextChanged; + } + + return acceptResult; + } + + private bool StartsWithKeyword(string newContent) + { + using (StringReader reader = new StringReader(newContent)) + { + return Keywords.Contains(reader.ReadWhile(ParserHelpers.IsIdentifierPart)); + } + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Editor/RazorEditorTrace.cs b/src/Microsoft.AspNet.Razor/Editor/RazorEditorTrace.cs new file mode 100644 index 0000000000..036e7ee1fd --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Editor/RazorEditorTrace.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.Razor.Resources; +using Microsoft.AspNet.Razor.Text; + +namespace Microsoft.AspNet.Razor.Editor +{ + internal static class RazorEditorTrace + { + private static bool? _enabled; + + private static bool IsEnabled() + { + if (_enabled == null) + { + bool enabled; + if (Boolean.TryParse(Environment.GetEnvironmentVariable("RAZOR_EDITOR_TRACE"), out enabled)) + { + Trace.WriteLine(String.Format( + CultureInfo.CurrentCulture, + RazorResources.Trace_Startup, + enabled ? RazorResources.Trace_Enabled : RazorResources.Trace_Disabled)); + _enabled = enabled; + } + else + { + _enabled = false; + } + } + return _enabled.Value; + } + + [Conditional("EDITOR_TRACING")] + public static void TraceLine(string format, params object[] args) + { + if (IsEnabled()) + { + Trace.WriteLine(String.Format( + CultureInfo.CurrentCulture, + RazorResources.Trace_Format, + String.Format(CultureInfo.CurrentCulture, format, args))); + } + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Editor/SingleLineMarkupEditHandler.cs b/src/Microsoft.AspNet.Razor/Editor/SingleLineMarkupEditHandler.cs new file mode 100644 index 0000000000..fcf8044dcb --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Editor/SingleLineMarkupEditHandler.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Tokenizer.Symbols; + +namespace Microsoft.AspNet.Razor.Editor +{ + public class SingleLineMarkupEditHandler : SpanEditHandler + { + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Func is the recommended delegate type and requires this level of nesting.")] + public SingleLineMarkupEditHandler(Func> tokenizer) + : base(tokenizer) + { + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Func is the recommended delegate type and requires this level of nesting.")] + public SingleLineMarkupEditHandler(Func> tokenizer, AcceptedCharacters accepted) + : base(tokenizer, accepted) + { + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Editor/SpanEditHandler.cs b/src/Microsoft.AspNet.Razor/Editor/SpanEditHandler.cs new file mode 100644 index 0000000000..8f82e73f00 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Editor/SpanEditHandler.cs @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.Internal.Web.Utils; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Text; +using Microsoft.AspNet.Razor.Tokenizer.Symbols; + +namespace Microsoft.AspNet.Razor.Editor +{ + // Manages edits to a span + public class SpanEditHandler + { + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Func is the recommended delegate type and requires this level of nesting.")] + public SpanEditHandler(Func> tokenizer) + : this(tokenizer, AcceptedCharacters.Any) + { + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Func is the recommended delegate type and requires this level of nesting.")] + public SpanEditHandler(Func> tokenizer, AcceptedCharacters accepted) + { + AcceptedCharacters = accepted; + Tokenizer = tokenizer; + } + + public AcceptedCharacters AcceptedCharacters { get; set; } + + /// + /// Provides a set of hints to editors which may be manipulating the document in which this span is located. + /// + public EditorHints EditorHints { get; set; } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Func is the recommended delegate type and requires this level of nesting.")] + public Func> Tokenizer { get; set; } + + public static SpanEditHandler CreateDefault() + { + return CreateDefault(s => Enumerable.Empty()); + } + + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "Func is the recommended delegate type and requires this level of nesting.")] + public static SpanEditHandler CreateDefault(Func> tokenizer) + { + return new SpanEditHandler(tokenizer); + } + + public virtual EditResult ApplyChange(Span target, TextChange change) + { + return ApplyChange(target, change, force: false); + } + + public virtual EditResult ApplyChange(Span target, TextChange change, bool force) + { + PartialParseResult result = PartialParseResult.Accepted; + TextChange normalized = change.Normalize(); + if (!force) + { + result = CanAcceptChange(target, normalized); + } + + // If the change is accepted then apply the change + if (result.HasFlag(PartialParseResult.Accepted)) + { + return new EditResult(result, UpdateSpan(target, normalized)); + } + return new EditResult(result, new SpanBuilder(target)); + } + + public virtual bool OwnsChange(Span target, TextChange change) + { + int end = target.Start.AbsoluteIndex + target.Length; + int changeOldEnd = change.OldPosition + change.OldLength; + return change.OldPosition >= target.Start.AbsoluteIndex && + (changeOldEnd < end || (changeOldEnd == end && AcceptedCharacters != AcceptedCharacters.None)); + } + + protected virtual PartialParseResult CanAcceptChange(Span target, TextChange normalizedChange) + { + return PartialParseResult.Rejected; + } + + protected virtual SpanBuilder UpdateSpan(Span target, TextChange normalizedChange) + { + string newContent = normalizedChange.ApplyChange(target); + SpanBuilder newSpan = new SpanBuilder(target); + newSpan.ClearSymbols(); + foreach (ISymbol sym in Tokenizer(newContent)) + { + sym.OffsetStart(target.Start); + newSpan.Accept(sym); + } + if (target.Next != null) + { + SourceLocation newEnd = SourceLocationTracker.CalculateNewLocation(target.Start, newContent); + target.Next.ChangeStart(newEnd); + } + return newSpan; + } + + protected internal static bool IsAtEndOfFirstLine(Span target, TextChange change) + { + int endOfFirstLine = target.Content.IndexOfAny(new char[] { (char)0x000d, (char)0x000a, (char)0x2028, (char)0x2029 }); + return (endOfFirstLine == -1 || (change.OldPosition - target.Start.AbsoluteIndex) <= endOfFirstLine); + } + + /// + /// Returns true if the specified change is an insertion of text at the end of this span. + /// + protected internal static bool IsEndInsertion(Span target, TextChange change) + { + return change.IsInsert && IsAtEndOfSpan(target, change); + } + + /// + /// Returns true if the specified change is an insertion of text at the end of this span. + /// + protected internal static bool IsEndDeletion(Span target, TextChange change) + { + return change.IsDelete && IsAtEndOfSpan(target, change); + } + + /// + /// Returns true if the specified change is a replacement of text at the end of this span. + /// + protected internal static bool IsEndReplace(Span target, TextChange change) + { + return change.IsReplace && IsAtEndOfSpan(target, change); + } + + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "This method should only be used on Spans")] + protected internal static bool IsAtEndOfSpan(Span target, TextChange change) + { + return (change.OldPosition + change.OldLength) == (target.Start.AbsoluteIndex + target.Length); + } + + /// + /// Returns the old text referenced by the change. + /// + /// + /// If the content has already been updated by applying the change, this data will be _invalid_ + /// + protected internal static string GetOldText(Span target, TextChange change) + { + return target.Content.Substring(change.OldPosition - target.Start.AbsoluteIndex, change.OldLength); + } + + // Is the specified span to the right of this span and immediately adjacent? + internal static bool IsAdjacentOnRight(Span target, Span other) + { + return target.Start.AbsoluteIndex < other.Start.AbsoluteIndex && target.Start.AbsoluteIndex + target.Length == other.Start.AbsoluteIndex; + } + + // Is the specified span to the left of this span and immediately adjacent? + internal static bool IsAdjacentOnLeft(Span target, Span other) + { + return other.Start.AbsoluteIndex < target.Start.AbsoluteIndex && other.Start.AbsoluteIndex + other.Length == target.Start.AbsoluteIndex; + } + + public override string ToString() + { + return GetType().Name + ";Accepts:" + AcceptedCharacters + ((EditorHints == EditorHints.None) ? String.Empty : (";Hints: " + EditorHints.ToString())); + } + + public override bool Equals(object obj) + { + SpanEditHandler other = obj as SpanEditHandler; + return other != null && + AcceptedCharacters == other.AcceptedCharacters && + EditorHints == other.EditorHints; + } + + public override int GetHashCode() + { + return HashCodeCombiner.Start() + .Add(AcceptedCharacters) + .Add(EditorHints) + .CombinedHash; + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Generator/AddImportCodeGenerator.cs b/src/Microsoft.AspNet.Razor/Generator/AddImportCodeGenerator.cs new file mode 100644 index 0000000000..8c16e7371c --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/AddImportCodeGenerator.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.CodeDom; +using System.Linq; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.Internal.Web.Utils; +using System; + +namespace Microsoft.AspNet.Razor.Generator +{ + public class AddImportCodeGenerator : SpanCodeGenerator + { + public AddImportCodeGenerator(string ns, int namespaceKeywordLength) + { + Namespace = ns; + NamespaceKeywordLength = namespaceKeywordLength; + } + + public string Namespace { get; private set; } + public int NamespaceKeywordLength { get; set; } + + public override void GenerateCode(Span target, CodeGeneratorContext context) + { + // Try to find the namespace in the existing imports + string ns = Namespace; + if (!String.IsNullOrEmpty(ns) && Char.IsWhiteSpace(ns[0])) + { + ns = ns.Substring(1); + } + + CodeNamespaceImport import = context.Namespace + .Imports + .OfType() + .Where(i => String.Equals(i.Namespace, ns.Trim(), StringComparison.Ordinal)) + .FirstOrDefault(); + + if (import == null) + { + // It doesn't exist, create it + import = new CodeNamespaceImport(ns); + context.Namespace.Imports.Add(import); + } + + // Attach our info to the existing/new import. + import.LinePragma = context.GenerateLinePragma(target); + } + + public override string ToString() + { + return "Import:" + Namespace + ";KwdLen:" + NamespaceKeywordLength; + } + + public override bool Equals(object obj) + { + AddImportCodeGenerator other = obj as AddImportCodeGenerator; + return other != null && + String.Equals(Namespace, other.Namespace, StringComparison.Ordinal) && + NamespaceKeywordLength == other.NamespaceKeywordLength; + } + + public override int GetHashCode() + { + return HashCodeCombiner.Start() + .Add(Namespace) + .Add(NamespaceKeywordLength) + .CombinedHash; + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Generator/AttributeBlockCodeGenerator.cs b/src/Microsoft.AspNet.Razor/Generator/AttributeBlockCodeGenerator.cs new file mode 100644 index 0000000000..5d6dec35d7 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/AttributeBlockCodeGenerator.cs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Globalization; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Text; +using Microsoft.Internal.Web.Utils; +using System; + +namespace Microsoft.AspNet.Razor.Generator +{ + public class AttributeBlockCodeGenerator : BlockCodeGenerator + { + public AttributeBlockCodeGenerator(string name, LocationTagged prefix, LocationTagged suffix) + { + Name = name; + Prefix = prefix; + Suffix = suffix; + } + + public string Name { get; private set; } + public LocationTagged Prefix { get; private set; } + public LocationTagged Suffix { get; private set; } + + public override void GenerateStartBlockCode(Block target, CodeGeneratorContext context) + { + if (context.Host.DesignTimeMode) + { + return; // Don't generate anything! + } + context.FlushBufferedStatement(); + context.AddStatement(context.BuildCodeString(cw => + { + if (!String.IsNullOrEmpty(context.TargetWriterName)) + { + cw.WriteStartMethodInvoke(context.Host.GeneratedClassContext.WriteAttributeToMethodName); + cw.WriteSnippet(context.TargetWriterName); + cw.WriteParameterSeparator(); + } + else + { + cw.WriteStartMethodInvoke(context.Host.GeneratedClassContext.WriteAttributeMethodName); + } + cw.WriteStringLiteral(Name); + cw.WriteParameterSeparator(); + cw.WriteLocationTaggedString(Prefix); + cw.WriteParameterSeparator(); + cw.WriteLocationTaggedString(Suffix); + + // In VB, we need a line continuation + cw.WriteLineContinuation(); + })); + } + + public override void GenerateEndBlockCode(Block target, CodeGeneratorContext context) + { + if (context.Host.DesignTimeMode) + { + return; // Don't generate anything! + } + context.FlushBufferedStatement(); + context.AddStatement(context.BuildCodeString(cw => + { + cw.WriteEndMethodInvoke(); + cw.WriteEndStatement(); + })); + } + + public override string ToString() + { + return String.Format(CultureInfo.CurrentCulture, "Attr:{0},{1:F},{2:F}", Name, Prefix, Suffix); + } + + public override bool Equals(object obj) + { + AttributeBlockCodeGenerator other = obj as AttributeBlockCodeGenerator; + return other != null && + String.Equals(other.Name, Name, StringComparison.Ordinal) && + Equals(other.Prefix, Prefix) && + Equals(other.Suffix, Suffix); + } + + public override int GetHashCode() + { + return HashCodeCombiner.Start() + .Add(Name) + .Add(Prefix) + .Add(Suffix) + .CombinedHash; + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Generator/BaseCodeWriter.cs b/src/Microsoft.AspNet.Razor/Generator/BaseCodeWriter.cs new file mode 100644 index 0000000000..2717e8b8a8 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/BaseCodeWriter.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +namespace Microsoft.AspNet.Razor.Generator +{ + internal abstract class BaseCodeWriter : CodeWriter + { + public override void WriteSnippet(string snippet) + { + InnerWriter.Write(snippet); + } + + protected internal override void EmitStartMethodInvoke(string methodName) + { + EmitStartMethodInvoke(methodName, new string[0]); + } + + protected internal override void EmitStartMethodInvoke(string methodName, params string[] genericArguments) + { + InnerWriter.Write(methodName); + if (genericArguments != null && genericArguments.Length > 0) + { + WriteStartGenerics(); + for (int i = 0; i < genericArguments.Length; i++) + { + if (i > 0) + { + WriteParameterSeparator(); + } + WriteSnippet(genericArguments[i]); + } + WriteEndGenerics(); + } + + InnerWriter.Write("("); + } + + protected internal override void EmitEndMethodInvoke() + { + InnerWriter.Write(")"); + } + + protected internal override void EmitEndConstructor() + { + InnerWriter.Write(")"); + } + + protected internal override void EmitEndLambdaExpression() + { + } + + public override void WriteParameterSeparator() + { + InnerWriter.Write(", "); + } + + protected internal void WriteCommaSeparatedList(T[] items, Action writeItemAction) + { + for (int i = 0; i < items.Length; i++) + { + if (i > 0) + { + InnerWriter.Write(", "); + } + writeItemAction(items[i]); + } + } + + protected internal virtual void WriteStartGenerics() + { + } + + protected internal virtual void WriteEndGenerics() + { + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Generator/BlockCodeGenerator.cs b/src/Microsoft.AspNet.Razor/Generator/BlockCodeGenerator.cs new file mode 100644 index 0000000000..e30a551c6f --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/BlockCodeGenerator.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Generator +{ + public abstract class BlockCodeGenerator : IBlockCodeGenerator + { + [SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification = "This class has no instance state")] + public static readonly IBlockCodeGenerator Null = new NullBlockCodeGenerator(); + + public virtual void GenerateStartBlockCode(Block target, CodeGeneratorContext context) + { + } + + public virtual void GenerateEndBlockCode(Block target, CodeGeneratorContext context) + { + } + + public override bool Equals(object obj) + { + return (obj as IBlockCodeGenerator) != null; + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + + private class NullBlockCodeGenerator : IBlockCodeGenerator + { + public void GenerateStartBlockCode(Block target, CodeGeneratorContext context) + { + } + + public void GenerateEndBlockCode(Block target, CodeGeneratorContext context) + { + } + + public override string ToString() + { + return "None"; + } + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Generator/CSharpCodeWriter.cs b/src/Microsoft.AspNet.Razor/Generator/CSharpCodeWriter.cs new file mode 100644 index 0000000000..f3001e4962 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/CSharpCodeWriter.cs @@ -0,0 +1,250 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; + +namespace Microsoft.AspNet.Razor.Generator +{ + internal class CSharpCodeWriter : BaseCodeWriter + { + protected internal override void WriteStartGenerics() + { + InnerWriter.Write("<"); + } + + protected internal override void WriteEndGenerics() + { + InnerWriter.Write(">"); + } + + public override int WriteVariableDeclaration(string type, string name, string value) + { + InnerWriter.Write(type); + InnerWriter.Write(" "); + InnerWriter.Write(name); + if (!String.IsNullOrEmpty(value)) + { + InnerWriter.Write(" = "); + InnerWriter.Write(value); + } + else + { + InnerWriter.Write(" = null"); + } + return 0; + } + + public override void WriteDisableUnusedFieldWarningPragma() + { + InnerWriter.Write("#pragma warning disable 219"); + } + + public override void WriteRestoreUnusedFieldWarningPragma() + { + InnerWriter.Write("#pragma warning restore 219"); + } + + public override void WriteStringLiteral(string literal) + { + if (literal == null) + { + throw new ArgumentNullException("literal"); + } + + // From CSharpCodeProvider in CodeDOM + // If the string is short, use C style quoting (e.g "\r\n") + // Also do it if it is too long to fit in one line + // If the string contains '\0', verbatim style won't work. + if (literal.Length >= 256 && literal.Length <= 1500 && literal.IndexOf('\0') == -1) + { + WriteVerbatimStringLiteral(literal); + } + else + { + WriteCStyleStringLiteral(literal); + } + } + + private void WriteVerbatimStringLiteral(string literal) + { + // From CSharpCodeGenerator.QuoteSnippetStringVerbatim in CodeDOM + InnerWriter.Write("@\""); + for (int i = 0; i < literal.Length; i++) + { + if (literal[i] == '\"') + { + InnerWriter.Write("\"\""); + } + else + { + InnerWriter.Write(literal[i]); + } + } + InnerWriter.Write("\""); + } + + private void WriteCStyleStringLiteral(string literal) + { + // From CSharpCodeGenerator.QuoteSnippetStringCStyle in CodeDOM + InnerWriter.Write("\""); + for (int i = 0; i < literal.Length; i++) + { + switch (literal[i]) + { + case '\r': + InnerWriter.Write("\\r"); + break; + case '\t': + InnerWriter.Write("\\t"); + break; + case '\"': + InnerWriter.Write("\\\""); + break; + case '\'': + InnerWriter.Write("\\\'"); + break; + case '\\': + InnerWriter.Write("\\\\"); + break; + case '\0': + InnerWriter.Write("\\\0"); + break; + case '\n': + InnerWriter.Write("\\n"); + break; + case '\u2028': + case '\u2029': + // Inlined CSharpCodeGenerator.AppendEscapedChar + InnerWriter.Write("\\u"); + InnerWriter.Write(((int)literal[i]).ToString("X4", CultureInfo.InvariantCulture)); + break; + default: + InnerWriter.Write(literal[i]); + break; + } + if (i > 0 && i % 80 == 0) + { + // If current character is a high surrogate and the following + // character is a low surrogate, don't break them. + // Otherwise when we write the string to a file, we might lose + // the characters. + if (Char.IsHighSurrogate(literal[i]) + && (i < literal.Length - 1) + && Char.IsLowSurrogate(literal[i + 1])) + { + InnerWriter.Write(literal[++i]); + } + + InnerWriter.Write("\" +"); + InnerWriter.Write(Environment.NewLine); + InnerWriter.Write('\"'); + } + } + InnerWriter.Write("\""); + } + + public override void WriteEndStatement() + { + InnerWriter.WriteLine(";"); + } + + public override void WriteIdentifier(string identifier) + { + InnerWriter.Write("@" + identifier); + } + + [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "Lowercase is intended here. C# boolean literals are all lowercase")] + public override void WriteBooleanLiteral(bool value) + { + WriteSnippet(value.ToString().ToLowerInvariant()); + } + + protected internal override void EmitStartLambdaExpression(string[] parameterNames) + { + if (parameterNames == null) + { + throw new ArgumentNullException("parameterNames"); + } + + if (parameterNames.Length == 0 || parameterNames.Length > 1) + { + InnerWriter.Write("("); + } + WriteCommaSeparatedList(parameterNames, InnerWriter.Write); + if (parameterNames.Length == 0 || parameterNames.Length > 1) + { + InnerWriter.Write(")"); + } + InnerWriter.Write(" => "); + } + + protected internal override void EmitStartLambdaDelegate(string[] parameterNames) + { + if (parameterNames == null) + { + throw new ArgumentNullException("parameterNames"); + } + + EmitStartLambdaExpression(parameterNames); + InnerWriter.WriteLine("{"); + } + + protected internal override void EmitEndLambdaDelegate() + { + InnerWriter.Write("}"); + } + + protected internal override void EmitStartConstructor(string typeName) + { + if (typeName == null) + { + throw new ArgumentNullException("typeName"); + } + + InnerWriter.Write("new "); + InnerWriter.Write(typeName); + InnerWriter.Write("("); + } + + public override void WriteReturn() + { + InnerWriter.Write("return "); + } + + public override void WriteLinePragma(int? lineNumber, string fileName) + { + InnerWriter.WriteLine(); + if (lineNumber != null) + { + InnerWriter.Write("#line "); + InnerWriter.Write(lineNumber); + InnerWriter.Write(" \""); + InnerWriter.Write(fileName); + InnerWriter.Write("\""); + InnerWriter.WriteLine(); + } + else + { + InnerWriter.WriteLine("#line default"); + InnerWriter.WriteLine("#line hidden"); + } + } + + public override void WriteHiddenLinePragma() + { + InnerWriter.WriteLine("#line hidden"); + } + + public override void WriteHelperHeaderPrefix(string templateTypeName, bool isStatic) + { + InnerWriter.Write("public "); + if (isStatic) + { + InnerWriter.Write("static "); + } + InnerWriter.Write(templateTypeName); + InnerWriter.Write(" "); + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Generator/CSharpRazorCodeGenerator.cs b/src/Microsoft.AspNet.Razor/Generator/CSharpRazorCodeGenerator.cs new file mode 100644 index 0000000000..fcb1f27d53 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/CSharpRazorCodeGenerator.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.CodeDom; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.AspNet.Razor.Generator +{ + public class CSharpRazorCodeGenerator : RazorCodeGenerator + { + private const string HiddenLinePragma = "#line hidden"; + + public CSharpRazorCodeGenerator(string className, string rootNamespaceName, string sourceFileName, RazorEngineHost host) + : base(className, rootNamespaceName, sourceFileName, host) + { + } + + internal override Func CodeWriterFactory + { + get { return () => new CSharpCodeWriter(); } + } + + [SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "System.CodeDom.CodeSnippetTypeMember.#ctor(System.String)", Justification = "Value is never to be localized")] + protected override void Initialize(CodeGeneratorContext context) + { + base.Initialize(context); + + context.GeneratedClass.Members.Insert(0, new CodeSnippetTypeMember(HiddenLinePragma)); + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Generator/CodeGenerationCompleteEventArgs.cs b/src/Microsoft.AspNet.Razor/Generator/CodeGenerationCompleteEventArgs.cs new file mode 100644 index 0000000000..32c12aac20 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/CodeGenerationCompleteEventArgs.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.CodeDom; +using Microsoft.Internal.Web.Utils; + +namespace Microsoft.AspNet.Razor.Generator +{ + public class CodeGenerationCompleteEventArgs : EventArgs + { + public CodeGenerationCompleteEventArgs(string virtualPath, string physicalPath, CodeCompileUnit generatedCode) + { + if (String.IsNullOrEmpty(virtualPath)) + { + throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "virtualPath"); + } + if (generatedCode == null) + { + throw new ArgumentNullException("generatedCode"); + } + VirtualPath = virtualPath; + PhysicalPath = physicalPath; + GeneratedCode = generatedCode; + } + + public CodeCompileUnit GeneratedCode { get; private set; } + public string VirtualPath { get; private set; } + public string PhysicalPath { get; private set; } + } +} diff --git a/src/Microsoft.AspNet.Razor/Generator/CodeGeneratorContext.cs b/src/Microsoft.AspNet.Razor/Generator/CodeGeneratorContext.cs new file mode 100644 index 0000000000..5c36bd24ca --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/CodeGeneratorContext.cs @@ -0,0 +1,334 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.CodeDom; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Text; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Resources; +using Microsoft.AspNet.Razor.Text; +using Microsoft.AspNet.Razor.Utils; + +namespace Microsoft.AspNet.Razor.Generator +{ + public class CodeGeneratorContext + { + private const string DesignTimeHelperMethodName = "__RazorDesignTimeHelpers__"; + + private int _nextDesignTimePragmaId = 1; + private bool _expressionHelperVariableWriten; + private CodeMemberMethod _designTimeHelperMethod; + private StatementBuffer _currentBuffer = new StatementBuffer(); + + private CodeGeneratorContext() + { + ExpressionRenderingMode = ExpressionRenderingMode.WriteToOutput; + } + + // Internal/Private state. Technically consumers might want to use some of these but they can implement them independently if necessary. + // It's way safer to make them internal for now, especially with the code generator stuff in a bit of flux. + internal ExpressionRenderingMode ExpressionRenderingMode { get; set; } + private Action StatementCollector { get; set; } + private Func CodeWriterFactory { get; set; } + + public string SourceFile { get; internal set; } + public CodeCompileUnit CompileUnit { get; internal set; } + public CodeNamespace Namespace { get; internal set; } + public CodeTypeDeclaration GeneratedClass { get; internal set; } + public RazorEngineHost Host { get; private set; } + public IDictionary CodeMappings { get; private set; } + public string TargetWriterName { get; set; } + public CodeMemberMethod TargetMethod { get; set; } + + public string CurrentBufferedStatement + { + get { return _currentBuffer == null ? String.Empty : _currentBuffer.Builder.ToString(); } + } + + public static CodeGeneratorContext Create(RazorEngineHost host, string className, string rootNamespace, string sourceFile, bool shouldGenerateLinePragmas) + { + return Create(host, null, className, rootNamespace, sourceFile, shouldGenerateLinePragmas); + } + + internal static CodeGeneratorContext Create(RazorEngineHost host, Func writerFactory, string className, string rootNamespace, string sourceFile, bool shouldGenerateLinePragmas) + { + CodeGeneratorContext context = new CodeGeneratorContext() + { + Host = host, + CodeWriterFactory = writerFactory, + SourceFile = shouldGenerateLinePragmas ? sourceFile : null, + CompileUnit = new CodeCompileUnit(), + Namespace = new CodeNamespace(rootNamespace), + GeneratedClass = new CodeTypeDeclaration(className) + { + IsClass = true + }, + TargetMethod = new CodeMemberMethod() + { + Name = host.GeneratedClassContext.ExecuteMethodName, + Attributes = MemberAttributes.Override | MemberAttributes.Public + }, + CodeMappings = new Dictionary() + }; + context.CompileUnit.Namespaces.Add(context.Namespace); + context.Namespace.Types.Add(context.GeneratedClass); + context.GeneratedClass.Members.Add(context.TargetMethod); + + context.Namespace.Imports.AddRange(host.NamespaceImports + .Select(s => new CodeNamespaceImport(s)) + .ToArray()); + + return context; + } + + public void AddDesignTimeHelperStatement(CodeSnippetStatement statement) + { + if (_designTimeHelperMethod == null) + { + _designTimeHelperMethod = new CodeMemberMethod() + { + Name = DesignTimeHelperMethodName, + Attributes = MemberAttributes.Private + }; + _designTimeHelperMethod.Statements.Add( + new CodeSnippetStatement(BuildCodeString(cw => cw.WriteDisableUnusedFieldWarningPragma()))); + _designTimeHelperMethod.Statements.Add( + new CodeSnippetStatement(BuildCodeString(cw => cw.WriteRestoreUnusedFieldWarningPragma()))); + GeneratedClass.Members.Insert(0, _designTimeHelperMethod); + } + _designTimeHelperMethod.Statements.Insert(_designTimeHelperMethod.Statements.Count - 1, statement); + } + + [SuppressMessage("Microsoft.Usage", "CA2233:OperationsShouldNotOverflow", MessageId = "generatedCodeStart+1", Justification = "There is no risk of overflow in this case")] + public int AddCodeMapping(SourceLocation sourceLocation, int generatedCodeStart, int generatedCodeLength) + { + if (generatedCodeStart == Int32.MaxValue) + { + throw new ArgumentOutOfRangeException("generatedCodeStart"); + } + + GeneratedCodeMapping mapping = new GeneratedCodeMapping( + startOffset: sourceLocation.AbsoluteIndex, + startLine: sourceLocation.LineIndex + 1, + startColumn: sourceLocation.CharacterIndex + 1, + startGeneratedColumn: generatedCodeStart + 1, + codeLength: generatedCodeLength); + + int id = _nextDesignTimePragmaId++; + CodeMappings[id] = mapping; + return id; + } + + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "This method requires that a Span be provided")] + public CodeLinePragma GenerateLinePragma(Span target) + { + return GenerateLinePragma(target, 0); + } + + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "This method requires that a Span be provided")] + public CodeLinePragma GenerateLinePragma(Span target, int generatedCodeStart) + { + return GenerateLinePragma(target, generatedCodeStart, target.Content.Length); + } + + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "This method requires that a Span be provided")] + public CodeLinePragma GenerateLinePragma(Span target, int generatedCodeStart, int codeLength) + { + return GenerateLinePragma(target.Start, generatedCodeStart, codeLength); + } + + public CodeLinePragma GenerateLinePragma(SourceLocation start, int generatedCodeStart, int codeLength) + { + if (!String.IsNullOrEmpty(SourceFile)) + { + if (Host.DesignTimeMode) + { + int mappingId = AddCodeMapping(start, generatedCodeStart, codeLength); + return new CodeLinePragma(SourceFile, mappingId); + } + return new CodeLinePragma(SourceFile, start.LineIndex + 1); + } + return null; + } + + public void BufferStatementFragment(Span sourceSpan) + { + BufferStatementFragment(sourceSpan.Content, sourceSpan); + } + + public void BufferStatementFragment(string fragment) + { + BufferStatementFragment(fragment, null); + } + + public void BufferStatementFragment(string fragment, Span sourceSpan) + { + if (sourceSpan != null && _currentBuffer.LinePragmaSpan == null) + { + _currentBuffer.LinePragmaSpan = sourceSpan; + + // Pad the output as necessary + int start = _currentBuffer.Builder.Length; + if (_currentBuffer.GeneratedCodeStart != null) + { + start = _currentBuffer.GeneratedCodeStart.Value; + } + + int paddingLength; // unused, in this case there is enough context in the original code to calculate the right padding length + // (padded.Length - _currentBuffer.Builder.Length) + + string padded = CodeGeneratorPaddingHelper.Pad(Host, _currentBuffer.Builder.ToString(), sourceSpan, start, out paddingLength); + _currentBuffer.GeneratedCodeStart = start + (padded.Length - _currentBuffer.Builder.Length); + _currentBuffer.Builder.Clear(); + _currentBuffer.Builder.Append(padded); + } + _currentBuffer.Builder.Append(fragment); + } + + public void MarkStartOfGeneratedCode() + { + _currentBuffer.MarkStart(); + } + + public void MarkEndOfGeneratedCode() + { + _currentBuffer.MarkEnd(); + } + + public void FlushBufferedStatement() + { + if (_currentBuffer.Builder.Length > 0) + { + CodeLinePragma pragma = null; + if (_currentBuffer.LinePragmaSpan != null) + { + int start = _currentBuffer.Builder.Length; + if (_currentBuffer.GeneratedCodeStart != null) + { + start = _currentBuffer.GeneratedCodeStart.Value; + } + int len = _currentBuffer.Builder.Length - start; + if (_currentBuffer.CodeLength != null) + { + len = _currentBuffer.CodeLength.Value; + } + pragma = GenerateLinePragma(_currentBuffer.LinePragmaSpan, start, len); + } + AddStatement(_currentBuffer.Builder.ToString(), pragma); + _currentBuffer.Reset(); + } + } + + public void AddStatement(string generatedCode) + { + AddStatement(generatedCode, null); + } + + public void AddStatement(string body, CodeLinePragma pragma) + { + if (StatementCollector == null) + { + TargetMethod.Statements.Add(new CodeSnippetStatement(body) { LinePragma = pragma }); + } + else + { + StatementCollector(body, pragma); + } + } + + public void EnsureExpressionHelperVariable() + { + if (!_expressionHelperVariableWriten) + { + GeneratedClass.Members.Insert(0, + new CodeMemberField(typeof(object), "__o") + { + Attributes = MemberAttributes.Private | MemberAttributes.Static + }); + _expressionHelperVariableWriten = true; + } + } + + public IDisposable ChangeStatementCollector(Action collector) + { + Action oldCollector = StatementCollector; + StatementCollector = collector; + return new DisposableAction(() => + { + StatementCollector = oldCollector; + }); + } + + [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "We explicitly want the lower-case string here")] + public void AddContextCall(Span contentSpan, string methodName, bool isLiteral) + { + AddStatement(BuildCodeString(cw => + { + cw.WriteStartMethodInvoke(methodName); + if (!String.IsNullOrEmpty(TargetWriterName)) + { + cw.WriteSnippet(TargetWriterName); + cw.WriteParameterSeparator(); + } + cw.WriteStringLiteral(Host.InstrumentedSourceFilePath); + cw.WriteParameterSeparator(); + cw.WriteSnippet(contentSpan.Start.AbsoluteIndex.ToString(CultureInfo.InvariantCulture)); + cw.WriteParameterSeparator(); + cw.WriteSnippet(contentSpan.Content.Length.ToString(CultureInfo.InvariantCulture)); + cw.WriteParameterSeparator(); + cw.WriteSnippet(isLiteral.ToString().ToLowerInvariant()); + cw.WriteEndMethodInvoke(); + cw.WriteEndStatement(); + })); + } + + internal CodeWriter CreateCodeWriter() + { + Debug.Assert(CodeWriterFactory != null); + if (CodeWriterFactory == null) + { + throw new InvalidOperationException(RazorResources.CreateCodeWriter_NoCodeWriter); + } + return CodeWriterFactory(); + } + + internal string BuildCodeString(Action action) + { + using (CodeWriter cw = CodeWriterFactory()) + { + action(cw); + return cw.Content; + } + } + + private class StatementBuffer + { + public StringBuilder Builder = new StringBuilder(); + public int? GeneratedCodeStart; + public int? CodeLength; + public Span LinePragmaSpan; + + public void Reset() + { + Builder.Clear(); + GeneratedCodeStart = null; + CodeLength = null; + LinePragmaSpan = null; + } + + public void MarkStart() + { + GeneratedCodeStart = Builder.Length; + } + + public void MarkEnd() + { + CodeLength = Builder.Length - GeneratedCodeStart; + } + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Generator/CodeGeneratorPaddingHelper.cs b/src/Microsoft.AspNet.Razor/Generator/CodeGeneratorPaddingHelper.cs new file mode 100644 index 0000000000..d1bd8f5d20 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/CodeGeneratorPaddingHelper.cs @@ -0,0 +1,204 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using Microsoft.AspNet.Razor.Parser; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Generator +{ + internal static class CodeGeneratorPaddingHelper + { + private static readonly char[] _newLineChars = { '\r', '\n' }; + + // there is some duplicity of code here, but its very simple and since this is a host path, I'd rather not create another class to encapsulate the data. + public static int PaddingCharCount(RazorEngineHost host, Span target, int generatedStart) + { + int padding = CalculatePadding(host, target, generatedStart); + + if (host.DesignTimeMode && host.IsIndentingWithTabs) + { + int spaces; + int tabs = Math.DivRem(padding, host.TabSize, out spaces); + + return tabs + spaces; + } + else + { + return padding; + } + } + + // Special case for statement padding to account for brace positioning in the editor. + public static string PadStatement(RazorEngineHost host, string code, Span target, ref int startGeneratedCode, out int paddingCharCount) + { + if (host == null) + { + throw new ArgumentNullException("host"); + } + + if (target == null) + { + throw new ArgumentNullException("target"); + } + + // We are passing 0 rather than startgeneratedcode intentionally (keeping v2 behavior). + int padding = CalculatePadding(host, target, 0); + + // We treat statement padding specially so for brace positioning, so that in the following example: + // @if (foo > 0) + // { + // } + // + // the braces shows up under the @ rather than under the if. + if (host.DesignTimeMode && + padding > 0 && + target.Previous.Kind == SpanKind.Transition && // target.Previous is guaranteed to be none null if you got any padding. + String.Equals(target.Previous.Content, SyntaxConstants.TransitionString)) + { + padding--; + startGeneratedCode--; + } + + string generatedCode = PadInternal(host, code, padding, out paddingCharCount); + + return generatedCode; + } + + public static string Pad(RazorEngineHost host, string code, Span target, out int paddingCharCount) + { + int padding = CalculatePadding(host, target, 0); + + return PadInternal(host, code, padding, out paddingCharCount); + } + + public static string Pad(RazorEngineHost host, string code, Span target, int generatedStart, out int paddingCharCount) + { + int padding = CalculatePadding(host, target, generatedStart); + + return PadInternal(host, code, padding, out paddingCharCount); + } + + // internal for unit testing only, not intended to be used directly in code + internal static int CalculatePadding(RazorEngineHost host, Span target, int generatedStart) + { + if (host == null) + { + throw new ArgumentNullException("host"); + } + + if (target == null) + { + throw new ArgumentNullException("target"); + } + + int padding; + + padding = CollectSpacesAndTabs(target, host.TabSize) - generatedStart; + + // if we add generated text that is longer than the padding we wanted to insert we have no recourse and we have to skip padding + // example: + // Razor code at column zero: @somecode() + // Generated code will be: + // In design time: __o = somecode(); + // In Run time: Write(somecode()); + // + // In both cases the padding would have been 1 space to remote the space the @ symbol takes, which will be smaller than the 6 chars the hidden generated code takes. + if (padding < 0) + { + padding = 0; + } + + return padding; + } + + private static string PadInternal(RazorEngineHost host, string code, int padding, out int paddingCharCount) + { + if (host.DesignTimeMode && host.IsIndentingWithTabs) + { + int spaces; + int tabs = Math.DivRem(padding, host.TabSize, out spaces); + + paddingCharCount = tabs + spaces; + + return new string('\t', tabs) + new string(' ', spaces) + code; + } + else + { + paddingCharCount = padding; + return code.PadLeft(padding + code.Length, ' '); + } + } + + private static int CollectSpacesAndTabs(Span target, int tabSize) + { + Span firstSpanInLine = target; + + string currentContent = null; + + while (firstSpanInLine.Previous != null) + { + // When scanning previous spans we need to be break down the spans with spaces. + // Because the parser doesn't so for example a span looking like \n\n\t needs to be broken down, and we should just grab the \t. + String previousContent = firstSpanInLine.Previous.Content ?? String.Empty; + + int lastNewLineIndex = previousContent.LastIndexOfAny(_newLineChars); + + if (lastNewLineIndex < 0) + { + firstSpanInLine = firstSpanInLine.Previous; + } + else + { + if (lastNewLineIndex != previousContent.Length - 1) + { + firstSpanInLine = firstSpanInLine.Previous; + currentContent = previousContent.Substring(lastNewLineIndex + 1); + } + + break; + } + } + + // We need to walk from the beginning of the line, because space + tab(tabSize) = tabSize columns, but tab(tabSize) + space = tabSize+1 columns. + Span currentSpanInLine = firstSpanInLine; + + if (currentContent == null) + { + currentContent = currentSpanInLine.Content; + } + + int padding = 0; + while (currentSpanInLine != target) + { + if (currentContent != null) + { + for (int i = 0; i < currentContent.Length; i++) + { + if (currentContent[i] == '\t') + { + // Example: + // : + // iter 1) 1 + // iter 2) 2 + // iter 3) 4 = 2 + (4 - 2) + // iter 4) 8 = 4 + (4 - 0) + padding = padding + (tabSize - (padding % tabSize)); + } + else + { + padding++; + } + } + } + + currentSpanInLine = currentSpanInLine.Next; + currentContent = currentSpanInLine.Content; + } + + return padding; + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Generator/CodeWriter.cs b/src/Microsoft.AspNet.Razor/Generator/CodeWriter.cs new file mode 100644 index 0000000000..8f378f162d --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/CodeWriter.cs @@ -0,0 +1,210 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.CodeDom; +using System.Globalization; +using System.IO; + +namespace Microsoft.AspNet.Razor.Generator +{ + // Utility class which helps write code snippets + internal abstract class CodeWriter : IDisposable + { + private StringWriter _writer; + + protected CodeWriter() + { + } + + private enum WriterMode + { + Constructor, + MethodCall, + LambdaDelegate, + LambdaExpression + } + + public string Content + { + get { return InnerWriter.ToString(); } + } + + public StringWriter InnerWriter + { + get + { + if (_writer == null) + { + _writer = new StringWriter(CultureInfo.InvariantCulture); + } + return _writer; + } + } + + public virtual bool SupportsMidStatementLinePragmas + { + get { return true; } + } + + public abstract void WriteParameterSeparator(); + public abstract void WriteReturn(); + public abstract void WriteLinePragma(int? lineNumber, string fileName); + public abstract void WriteHelperHeaderPrefix(string templateTypeName, bool isStatic); + public abstract void WriteSnippet(string snippet); + public abstract void WriteStringLiteral(string literal); + public abstract int WriteVariableDeclaration(string type, string name, string value); + + public virtual void WriteLinePragma() + { + WriteLinePragma(null); + } + + public virtual void WriteLinePragma(CodeLinePragma pragma) + { + if (pragma == null) + { + WriteLinePragma(null, null); + } + else + { + WriteLinePragma(pragma.LineNumber, pragma.FileName); + } + } + + public virtual void WriteHiddenLinePragma() + { + } + + public virtual void WriteDisableUnusedFieldWarningPragma() + { + } + + public virtual void WriteRestoreUnusedFieldWarningPragma() + { + } + + public virtual void WriteIdentifier(string identifier) + { + InnerWriter.Write(identifier); + } + + public virtual void WriteHelperHeaderSuffix(string templateTypeName) + { + } + + public virtual void WriteHelperTrailer() + { + } + + public void WriteStartMethodInvoke(string methodName) + { + EmitStartMethodInvoke(methodName); + } + + public void WriteStartMethodInvoke(string methodName, params string[] genericArguments) + { + EmitStartMethodInvoke(methodName, genericArguments); + } + + public void WriteEndMethodInvoke() + { + EmitEndMethodInvoke(); + } + + public virtual void WriteEndStatement() + { + } + + public virtual void WriteStartAssignment(string variableName) + { + InnerWriter.Write(variableName); + InnerWriter.Write(" = "); + } + + public void WriteStartLambdaExpression(params string[] parameterNames) + { + EmitStartLambdaExpression(parameterNames); + } + + public void WriteStartConstructor(string typeName) + { + EmitStartConstructor(typeName); + } + + public void WriteStartLambdaDelegate(params string[] parameterNames) + { + EmitStartLambdaDelegate(parameterNames); + } + + public void WriteEndLambdaExpression() + { + EmitEndLambdaExpression(); + } + + public void WriteEndConstructor() + { + EmitEndConstructor(); + } + + public void WriteEndLambdaDelegate() + { + EmitEndLambdaDelegate(); + } + + public virtual void WriteLineContinuation() + { + } + + public virtual void WriteBooleanLiteral(bool value) + { + WriteSnippet(value.ToString(CultureInfo.InvariantCulture)); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public void Clear() + { + if (InnerWriter != null) + { + InnerWriter.GetStringBuilder().Clear(); + } + } + + public CodeSnippetStatement ToStatement() + { + return new CodeSnippetStatement(Content); + } + + public CodeSnippetTypeMember ToTypeMember() + { + return new CodeSnippetTypeMember(Content); + } + + protected internal abstract void EmitStartLambdaDelegate(string[] parameterNames); + protected internal abstract void EmitStartLambdaExpression(string[] parameterNames); + protected internal abstract void EmitStartConstructor(string typeName); + protected internal abstract void EmitStartMethodInvoke(string methodName); + + protected internal virtual void EmitStartMethodInvoke(string methodName, params string[] genericArguments) + { + EmitStartMethodInvoke(methodName); + } + + protected internal abstract void EmitEndLambdaDelegate(); + protected internal abstract void EmitEndLambdaExpression(); + protected internal abstract void EmitEndConstructor(); + protected internal abstract void EmitEndMethodInvoke(); + + protected virtual void Dispose(bool disposing) + { + if (disposing && _writer != null) + { + _writer.Dispose(); + } + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Generator/CodeWriterExtensions.cs b/src/Microsoft.AspNet.Razor/Generator/CodeWriterExtensions.cs new file mode 100644 index 0000000000..9e57f5c1fa --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/CodeWriterExtensions.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Globalization; +using Microsoft.AspNet.Razor.Text; + +namespace Microsoft.AspNet.Razor.Generator +{ + internal static class CodeWriterExtensions + { + public static void WriteLocationTaggedString(this CodeWriter writer, LocationTagged value) + { + writer.WriteStartMethodInvoke("Tuple.Create"); + writer.WriteStringLiteral(value.Value); + writer.WriteParameterSeparator(); + writer.WriteSnippet(value.Location.AbsoluteIndex.ToString(CultureInfo.CurrentCulture)); + writer.WriteEndMethodInvoke(); + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Generator/DynamicAttributeBlockCodeGenerator.cs b/src/Microsoft.AspNet.Razor/Generator/DynamicAttributeBlockCodeGenerator.cs new file mode 100644 index 0000000000..81a5580215 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/DynamicAttributeBlockCodeGenerator.cs @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Globalization; +using System.Linq; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Text; +using Microsoft.Internal.Web.Utils; +using System; + +namespace Microsoft.AspNet.Razor.Generator +{ + public class DynamicAttributeBlockCodeGenerator : BlockCodeGenerator + { + private const string ValueWriterName = "__razor_attribute_value_writer"; + private string _oldTargetWriter; + private bool _isExpression; + private ExpressionRenderingMode _oldRenderingMode; + + public DynamicAttributeBlockCodeGenerator(LocationTagged prefix, int offset, int line, int col) + : this(prefix, new SourceLocation(offset, line, col)) + { + } + + public DynamicAttributeBlockCodeGenerator(LocationTagged prefix, SourceLocation valueStart) + { + Prefix = prefix; + ValueStart = valueStart; + } + + public LocationTagged Prefix { get; private set; } + public SourceLocation ValueStart { get; private set; } + + public override void GenerateStartBlockCode(Block target, CodeGeneratorContext context) + { + if (context.Host.DesignTimeMode) + { + return; // Don't generate anything! + } + + // What kind of block is nested within + string generatedCode; + Block child = target.Children.Where(n => n.IsBlock).Cast().FirstOrDefault(); + if (child != null && child.Type == BlockType.Expression) + { + _isExpression = true; + generatedCode = context.BuildCodeString(cw => + { + cw.WriteParameterSeparator(); + cw.WriteStartMethodInvoke("Tuple.Create"); + cw.WriteLocationTaggedString(Prefix); + cw.WriteParameterSeparator(); + cw.WriteStartMethodInvoke("Tuple.Create", "System.Object", "System.Int32"); + }); + + _oldRenderingMode = context.ExpressionRenderingMode; + context.ExpressionRenderingMode = ExpressionRenderingMode.InjectCode; + } + else + { + generatedCode = context.BuildCodeString(cw => + { + cw.WriteParameterSeparator(); + cw.WriteStartMethodInvoke("Tuple.Create"); + cw.WriteLocationTaggedString(Prefix); + cw.WriteParameterSeparator(); + cw.WriteStartMethodInvoke("Tuple.Create", "System.Object", "System.Int32"); + cw.WriteStartConstructor(context.Host.GeneratedClassContext.TemplateTypeName); + cw.WriteStartLambdaDelegate(ValueWriterName); + }); + } + + context.MarkEndOfGeneratedCode(); + context.BufferStatementFragment(generatedCode); + + _oldTargetWriter = context.TargetWriterName; + context.TargetWriterName = ValueWriterName; + } + + public override void GenerateEndBlockCode(Block target, CodeGeneratorContext context) + { + if (context.Host.DesignTimeMode) + { + return; // Don't generate anything! + } + + string generatedCode; + if (_isExpression) + { + generatedCode = context.BuildCodeString(cw => + { + cw.WriteParameterSeparator(); + cw.WriteSnippet(ValueStart.AbsoluteIndex.ToString(CultureInfo.CurrentCulture)); + cw.WriteEndMethodInvoke(); + cw.WriteParameterSeparator(); + // literal: false - This attribute value is not a literal value, it is dynamically generated + cw.WriteBooleanLiteral(false); + cw.WriteEndMethodInvoke(); + cw.WriteLineContinuation(); + }); + context.ExpressionRenderingMode = _oldRenderingMode; + } + else + { + generatedCode = context.BuildCodeString(cw => + { + cw.WriteEndLambdaDelegate(); + cw.WriteEndConstructor(); + cw.WriteParameterSeparator(); + cw.WriteSnippet(ValueStart.AbsoluteIndex.ToString(CultureInfo.CurrentCulture)); + cw.WriteEndMethodInvoke(); + cw.WriteParameterSeparator(); + // literal: false - This attribute value is not a literal value, it is dynamically generated + cw.WriteBooleanLiteral(false); + cw.WriteEndMethodInvoke(); + cw.WriteLineContinuation(); + }); + } + + context.AddStatement(generatedCode); + context.TargetWriterName = _oldTargetWriter; + } + + public override string ToString() + { + return String.Format(CultureInfo.CurrentCulture, "DynAttr:{0:F}", Prefix); + } + + public override bool Equals(object obj) + { + DynamicAttributeBlockCodeGenerator other = obj as DynamicAttributeBlockCodeGenerator; + return other != null && + Equals(other.Prefix, Prefix); + } + + public override int GetHashCode() + { + return HashCodeCombiner.Start() + .Add(Prefix) + .CombinedHash; + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Generator/ExpressionCodeGenerator.cs b/src/Microsoft.AspNet.Razor/Generator/ExpressionCodeGenerator.cs new file mode 100644 index 0000000000..204ff17dab --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/ExpressionCodeGenerator.cs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Linq; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Generator +{ + public class ExpressionCodeGenerator : HybridCodeGenerator + { + public override void GenerateStartBlockCode(Block target, CodeGeneratorContext context) + { + if (context.Host.EnableInstrumentation && context.ExpressionRenderingMode == ExpressionRenderingMode.WriteToOutput) + { + Span contentSpan = target.Children + .OfType() + .Where(s => s.Kind == SpanKind.Code || s.Kind == SpanKind.Markup) + .FirstOrDefault(); + + if (contentSpan != null) + { + context.AddContextCall(contentSpan, context.Host.GeneratedClassContext.BeginContextMethodName, false); + } + } + + string writeInvocation = context.BuildCodeString(cw => + { + if (context.Host.DesignTimeMode) + { + context.EnsureExpressionHelperVariable(); + cw.WriteStartAssignment("__o"); + } + else if (context.ExpressionRenderingMode == ExpressionRenderingMode.WriteToOutput) + { + if (!String.IsNullOrEmpty(context.TargetWriterName)) + { + cw.WriteStartMethodInvoke(context.Host.GeneratedClassContext.WriteToMethodName); + cw.WriteSnippet(context.TargetWriterName); + cw.WriteParameterSeparator(); + } + else + { + cw.WriteStartMethodInvoke(context.Host.GeneratedClassContext.WriteMethodName); + } + } + }); + + context.BufferStatementFragment(writeInvocation); + context.MarkStartOfGeneratedCode(); + } + + public override void GenerateEndBlockCode(Block target, CodeGeneratorContext context) + { + string endBlock = context.BuildCodeString(cw => + { + if (context.ExpressionRenderingMode == ExpressionRenderingMode.WriteToOutput) + { + if (!context.Host.DesignTimeMode) + { + cw.WriteEndMethodInvoke(); + } + cw.WriteEndStatement(); + } + else + { + cw.WriteLineContinuation(); + } + }); + + context.MarkEndOfGeneratedCode(); + context.BufferStatementFragment(endBlock); + context.FlushBufferedStatement(); + + if (context.Host.EnableInstrumentation && context.ExpressionRenderingMode == ExpressionRenderingMode.WriteToOutput) + { + Span contentSpan = target.Children + .OfType() + .Where(s => s.Kind == SpanKind.Code || s.Kind == SpanKind.Markup) + .FirstOrDefault(); + + if (contentSpan != null) + { + context.AddContextCall(contentSpan, context.Host.GeneratedClassContext.EndContextMethodName, false); + } + } + } + + public override void GenerateCode(Span target, CodeGeneratorContext context) + { + Span sourceSpan = null; + if (context.CreateCodeWriter().SupportsMidStatementLinePragmas || context.ExpressionRenderingMode == ExpressionRenderingMode.WriteToOutput) + { + sourceSpan = target; + } + context.BufferStatementFragment(target.Content, sourceSpan); + } + + public override string ToString() + { + return "Expr"; + } + + public override bool Equals(object obj) + { + return obj is ExpressionCodeGenerator; + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Generator/ExpressionRenderingMode.cs b/src/Microsoft.AspNet.Razor/Generator/ExpressionRenderingMode.cs new file mode 100644 index 0000000000..77429fdb12 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/ExpressionRenderingMode.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Razor.Generator +{ + public enum ExpressionRenderingMode + { + /// + /// Indicates that expressions should be written to the output stream + /// + /// + /// If @foo is rendered with WriteToOutput, the code generator would output the following code: + /// + /// Write(foo); + /// + WriteToOutput, + + /// + /// Indicates that expressions should simply be placed as-is in the code, and the context in which + /// the code exists will be used to render it + /// + /// + /// If @foo is rendered with InjectCode, the code generator would output the following code: + /// + /// foo + /// + InjectCode + } +} diff --git a/src/Microsoft.AspNet.Razor/Generator/GeneratedClassContext.cs b/src/Microsoft.AspNet.Razor/Generator/GeneratedClassContext.cs new file mode 100644 index 0000000000..4037e7f6e3 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/GeneratedClassContext.cs @@ -0,0 +1,177 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using Microsoft.Internal.Web.Utils; + +namespace Microsoft.AspNet.Razor.Generator +{ + public struct GeneratedClassContext + { + public static readonly string DefaultWriteMethodName = "Write"; + public static readonly string DefaultWriteLiteralMethodName = "WriteLiteral"; + public static readonly string DefaultExecuteMethodName = "Execute"; + public static readonly string DefaultLayoutPropertyName = "Layout"; + public static readonly string DefaultWriteAttributeMethodName = "WriteAttribute"; + public static readonly string DefaultWriteAttributeToMethodName = "WriteAttributeTo"; + + public static readonly GeneratedClassContext Default = new GeneratedClassContext(DefaultExecuteMethodName, + DefaultWriteMethodName, + DefaultWriteLiteralMethodName); + + public GeneratedClassContext(string executeMethodName, string writeMethodName, string writeLiteralMethodName) + : this() + { + if (String.IsNullOrEmpty(executeMethodName)) + { + throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, + CommonResources.Argument_Cannot_Be_Null_Or_Empty, + "executeMethodName"), + "executeMethodName"); + } + if (String.IsNullOrEmpty(writeMethodName)) + { + throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, + CommonResources.Argument_Cannot_Be_Null_Or_Empty, + "writeMethodName"), + "writeMethodName"); + } + if (String.IsNullOrEmpty(writeLiteralMethodName)) + { + throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, + CommonResources.Argument_Cannot_Be_Null_Or_Empty, + "writeLiteralMethodName"), + "writeLiteralMethodName"); + } + + WriteMethodName = writeMethodName; + WriteLiteralMethodName = writeLiteralMethodName; + ExecuteMethodName = executeMethodName; + + WriteToMethodName = null; + WriteLiteralToMethodName = null; + TemplateTypeName = null; + DefineSectionMethodName = null; + + LayoutPropertyName = DefaultLayoutPropertyName; + WriteAttributeMethodName = DefaultWriteAttributeMethodName; + WriteAttributeToMethodName = DefaultWriteAttributeToMethodName; + } + + public GeneratedClassContext(string executeMethodName, + string writeMethodName, + string writeLiteralMethodName, + string writeToMethodName, + string writeLiteralToMethodName, + string templateTypeName) + : this(executeMethodName, writeMethodName, writeLiteralMethodName) + { + WriteToMethodName = writeToMethodName; + WriteLiteralToMethodName = writeLiteralToMethodName; + TemplateTypeName = templateTypeName; + } + + public GeneratedClassContext(string executeMethodName, + string writeMethodName, + string writeLiteralMethodName, + string writeToMethodName, + string writeLiteralToMethodName, + string templateTypeName, + string defineSectionMethodName) + : this(executeMethodName, writeMethodName, writeLiteralMethodName, writeToMethodName, writeLiteralToMethodName, templateTypeName) + { + DefineSectionMethodName = defineSectionMethodName; + } + + public GeneratedClassContext(string executeMethodName, + string writeMethodName, + string writeLiteralMethodName, + string writeToMethodName, + string writeLiteralToMethodName, + string templateTypeName, + string defineSectionMethodName, + string beginContextMethodName, + string endContextMethodName) + : this(executeMethodName, writeMethodName, writeLiteralMethodName, writeToMethodName, writeLiteralToMethodName, templateTypeName, defineSectionMethodName) + { + BeginContextMethodName = beginContextMethodName; + EndContextMethodName = endContextMethodName; + } + + public string WriteMethodName { get; private set; } + public string WriteLiteralMethodName { get; private set; } + public string WriteToMethodName { get; private set; } + public string WriteLiteralToMethodName { get; private set; } + public string ExecuteMethodName { get; private set; } + + // Optional Items + public string BeginContextMethodName { get; set; } + public string EndContextMethodName { get; set; } + public string LayoutPropertyName { get; set; } + public string DefineSectionMethodName { get; set; } + public string TemplateTypeName { get; set; } + public string WriteAttributeMethodName { get; set; } + public string WriteAttributeToMethodName { get; set; } + + [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings", Justification = "Property is not a URL property")] + public string ResolveUrlMethodName { get; set; } + + public bool AllowSections + { + get { return !String.IsNullOrEmpty(DefineSectionMethodName); } + } + + public bool AllowTemplates + { + get { return !String.IsNullOrEmpty(TemplateTypeName); } + } + + public bool SupportsInstrumentation + { + get { return !String.IsNullOrEmpty(BeginContextMethodName) && !String.IsNullOrEmpty(EndContextMethodName); } + } + + public override bool Equals(object obj) + { + if (!(obj is GeneratedClassContext)) + { + return false; + } + GeneratedClassContext other = (GeneratedClassContext)obj; + return String.Equals(DefineSectionMethodName, other.DefineSectionMethodName, StringComparison.Ordinal) && + String.Equals(WriteMethodName, other.WriteMethodName, StringComparison.Ordinal) && + String.Equals(WriteLiteralMethodName, other.WriteLiteralMethodName, StringComparison.Ordinal) && + String.Equals(WriteToMethodName, other.WriteToMethodName, StringComparison.Ordinal) && + String.Equals(WriteLiteralToMethodName, other.WriteLiteralToMethodName, StringComparison.Ordinal) && + String.Equals(ExecuteMethodName, other.ExecuteMethodName, StringComparison.Ordinal) && + String.Equals(TemplateTypeName, other.TemplateTypeName, StringComparison.Ordinal) && + String.Equals(BeginContextMethodName, other.BeginContextMethodName, StringComparison.Ordinal) && + String.Equals(EndContextMethodName, other.EndContextMethodName, StringComparison.Ordinal); + } + + public override int GetHashCode() + { + // TODO: Use HashCodeCombiner + return DefineSectionMethodName.GetHashCode() ^ + WriteMethodName.GetHashCode() ^ + WriteLiteralMethodName.GetHashCode() ^ + WriteToMethodName.GetHashCode() ^ + WriteLiteralToMethodName.GetHashCode() ^ + ExecuteMethodName.GetHashCode() ^ + TemplateTypeName.GetHashCode() ^ + BeginContextMethodName.GetHashCode() ^ + EndContextMethodName.GetHashCode(); + } + + public static bool operator ==(GeneratedClassContext left, GeneratedClassContext right) + { + return left.Equals(right); + } + + public static bool operator !=(GeneratedClassContext left, GeneratedClassContext right) + { + return !left.Equals(right); + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Generator/GeneratedCodeMapping.cs b/src/Microsoft.AspNet.Razor/Generator/GeneratedCodeMapping.cs new file mode 100644 index 0000000000..8dacffcf58 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/GeneratedCodeMapping.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Globalization; +using Microsoft.Internal.Web.Utils; + +namespace Microsoft.AspNet.Razor.Generator +{ + public struct GeneratedCodeMapping + { + public GeneratedCodeMapping(int startLine, int startColumn, int startGeneratedColumn, int codeLength) + : this(null, startLine, startColumn, startGeneratedColumn, codeLength) + { + } + + public GeneratedCodeMapping(int startOffset, int startLine, int startColumn, int startGeneratedColumn, int codeLength) + : this((int?)startOffset, startLine, startColumn, startGeneratedColumn, codeLength) + { + } + + private GeneratedCodeMapping(int? startOffset, int startLine, int startColumn, int startGeneratedColumn, int codeLength) + : this() + { + if (startLine < 0) + { + throw new ArgumentOutOfRangeException("startLine", String.Format(CultureInfo.CurrentCulture, CommonResources.Argument_Must_Be_GreaterThanOrEqualTo, "startLine", "0")); + } + if (startColumn < 0) + { + throw new ArgumentOutOfRangeException("startColumn", String.Format(CultureInfo.CurrentCulture, CommonResources.Argument_Must_Be_GreaterThanOrEqualTo, "startColumn", "0")); + } + if (startGeneratedColumn < 0) + { + throw new ArgumentOutOfRangeException("startGeneratedColumn", String.Format(CultureInfo.CurrentCulture, CommonResources.Argument_Must_Be_GreaterThanOrEqualTo, "startGeneratedColumn", "0")); + } + if (codeLength < 0) + { + throw new ArgumentOutOfRangeException("codeLength", String.Format(CultureInfo.CurrentCulture, CommonResources.Argument_Must_Be_GreaterThanOrEqualTo, "codeLength", "0")); + } + + StartOffset = startOffset; + StartLine = startLine; + StartColumn = startColumn; + StartGeneratedColumn = startGeneratedColumn; + CodeLength = codeLength; + } + + public int? StartOffset { get; set; } + public int CodeLength { get; set; } + public int StartColumn { get; set; } + public int StartGeneratedColumn { get; set; } + public int StartLine { get; set; } + + public override bool Equals(object obj) + { + if (!(obj is GeneratedCodeMapping)) + { + return false; + } + GeneratedCodeMapping other = (GeneratedCodeMapping)obj; + return CodeLength == other.CodeLength && + StartColumn == other.StartColumn && + StartGeneratedColumn == other.StartGeneratedColumn && + StartLine == other.StartLine && + // Null means it matches the other no matter what. + (StartOffset == null || other.StartOffset == null || StartOffset.Equals(other.StartOffset)); + } + + public override string ToString() + { + return String.Format( + CultureInfo.CurrentCulture, + "({0}, {1}, {2}) -> (?, {3}) [{4}]", + StartOffset == null ? "?" : StartOffset.Value.ToString(CultureInfo.CurrentCulture), + StartLine, + StartColumn, + StartGeneratedColumn, + CodeLength); + } + + public override int GetHashCode() + { + return HashCodeCombiner.Start() + .Add(CodeLength) + .Add(StartColumn) + .Add(StartGeneratedColumn) + .Add(StartLine) + .Add(StartOffset) + .CombinedHash; + } + + public static bool operator ==(GeneratedCodeMapping left, GeneratedCodeMapping right) + { + return left.Equals(right); + } + + public static bool operator !=(GeneratedCodeMapping left, GeneratedCodeMapping right) + { + return !left.Equals(right); + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Generator/HelperCodeGenerator.cs b/src/Microsoft.AspNet.Razor/Generator/HelperCodeGenerator.cs new file mode 100644 index 0000000000..754944beb2 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/HelperCodeGenerator.cs @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.CodeDom; +using System.Globalization; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Text; +using Microsoft.Internal.Web.Utils; +using System; + +namespace Microsoft.AspNet.Razor.Generator +{ + public class HelperCodeGenerator : BlockCodeGenerator + { + private const string HelperWriterName = "__razor_helper_writer"; + + private CodeWriter _writer; + private string _oldWriter; + private IDisposable _statementCollectorToken; + + public HelperCodeGenerator(LocationTagged signature, bool headerComplete) + { + Signature = signature; + HeaderComplete = headerComplete; + } + + public LocationTagged Signature { get; private set; } + public LocationTagged Footer { get; set; } + public bool HeaderComplete { get; private set; } + + public override void GenerateStartBlockCode(Block target, CodeGeneratorContext context) + { + _writer = context.CreateCodeWriter(); + + string prefix = context.BuildCodeString( + cw => cw.WriteHelperHeaderPrefix(context.Host.GeneratedClassContext.TemplateTypeName, context.Host.StaticHelpers)); + + _writer.WriteLinePragma( + context.GenerateLinePragma(Signature.Location, prefix.Length, Signature.Value.Length)); + _writer.WriteSnippet(prefix); + _writer.WriteSnippet(Signature); + if (HeaderComplete) + { + _writer.WriteHelperHeaderSuffix(context.Host.GeneratedClassContext.TemplateTypeName); + } + _writer.WriteLinePragma(null); + if (HeaderComplete) + { + _writer.WriteReturn(); + _writer.WriteStartConstructor(context.Host.GeneratedClassContext.TemplateTypeName); + _writer.WriteStartLambdaDelegate(HelperWriterName); + } + + _statementCollectorToken = context.ChangeStatementCollector(AddStatementToHelper); + _oldWriter = context.TargetWriterName; + context.TargetWriterName = HelperWriterName; + } + + public override void GenerateEndBlockCode(Block target, CodeGeneratorContext context) + { + _statementCollectorToken.Dispose(); + if (HeaderComplete) + { + _writer.WriteEndLambdaDelegate(); + _writer.WriteEndConstructor(); + _writer.WriteEndStatement(); + } + if (Footer != null && !String.IsNullOrEmpty(Footer.Value)) + { + _writer.WriteLinePragma( + context.GenerateLinePragma(Footer.Location, 0, Footer.Value.Length)); + _writer.WriteSnippet(Footer); + _writer.WriteLinePragma(); + } + _writer.WriteHelperTrailer(); + + context.GeneratedClass.Members.Add(new CodeSnippetTypeMember(_writer.Content)); + context.TargetWriterName = _oldWriter; + } + + public override bool Equals(object obj) + { + HelperCodeGenerator other = obj as HelperCodeGenerator; + return other != null && + base.Equals(other) && + HeaderComplete == other.HeaderComplete && + Equals(Signature, other.Signature); + } + + public override int GetHashCode() + { + return HashCodeCombiner.Start() + .Add(base.GetHashCode()) + .Add(Signature) + .CombinedHash; + } + + public override string ToString() + { + return "Helper:" + Signature.ToString("F", CultureInfo.CurrentCulture) + ";" + (HeaderComplete ? "C" : "I"); + } + + private void AddStatementToHelper(string statement, CodeLinePragma pragma) + { + if (pragma != null) + { + _writer.WriteLinePragma(pragma); + } + _writer.WriteSnippet(statement); + _writer.InnerWriter.WriteLine(); // CodeDOM normally inserts an extra line so we need to do so here. + if (pragma != null) + { + _writer.WriteLinePragma(); + } + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Generator/HybridCodeGenerator.cs b/src/Microsoft.AspNet.Razor/Generator/HybridCodeGenerator.cs new file mode 100644 index 0000000000..9c17c6fa46 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/HybridCodeGenerator.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Generator +{ + public abstract class HybridCodeGenerator : ISpanCodeGenerator, IBlockCodeGenerator + { + public virtual void GenerateStartBlockCode(Block target, CodeGeneratorContext context) + { + } + + public virtual void GenerateEndBlockCode(Block target, CodeGeneratorContext context) + { + } + + public virtual void GenerateCode(Span target, CodeGeneratorContext context) + { + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Generator/IBlockCodeGenerator.cs b/src/Microsoft.AspNet.Razor/Generator/IBlockCodeGenerator.cs new file mode 100644 index 0000000000..b3090f462a --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/IBlockCodeGenerator.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Generator +{ + public interface IBlockCodeGenerator + { + void GenerateStartBlockCode(Block target, CodeGeneratorContext context); + void GenerateEndBlockCode(Block target, CodeGeneratorContext context); + } +} diff --git a/src/Microsoft.AspNet.Razor/Generator/ISpanCodeGenerator.cs b/src/Microsoft.AspNet.Razor/Generator/ISpanCodeGenerator.cs new file mode 100644 index 0000000000..4889181bb3 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/ISpanCodeGenerator.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Generator +{ + public interface ISpanCodeGenerator + { + void GenerateCode(Span target, CodeGeneratorContext context); + } +} diff --git a/src/Microsoft.AspNet.Razor/Generator/LiteralAttributeCodeGenerator.cs b/src/Microsoft.AspNet.Razor/Generator/LiteralAttributeCodeGenerator.cs new file mode 100644 index 0000000000..caa2d2b08e --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/LiteralAttributeCodeGenerator.cs @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Globalization; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Text; +using Microsoft.Internal.Web.Utils; +using System; + +namespace Microsoft.AspNet.Razor.Generator +{ + public class LiteralAttributeCodeGenerator : SpanCodeGenerator + { + public LiteralAttributeCodeGenerator(LocationTagged prefix, LocationTagged valueGenerator) + { + Prefix = prefix; + ValueGenerator = valueGenerator; + } + + public LiteralAttributeCodeGenerator(LocationTagged prefix, LocationTagged value) + { + Prefix = prefix; + Value = value; + } + + public LocationTagged Prefix { get; private set; } + public LocationTagged Value { get; private set; } + public LocationTagged ValueGenerator { get; private set; } + + public override void GenerateCode(Span target, CodeGeneratorContext context) + { + if (context.Host.DesignTimeMode) + { + return; + } + ExpressionRenderingMode oldMode = context.ExpressionRenderingMode; + context.BufferStatementFragment(context.BuildCodeString(cw => + { + cw.WriteParameterSeparator(); + cw.WriteStartMethodInvoke("Tuple.Create"); + cw.WriteLocationTaggedString(Prefix); + cw.WriteParameterSeparator(); + if (ValueGenerator != null) + { + cw.WriteStartMethodInvoke("Tuple.Create", "System.Object", "System.Int32"); + context.ExpressionRenderingMode = ExpressionRenderingMode.InjectCode; + } + else + { + cw.WriteLocationTaggedString(Value); + cw.WriteParameterSeparator(); + // literal: true - This attribute value is a literal value + cw.WriteBooleanLiteral(true); + cw.WriteEndMethodInvoke(); + + // In VB, we need a line continuation + cw.WriteLineContinuation(); + } + })); + if (ValueGenerator != null) + { + ValueGenerator.Value.GenerateCode(target, context); + context.FlushBufferedStatement(); + context.ExpressionRenderingMode = oldMode; + context.AddStatement(context.BuildCodeString(cw => + { + cw.WriteParameterSeparator(); + cw.WriteSnippet(ValueGenerator.Location.AbsoluteIndex.ToString(CultureInfo.CurrentCulture)); + cw.WriteEndMethodInvoke(); + cw.WriteParameterSeparator(); + // literal: false - This attribute value is not a literal value, it is dynamically generated + cw.WriteBooleanLiteral(false); + cw.WriteEndMethodInvoke(); + + // In VB, we need a line continuation + cw.WriteLineContinuation(); + })); + } + else + { + context.FlushBufferedStatement(); + } + } + + public override string ToString() + { + if (ValueGenerator == null) + { + return String.Format(CultureInfo.CurrentCulture, "LitAttr:{0:F},{1:F}", Prefix, Value); + } + else + { + return String.Format(CultureInfo.CurrentCulture, "LitAttr:{0:F},", Prefix, ValueGenerator); + } + } + + public override bool Equals(object obj) + { + LiteralAttributeCodeGenerator other = obj as LiteralAttributeCodeGenerator; + return other != null && + Equals(other.Prefix, Prefix) && + Equals(other.Value, Value) && + Equals(other.ValueGenerator, ValueGenerator); + } + + public override int GetHashCode() + { + return HashCodeCombiner.Start() + .Add(Prefix) + .Add(Value) + .Add(ValueGenerator) + .CombinedHash; + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Generator/MarkupCodeGenerator.cs b/src/Microsoft.AspNet.Razor/Generator/MarkupCodeGenerator.cs new file mode 100644 index 0000000000..83a5d952de --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/MarkupCodeGenerator.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Generator +{ + public class MarkupCodeGenerator : SpanCodeGenerator + { + public override void GenerateCode(Span target, CodeGeneratorContext context) + { + if (!context.Host.DesignTimeMode && String.IsNullOrEmpty(target.Content)) + { + return; + } + + if (context.Host.EnableInstrumentation) + { + context.AddContextCall(target, context.Host.GeneratedClassContext.BeginContextMethodName, isLiteral: true); + } + + if (!String.IsNullOrEmpty(target.Content) && !context.Host.DesignTimeMode) + { + string code = context.BuildCodeString(cw => + { + if (!String.IsNullOrEmpty(context.TargetWriterName)) + { + cw.WriteStartMethodInvoke(context.Host.GeneratedClassContext.WriteLiteralToMethodName); + cw.WriteSnippet(context.TargetWriterName); + cw.WriteParameterSeparator(); + } + else + { + cw.WriteStartMethodInvoke(context.Host.GeneratedClassContext.WriteLiteralMethodName); + } + cw.WriteStringLiteral(target.Content); + cw.WriteEndMethodInvoke(); + cw.WriteEndStatement(); + }); + context.AddStatement(code); + } + + if (context.Host.EnableInstrumentation) + { + context.AddContextCall(target, context.Host.GeneratedClassContext.EndContextMethodName, isLiteral: true); + } + } + + public override string ToString() + { + return "Markup"; + } + + public override bool Equals(object obj) + { + return obj is MarkupCodeGenerator; + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Generator/RazorCodeGenerator.cs b/src/Microsoft.AspNet.Razor/Generator/RazorCodeGenerator.cs new file mode 100644 index 0000000000..69216e7ead --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/RazorCodeGenerator.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.CodeDom; +using System.Linq; +using Microsoft.AspNet.Razor.Parser; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.Internal.Web.Utils; + +namespace Microsoft.AspNet.Razor.Generator +{ + public abstract class RazorCodeGenerator : ParserVisitor + { + private CodeGeneratorContext _context; + + protected RazorCodeGenerator(string className, string rootNamespaceName, string sourceFileName, RazorEngineHost host) + { + if (String.IsNullOrEmpty(className)) + { + throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "className"); + } + if (rootNamespaceName == null) + { + throw new ArgumentNullException("rootNamespaceName"); + } + if (host == null) + { + throw new ArgumentNullException("host"); + } + + ClassName = className; + RootNamespaceName = rootNamespaceName; + SourceFileName = sourceFileName; + GenerateLinePragmas = String.IsNullOrEmpty(SourceFileName) ? false : true; + Host = host; + } + + // Data pulled from constructor + public string ClassName { get; private set; } + public string RootNamespaceName { get; private set; } + public string SourceFileName { get; private set; } + public RazorEngineHost Host { get; private set; } + + // Generation settings + public bool GenerateLinePragmas { get; set; } + public bool DesignTimeMode { get; set; } + + public CodeGeneratorContext Context + { + get + { + EnsureContextInitialized(); + return _context; + } + } + + internal virtual Func CodeWriterFactory + { + get { return null; } + } + + public override void VisitStartBlock(Block block) + { + block.CodeGenerator.GenerateStartBlockCode(block, Context); + } + + public override void VisitEndBlock(Block block) + { + block.CodeGenerator.GenerateEndBlockCode(block, Context); + } + + public override void VisitSpan(Span span) + { + span.CodeGenerator.GenerateCode(span, Context); + } + + public override void OnComplete() + { + Context.FlushBufferedStatement(); + } + + private void EnsureContextInitialized() + { + if (_context == null) + { + _context = CodeGeneratorContext.Create(Host, CodeWriterFactory, ClassName, RootNamespaceName, SourceFileName, GenerateLinePragmas); + Initialize(_context); + } + } + + protected virtual void Initialize(CodeGeneratorContext context) + { + context.Namespace.Imports.AddRange(Host.NamespaceImports.Select(s => new CodeNamespaceImport(s)).ToArray()); + + if (!String.IsNullOrEmpty(Host.DefaultBaseClass)) + { + context.GeneratedClass.BaseTypes.Add(new CodeTypeReference(Host.DefaultBaseClass)); + } + + // Dev10 Bug 937438: Generate explicit Parameter-less constructor on Razor generated class + context.GeneratedClass.Members.Add(new CodeConstructor() { Attributes = MemberAttributes.Public }); + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Generator/RazorCommentCodeGenerator.cs b/src/Microsoft.AspNet.Razor/Generator/RazorCommentCodeGenerator.cs new file mode 100644 index 0000000000..3d8cc6f8a4 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/RazorCommentCodeGenerator.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Generator +{ + public class RazorCommentCodeGenerator : BlockCodeGenerator + { + public override void GenerateStartBlockCode(Block target, CodeGeneratorContext context) + { + // Flush the buffered statement since we're interrupting it with a comment. + if (!String.IsNullOrEmpty(context.CurrentBufferedStatement)) + { + context.MarkEndOfGeneratedCode(); + context.BufferStatementFragment(context.BuildCodeString(cw => cw.WriteLineContinuation())); + } + context.FlushBufferedStatement(); + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Generator/RazorDirectiveAttributeCodeGenerator.cs b/src/Microsoft.AspNet.Razor/Generator/RazorDirectiveAttributeCodeGenerator.cs new file mode 100644 index 0000000000..abfa12f2a5 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/RazorDirectiveAttributeCodeGenerator.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.CodeDom; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.Internal.Web.Utils; + +namespace Microsoft.AspNet.Razor.Generator +{ + public class RazorDirectiveAttributeCodeGenerator : SpanCodeGenerator + { + public RazorDirectiveAttributeCodeGenerator(string name, string value) + { + if (String.IsNullOrEmpty(name)) + { + throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "name"); + } + Name = name; + Value = value ?? String.Empty; // Coerce to empty string if it was null. + } + + public string Name { get; private set; } + + public string Value { get; private set; } + + public override void GenerateCode(Span target, CodeGeneratorContext context) + { + var attributeType = new CodeTypeReference(typeof(RazorDirectiveAttribute)); + var attributeDeclaration = new CodeAttributeDeclaration( + attributeType, + new CodeAttributeArgument(new CodePrimitiveExpression(Name)), + new CodeAttributeArgument(new CodePrimitiveExpression(Value))); + context.GeneratedClass.CustomAttributes.Add(attributeDeclaration); + } + + public override string ToString() + { + return "Directive: " + Name + ", Value: " + Value; + } + + public override bool Equals(object obj) + { + RazorDirectiveAttributeCodeGenerator other = obj as RazorDirectiveAttributeCodeGenerator; + return other != null && + Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase) && + Value.Equals(other.Value, StringComparison.OrdinalIgnoreCase); + } + + public override int GetHashCode() + { + return Tuple.Create(Name.ToUpperInvariant(), Value.ToUpperInvariant()) + .GetHashCode(); + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Generator/ResolveUrlCodeGenerator.cs b/src/Microsoft.AspNet.Razor/Generator/ResolveUrlCodeGenerator.cs new file mode 100644 index 0000000000..97e89c5393 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/ResolveUrlCodeGenerator.cs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Generator +{ + public class ResolveUrlCodeGenerator : SpanCodeGenerator + { + public override void GenerateCode(Span target, CodeGeneratorContext context) + { + // Check if the host supports it + if (String.IsNullOrEmpty(context.Host.GeneratedClassContext.ResolveUrlMethodName)) + { + // Nope, just use the default MarkupCodeGenerator behavior + new MarkupCodeGenerator().GenerateCode(target, context); + return; + } + + if (!context.Host.DesignTimeMode && String.IsNullOrEmpty(target.Content)) + { + return; + } + + if (context.Host.EnableInstrumentation && context.ExpressionRenderingMode == ExpressionRenderingMode.WriteToOutput) + { + // Add a non-literal context call (non-literal because the expanded URL will not match the source character-by-character) + context.AddContextCall(target, context.Host.GeneratedClassContext.BeginContextMethodName, isLiteral: false); + } + + if (!String.IsNullOrEmpty(target.Content) && !context.Host.DesignTimeMode) + { + string code = context.BuildCodeString(cw => + { + if (context.ExpressionRenderingMode == ExpressionRenderingMode.WriteToOutput) + { + if (!String.IsNullOrEmpty(context.TargetWriterName)) + { + cw.WriteStartMethodInvoke(context.Host.GeneratedClassContext.WriteLiteralToMethodName); + cw.WriteSnippet(context.TargetWriterName); + cw.WriteParameterSeparator(); + } + else + { + cw.WriteStartMethodInvoke(context.Host.GeneratedClassContext.WriteLiteralMethodName); + } + } + cw.WriteStartMethodInvoke(context.Host.GeneratedClassContext.ResolveUrlMethodName); + cw.WriteStringLiteral(target.Content); + cw.WriteEndMethodInvoke(); + + if (context.ExpressionRenderingMode == ExpressionRenderingMode.WriteToOutput) + { + cw.WriteEndMethodInvoke(); + cw.WriteEndStatement(); + } + else + { + cw.WriteLineContinuation(); + } + }); + if (context.ExpressionRenderingMode == ExpressionRenderingMode.WriteToOutput) + { + context.AddStatement(code); + } + else + { + context.BufferStatementFragment(code); + } + } + + if (context.Host.EnableInstrumentation && context.ExpressionRenderingMode == ExpressionRenderingMode.WriteToOutput) + { + context.AddContextCall(target, context.Host.GeneratedClassContext.EndContextMethodName, isLiteral: false); + } + } + + public override string ToString() + { + return "VirtualPath"; + } + + public override bool Equals(object obj) + { + return obj is ResolveUrlCodeGenerator; + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Generator/SectionCodeGenerator.cs b/src/Microsoft.AspNet.Razor/Generator/SectionCodeGenerator.cs new file mode 100644 index 0000000000..e005aa5b8b --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/SectionCodeGenerator.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.Internal.Web.Utils; +using System; + +namespace Microsoft.AspNet.Razor.Generator +{ + public class SectionCodeGenerator : BlockCodeGenerator + { + public SectionCodeGenerator(string sectionName) + { + SectionName = sectionName; + } + + public string SectionName { get; private set; } + + public override void GenerateStartBlockCode(Block target, CodeGeneratorContext context) + { + string startBlock = context.BuildCodeString(cw => + { + cw.WriteStartMethodInvoke(context.Host.GeneratedClassContext.DefineSectionMethodName); + cw.WriteStringLiteral(SectionName); + cw.WriteParameterSeparator(); + cw.WriteStartLambdaDelegate(); + }); + context.AddStatement(startBlock); + } + + public override void GenerateEndBlockCode(Block target, CodeGeneratorContext context) + { + string startBlock = context.BuildCodeString(cw => + { + cw.WriteEndLambdaDelegate(); + cw.WriteEndMethodInvoke(); + cw.WriteEndStatement(); + }); + context.AddStatement(startBlock); + } + + public override bool Equals(object obj) + { + SectionCodeGenerator other = obj as SectionCodeGenerator; + return other != null && + base.Equals(other) && + String.Equals(SectionName, other.SectionName, StringComparison.Ordinal); + } + + public override int GetHashCode() + { + return HashCodeCombiner.Start() + .Add(base.GetHashCode()) + .Add(SectionName) + .CombinedHash; + } + + public override string ToString() + { + return "Section:" + SectionName; + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Generator/SetBaseTypeCodeGenerator.cs b/src/Microsoft.AspNet.Razor/Generator/SetBaseTypeCodeGenerator.cs new file mode 100644 index 0000000000..9b45edeacf --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/SetBaseTypeCodeGenerator.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.CodeDom; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Generator +{ + public class SetBaseTypeCodeGenerator : SpanCodeGenerator + { + public SetBaseTypeCodeGenerator(string baseType) + { + BaseType = baseType; + } + + public string BaseType { get; private set; } + + public override void GenerateCode(Span target, CodeGeneratorContext context) + { + context.GeneratedClass.BaseTypes.Clear(); + context.GeneratedClass.BaseTypes.Add(new CodeTypeReference(ResolveType(context, BaseType.Trim()))); + + if (context.Host.DesignTimeMode) + { + int generatedCodeStart = 0; + string code = context.BuildCodeString(cw => + { + generatedCodeStart = cw.WriteVariableDeclaration(target.Content, "__inheritsHelper", null); + cw.WriteEndStatement(); + }); + + int paddingCharCount; + + CodeSnippetStatement stmt = new CodeSnippetStatement( + CodeGeneratorPaddingHelper.Pad(context.Host, code, target, generatedCodeStart, out paddingCharCount)) + { + LinePragma = context.GenerateLinePragma(target, generatedCodeStart + paddingCharCount) + }; + context.AddDesignTimeHelperStatement(stmt); + } + } + + protected virtual string ResolveType(CodeGeneratorContext context, string baseType) + { + return baseType; + } + + public override string ToString() + { + return "Base:" + BaseType; + } + + public override bool Equals(object obj) + { + SetBaseTypeCodeGenerator other = obj as SetBaseTypeCodeGenerator; + return other != null && + String.Equals(BaseType, other.BaseType, StringComparison.Ordinal); + } + + public override int GetHashCode() + { + return BaseType.GetHashCode(); + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Generator/SetLayoutCodeGenerator.cs b/src/Microsoft.AspNet.Razor/Generator/SetLayoutCodeGenerator.cs new file mode 100644 index 0000000000..3932bfbf73 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/SetLayoutCodeGenerator.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.CodeDom; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Generator +{ + public class SetLayoutCodeGenerator : SpanCodeGenerator + { + public SetLayoutCodeGenerator(string layoutPath) + { + LayoutPath = layoutPath; + } + + public string LayoutPath { get; set; } + + public override void GenerateCode(Span target, CodeGeneratorContext context) + { + if (!context.Host.DesignTimeMode && !String.IsNullOrEmpty(context.Host.GeneratedClassContext.LayoutPropertyName)) + { + context.TargetMethod.Statements.Add( + new CodeAssignStatement( + new CodePropertyReferenceExpression(null, context.Host.GeneratedClassContext.LayoutPropertyName), + new CodePrimitiveExpression(LayoutPath))); + } + } + + public override string ToString() + { + return "Layout: " + LayoutPath; + } + + public override bool Equals(object obj) + { + SetLayoutCodeGenerator other = obj as SetLayoutCodeGenerator; + return other != null && String.Equals(other.LayoutPath, LayoutPath, StringComparison.Ordinal); + } + + public override int GetHashCode() + { + return LayoutPath.GetHashCode(); + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Generator/SetVBOptionCodeGenerator.cs b/src/Microsoft.AspNet.Razor/Generator/SetVBOptionCodeGenerator.cs new file mode 100644 index 0000000000..b707f4d3de --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/SetVBOptionCodeGenerator.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Generator +{ + public class SetVBOptionCodeGenerator : SpanCodeGenerator + { + public static readonly string StrictCodeDomOptionName = "AllowLateBound"; + public static readonly string ExplicitCodeDomOptionName = "RequireVariableDeclaration"; + + public SetVBOptionCodeGenerator(string optionName, bool value) + { + OptionName = optionName; + Value = value; + } + + // CodeDOM Option Name, which is NOT the same as the VB Option Name + public string OptionName { get; private set; } + public bool Value { get; private set; } + + public static SetVBOptionCodeGenerator Strict(bool onOffValue) + { + // Strict On = AllowLateBound Off + return new SetVBOptionCodeGenerator(StrictCodeDomOptionName, !onOffValue); + } + + public static SetVBOptionCodeGenerator Explicit(bool onOffValue) + { + return new SetVBOptionCodeGenerator(ExplicitCodeDomOptionName, onOffValue); + } + + public override void GenerateCode(Span target, CodeGeneratorContext context) + { + context.CompileUnit.UserData[OptionName] = Value; + } + + public override string ToString() + { + return "Option:" + OptionName + "=" + Value; + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Generator/SpanCodeGenerator.cs b/src/Microsoft.AspNet.Razor/Generator/SpanCodeGenerator.cs new file mode 100644 index 0000000000..ce6a1123e5 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/SpanCodeGenerator.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Generator +{ + public abstract class SpanCodeGenerator : ISpanCodeGenerator + { + [SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification = "This class has no instance state")] + public static readonly ISpanCodeGenerator Null = new NullSpanCodeGenerator(); + + public virtual void GenerateCode(Span target, CodeGeneratorContext context) + { + } + + public override bool Equals(object obj) + { + return (obj as ISpanCodeGenerator) != null; + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + + private class NullSpanCodeGenerator : ISpanCodeGenerator + { + public void GenerateCode(Span target, CodeGeneratorContext context) + { + } + + public override string ToString() + { + return "None"; + } + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Generator/StatementCodeGenerator.cs b/src/Microsoft.AspNet.Razor/Generator/StatementCodeGenerator.cs new file mode 100644 index 0000000000..cae0f670de --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/StatementCodeGenerator.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using Microsoft.AspNet.Razor.Parser; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Generator +{ + public class StatementCodeGenerator : SpanCodeGenerator + { + public override void GenerateCode(Span target, CodeGeneratorContext context) + { + context.FlushBufferedStatement(); + + string generatedCode = context.BuildCodeString(cw => + { + cw.WriteSnippet(target.Content); + }); + + int startGeneratedCode = target.Start.CharacterIndex; + int paddingCharCount; + generatedCode = CodeGeneratorPaddingHelper.PadStatement(context.Host, generatedCode, target, ref startGeneratedCode, out paddingCharCount); + + context.AddStatement( + generatedCode, + context.GenerateLinePragma(target, paddingCharCount)); + } + + public override string ToString() + { + return "Stmt"; + } + + public override bool Equals(object obj) + { + return obj is StatementCodeGenerator; + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Generator/TemplateBlockCodeGenerator.cs b/src/Microsoft.AspNet.Razor/Generator/TemplateBlockCodeGenerator.cs new file mode 100644 index 0000000000..7b06b17aea --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/TemplateBlockCodeGenerator.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Generator +{ + public class TemplateBlockCodeGenerator : BlockCodeGenerator + { + private const string TemplateWriterName = "__razor_template_writer"; + private const string ItemParameterName = "item"; + + private string _oldTargetWriter; + + public override void GenerateStartBlockCode(Block target, CodeGeneratorContext context) + { + string generatedCode = context.BuildCodeString(cw => + { + cw.WriteStartLambdaExpression(ItemParameterName); + cw.WriteStartConstructor(context.Host.GeneratedClassContext.TemplateTypeName); + cw.WriteStartLambdaDelegate(TemplateWriterName); + }); + + context.MarkEndOfGeneratedCode(); + context.BufferStatementFragment(generatedCode); + context.FlushBufferedStatement(); + + _oldTargetWriter = context.TargetWriterName; + context.TargetWriterName = TemplateWriterName; + } + + public override void GenerateEndBlockCode(Block target, CodeGeneratorContext context) + { + string generatedCode = context.BuildCodeString(cw => + { + cw.WriteEndLambdaDelegate(); + cw.WriteEndConstructor(); + cw.WriteEndLambdaExpression(); + }); + + context.BufferStatementFragment(generatedCode); + context.TargetWriterName = _oldTargetWriter; + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Generator/TypeMemberCodeGenerator.cs b/src/Microsoft.AspNet.Razor/Generator/TypeMemberCodeGenerator.cs new file mode 100644 index 0000000000..6ba6312b36 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/TypeMemberCodeGenerator.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.CodeDom; +using System.Diagnostics.Contracts; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Generator +{ + public class TypeMemberCodeGenerator : SpanCodeGenerator + { + public override void GenerateCode(Span target, CodeGeneratorContext context) + { + string generatedCode = context.BuildCodeString(cw => + { + cw.WriteSnippet(target.Content); + }); + + int paddingCharCount; + string paddedCode = CodeGeneratorPaddingHelper.Pad(context.Host, generatedCode, target, out paddingCharCount); + + Contract.Assert(paddingCharCount > 0); + + context.GeneratedClass.Members.Add( + new CodeSnippetTypeMember(paddedCode) + { + LinePragma = context.GenerateLinePragma(target, paddingCharCount) + }); + } + + public override string ToString() + { + return "TypeMember"; + } + + public override bool Equals(object obj) + { + return obj is TypeMemberCodeGenerator; + } + + // C# complains at us if we don't provide an implementation, even one like this + public override int GetHashCode() + { + return base.GetHashCode(); + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Generator/VBCodeWriter.cs b/src/Microsoft.AspNet.Razor/Generator/VBCodeWriter.cs new file mode 100644 index 0000000000..1b72efed7f --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/VBCodeWriter.cs @@ -0,0 +1,201 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +namespace Microsoft.AspNet.Razor.Generator +{ + internal class VBCodeWriter : BaseCodeWriter + { + public override bool SupportsMidStatementLinePragmas + { + get { return false; } + } + + protected internal override void WriteStartGenerics() + { + InnerWriter.Write("(Of "); + } + + protected internal override void WriteEndGenerics() + { + InnerWriter.Write(")"); + } + + public override void WriteLineContinuation() + { + InnerWriter.Write(" _"); + } + + public override int WriteVariableDeclaration(string type, string name, string value) + { + InnerWriter.Write("Dim "); + InnerWriter.Write(name); + InnerWriter.Write(" As "); + int typePos = InnerWriter.GetStringBuilder().Length; + InnerWriter.Write(type); + if (!String.IsNullOrEmpty(value)) + { + InnerWriter.Write(" = "); + InnerWriter.Write(value); + } + else + { + InnerWriter.Write(" = Nothing"); + } + return typePos; + } + + public override void WriteStringLiteral(string literal) + { + bool inQuotes = true; + InnerWriter.Write("\""); + for (int i = 0; i < literal.Length; i++) + { + switch (literal[i]) + { + case '\t': + case '\n': + case '\r': + case '\0': + case '\u2028': + case '\u2029': + // Exit quotes + EnsureOutOfQuotes(ref inQuotes); + + // Write concat character + InnerWriter.Write("&"); + + // Write character literal + WriteCharLiteral(literal[i]); + break; + case '"': + case '“': + case '”': + case (char)0xff02: + EnsureInQuotes(ref inQuotes); + InnerWriter.Write(literal[i]); + InnerWriter.Write(literal[i]); + break; + default: + EnsureInQuotes(ref inQuotes); + InnerWriter.Write(literal[i]); + break; + } + if (i > 0 && (i % 80) == 0) + { + if ((Char.IsHighSurrogate(literal[i]) && (i < (literal.Length - 1))) && Char.IsLowSurrogate(literal[i + 1])) + { + InnerWriter.Write(literal[++i]); + } + if (inQuotes) + { + InnerWriter.Write("\""); + } + inQuotes = true; + InnerWriter.Write("& _ "); + InnerWriter.Write(Environment.NewLine); + InnerWriter.Write('"'); + } + } + EnsureOutOfQuotes(ref inQuotes); + } + + protected internal override void EmitStartLambdaExpression(string[] parameterNames) + { + InnerWriter.Write("Function ("); + WriteCommaSeparatedList(parameterNames, InnerWriter.Write); + InnerWriter.Write(") "); + } + + protected internal override void EmitStartConstructor(string typeName) + { + InnerWriter.Write("New "); + InnerWriter.Write(typeName); + InnerWriter.Write("("); + } + + protected internal override void EmitStartLambdaDelegate(string[] parameterNames) + { + InnerWriter.Write("Sub ("); + WriteCommaSeparatedList(parameterNames, InnerWriter.Write); + InnerWriter.WriteLine(")"); + } + + protected internal override void EmitEndLambdaDelegate() + { + InnerWriter.Write("End Sub"); + } + + private void WriteCharLiteral(char literal) + { + InnerWriter.Write("Global.Microsoft.VisualBasic.ChrW("); + InnerWriter.Write((int)literal); + InnerWriter.Write(")"); + } + + private void EnsureInQuotes(ref bool inQuotes) + { + if (!inQuotes) + { + InnerWriter.Write("&\""); + inQuotes = true; + } + } + + private void EnsureOutOfQuotes(ref bool inQuotes) + { + if (inQuotes) + { + InnerWriter.Write("\""); + inQuotes = false; + } + } + + public override void WriteReturn() + { + InnerWriter.Write("Return "); + } + + public override void WriteLinePragma(int? lineNumber, string fileName) + { + InnerWriter.WriteLine(); + if (lineNumber != null) + { + InnerWriter.Write("#ExternalSource(\""); + InnerWriter.Write(fileName); + InnerWriter.Write("\", "); + InnerWriter.Write(lineNumber); + InnerWriter.WriteLine(")"); + } + else + { + InnerWriter.WriteLine("#End ExternalSource"); + } + } + + public override void WriteHelperHeaderPrefix(string templateTypeName, bool isStatic) + { + InnerWriter.Write("Public "); + if (isStatic) + { + InnerWriter.Write("Shared "); + } + InnerWriter.Write("Function "); + } + + public override void WriteHelperHeaderSuffix(string templateTypeName) + { + InnerWriter.Write(" As "); + InnerWriter.WriteLine(templateTypeName); + } + + public override void WriteHelperTrailer() + { + InnerWriter.WriteLine("End Function"); + } + + public override void WriteEndStatement() + { + InnerWriter.WriteLine(); + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Generator/VBRazorCodeGenerator.cs b/src/Microsoft.AspNet.Razor/Generator/VBRazorCodeGenerator.cs new file mode 100644 index 0000000000..71f86ea1ca --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Generator/VBRazorCodeGenerator.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +namespace Microsoft.AspNet.Razor.Generator +{ + public class VBRazorCodeGenerator : RazorCodeGenerator + { + public VBRazorCodeGenerator(string className, string rootNamespaceName, string sourceFileName, RazorEngineHost host) + : base(className, rootNamespaceName, sourceFileName, host) + { + } + + internal override Func CodeWriterFactory + { + get { return () => new VBCodeWriter(); } + } + } +} diff --git a/src/Microsoft.AspNet.Razor/GeneratorResults.cs b/src/Microsoft.AspNet.Razor/GeneratorResults.cs new file mode 100644 index 0000000000..b363e0130d --- /dev/null +++ b/src/Microsoft.AspNet.Razor/GeneratorResults.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System.CodeDom; +using System.Collections.Generic; +using Microsoft.AspNet.Razor.Generator; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor +{ + /// + /// Represents results from code generation (and parsing, since that is a pre-requisite of code generation) + /// + /// + /// Since this inherits from ParserResults, it has all the data from ParserResults, and simply adds code generation data + /// + public class GeneratorResults : ParserResults + { + public GeneratorResults(ParserResults parserResults, + CodeCompileUnit generatedCode, + IDictionary designTimeLineMappings) + : this(parserResults.Document, parserResults.ParserErrors, generatedCode, designTimeLineMappings) + { + } + + public GeneratorResults(Block document, + IList parserErrors, + CodeCompileUnit generatedCode, + IDictionary designTimeLineMappings) + : this(parserErrors.Count == 0, document, parserErrors, generatedCode, designTimeLineMappings) + { + } + + protected GeneratorResults(bool success, + Block document, + IList parserErrors, + CodeCompileUnit generatedCode, + IDictionary designTimeLineMappings) + : base(success, document, parserErrors) + { + GeneratedCode = generatedCode; + DesignTimeLineMappings = designTimeLineMappings; + } + + /// + /// The generated code + /// + public CodeCompileUnit GeneratedCode { get; private set; } + + /// + /// If design-time mode was used in the Code Generator, this will contain the dictionary + /// of design-time generated code mappings + /// + public IDictionary DesignTimeLineMappings { get; private set; } + } +} diff --git a/src/Microsoft.AspNet.Razor/GlobalSuppressions.cs b/src/Microsoft.AspNet.Razor/GlobalSuppressions.cs new file mode 100644 index 0000000000..aac9dd64dc --- /dev/null +++ b/src/Microsoft.AspNet.Razor/GlobalSuppressions.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. +// +// To add a suppression to this file, right-click the message in the +// Error List, point to "Suppress Message(s)", and click +// "In Project Suppression File". +// You do not need to add suppressions to this file manually. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId = "br", Scope = "resource", Target = "System.Web.Razor.Resources.RazorResources.resources", Justification = "Resource is referencing html tag")] +[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.Web.Razor.Tokenizer.Symbols", Justification = "These namespaces are design to group classes by function. They will be reviewed to ensure they remain relevant.")] +[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.Web.Razor.Tokenizer", Justification = "These namespaces are design to group classes by function. They will be reviewed to ensure they remain relevant.")] +[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.Web.Razor.Text", Justification = "These namespaces are design to group classes by function. They will be reviewed to ensure they remain relevant.")] +[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.Web.Razor.Parser", Justification = "These namespaces are design to group classes by function. They will be reviewed to ensure they remain relevant.")] +[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.Web.Razor.Editor", Justification = "These namespaces are design to group classes by function. They will be reviewed to ensure they remain relevant.")] +[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "System.Web.Razor", Justification = "These namespaces are design to group classes by function. They will be reviewed to ensure they remain relevant.")] +[assembly: SuppressMessage("Microsoft.Web.FxCop", "MW1000:UnusedResourceUsageRule", Justification = "There are numerous unused resources due to VB being disabled. This rule will be re-run after VB is restored")] diff --git a/src/Microsoft.AspNet.Razor/Microsoft.AspNet.Razor.csproj b/src/Microsoft.AspNet.Razor/Microsoft.AspNet.Razor.csproj new file mode 100644 index 0000000000..2522b7203b --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Microsoft.AspNet.Razor.csproj @@ -0,0 +1,216 @@ + + + + + Debug + AnyCPU + {E75D8296-3BA6-4E67-AFEB-90FF77460B15} + Library + Properties + Microsoft.AspNet.Razor + Microsoft.AspNet.Razor + v4.5 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + True + True + CommonResources.resx + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + RazorResources.resx + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ResXFileCodeGenerator + CommonResources.Designer.cs + + + ResXFileCodeGenerator + RazorResources.Designer.cs + Designer + + + + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Razor/Parser/BalancingModes.cs b/src/Microsoft.AspNet.Razor/Parser/BalancingModes.cs new file mode 100644 index 0000000000..b3ffa5bafb --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Parser/BalancingModes.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +namespace Microsoft.AspNet.Razor.Parser +{ + [Flags] + public enum BalancingModes + { + None = 0, + BacktrackOnFailure = 1, + NoErrorOnFailure = 2, + AllowCommentsAndTemplates = 4, + AllowEmbeddedTransitions = 8 + } +} diff --git a/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.Directives.cs b/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.Directives.cs new file mode 100644 index 0000000000..ec0943214d --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.Directives.cs @@ -0,0 +1,505 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using Microsoft.AspNet.Razor.Editor; +using Microsoft.AspNet.Razor.Generator; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Resources; +using Microsoft.AspNet.Razor.Text; +using Microsoft.AspNet.Razor.Tokenizer.Symbols; + +namespace Microsoft.AspNet.Razor.Parser +{ + public partial class CSharpCodeParser + { + private void SetupDirectives() + { + MapDirectives(InheritsDirective, SyntaxConstants.CSharp.InheritsKeyword); + MapDirectives(FunctionsDirective, SyntaxConstants.CSharp.FunctionsKeyword); + MapDirectives(SectionDirective, SyntaxConstants.CSharp.SectionKeyword); + MapDirectives(HelperDirective, SyntaxConstants.CSharp.HelperKeyword); + MapDirectives(LayoutDirective, SyntaxConstants.CSharp.LayoutKeyword); + MapDirectives(SessionStateDirective, SyntaxConstants.CSharp.SessionStateKeyword); + } + + protected virtual void LayoutDirective() + { + AssertDirective(SyntaxConstants.CSharp.LayoutKeyword); + AcceptAndMoveNext(); + Context.CurrentBlock.Type = BlockType.Directive; + + // Accept spaces, but not newlines + bool foundSomeWhitespace = At(CSharpSymbolType.WhiteSpace); + AcceptWhile(CSharpSymbolType.WhiteSpace); + Output(SpanKind.MetaCode, foundSomeWhitespace ? AcceptedCharacters.None : AcceptedCharacters.Any); + + // First non-whitespace character starts the Layout Page, then newline ends it + AcceptUntil(CSharpSymbolType.NewLine); + Span.CodeGenerator = new SetLayoutCodeGenerator(Span.GetContent()); + Span.EditHandler.EditorHints = EditorHints.LayoutPage | EditorHints.VirtualPath; + bool foundNewline = Optional(CSharpSymbolType.NewLine); + AddMarkerSymbolIfNecessary(); + Output(SpanKind.MetaCode, foundNewline ? AcceptedCharacters.None : AcceptedCharacters.Any); + } + + protected virtual void SessionStateDirective() + { + AssertDirective(SyntaxConstants.CSharp.SessionStateKeyword); + AcceptAndMoveNext(); + + SessionStateDirectiveCore(); + } + + protected void SessionStateDirectiveCore() + { + SessionStateTypeDirective(RazorResources.ParserEror_SessionDirectiveMissingValue, (key, value) => new RazorDirectiveAttributeCodeGenerator(key, value)); + } + + protected void SessionStateTypeDirective(string noValueError, Func createCodeGenerator) + { + // Set the block type + Context.CurrentBlock.Type = BlockType.Directive; + + // Accept whitespace + CSharpSymbol remainingWs = AcceptSingleWhiteSpaceCharacter(); + + if (Span.Symbols.Count > 1) + { + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + } + + Output(SpanKind.MetaCode); + + if (remainingWs != null) + { + Accept(remainingWs); + } + AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); + + // Parse a Type Name + if (!ValidSessionStateValue()) + { + Context.OnError(CurrentLocation, noValueError); + } + + // Pull out the type name + string sessionStateValue = String.Concat( + Span.Symbols + .Cast() + .Select(sym => sym.Content)).Trim(); + + // Set up code generation + Span.CodeGenerator = createCodeGenerator(SyntaxConstants.CSharp.SessionStateKeyword, sessionStateValue); + + // Output the span and finish the block + CompleteBlock(); + Output(SpanKind.Code); + } + + protected virtual bool ValidSessionStateValue() + { + return Optional(CSharpSymbolType.Identifier); + } + + [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Coupling will be reviewed at a later date")] + [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "C# Keywords are always lower-case")] + protected virtual void HelperDirective() + { + bool nested = Context.IsWithin(BlockType.Helper); + + // Set the block and span type + Context.CurrentBlock.Type = BlockType.Helper; + + // Verify we're on "helper" and accept + AssertDirective(SyntaxConstants.CSharp.HelperKeyword); + Block block = new Block(CurrentSymbol.Content.ToString().ToLowerInvariant(), CurrentLocation); + AcceptAndMoveNext(); + + if (nested) + { + Context.OnError(CurrentLocation, RazorResources.ParseError_Helpers_Cannot_Be_Nested); + } + + // Accept a single whitespace character if present, if not, we should stop now + if (!At(CSharpSymbolType.WhiteSpace)) + { + string error; + if (At(CSharpSymbolType.NewLine)) + { + error = RazorResources.ErrorComponent_Newline; + } + else if (EndOfFile) + { + error = RazorResources.ErrorComponent_EndOfFile; + } + else + { + error = String.Format(CultureInfo.CurrentCulture, RazorResources.ErrorComponent_Character, CurrentSymbol.Content); + } + + Context.OnError( + CurrentLocation, + RazorResources.ParseError_Unexpected_Character_At_Helper_Name_Start, + error); + PutCurrentBack(); + Output(SpanKind.MetaCode); + return; + } + + CSharpSymbol remainingWs = AcceptSingleWhiteSpaceCharacter(); + + // Output metacode and continue + Output(SpanKind.MetaCode); + if (remainingWs != null) + { + Accept(remainingWs); + } + AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); // Don't accept newlines. + + // Expecting an identifier (helper name) + bool errorReported = !Required(CSharpSymbolType.Identifier, errorIfNotFound: true, errorBase: RazorResources.ParseError_Unexpected_Character_At_Helper_Name_Start); + if (!errorReported) + { + Assert(CSharpSymbolType.Identifier); + AcceptAndMoveNext(); + } + + AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); + + // Expecting parameter list start: "(" + SourceLocation bracketErrorPos = CurrentLocation; + if (!Optional(CSharpSymbolType.LeftParenthesis)) + { + if (!errorReported) + { + errorReported = true; + Context.OnError( + CurrentLocation, + RazorResources.ParseError_MissingCharAfterHelperName, + "("); + } + } + else + { + SourceLocation bracketStart = CurrentLocation; + if (!Balance(BalancingModes.NoErrorOnFailure, + CSharpSymbolType.LeftParenthesis, + CSharpSymbolType.RightParenthesis, + bracketStart)) + { + errorReported = true; + Context.OnError( + bracketErrorPos, + RazorResources.ParseError_UnterminatedHelperParameterList); + } + Optional(CSharpSymbolType.RightParenthesis); + } + + int bookmark = CurrentLocation.AbsoluteIndex; + IEnumerable ws = ReadWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + + // Expecting a "{" + SourceLocation errorLocation = CurrentLocation; + bool headerComplete = At(CSharpSymbolType.LeftBrace); + if (headerComplete) + { + Accept(ws); + AcceptAndMoveNext(); + } + else + { + Context.Source.Position = bookmark; + NextToken(); + AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); + if (!errorReported) + { + Context.OnError( + errorLocation, + RazorResources.ParseError_MissingCharAfterHelperParameters, + Language.GetSample(CSharpSymbolType.LeftBrace)); + } + } + + // Grab the signature and build the code generator + AddMarkerSymbolIfNecessary(); + LocationTagged signature = Span.GetContent(); + HelperCodeGenerator blockGen = new HelperCodeGenerator(signature, headerComplete); + Context.CurrentBlock.CodeGenerator = blockGen; + + // The block will generate appropriate code, + Span.CodeGenerator = SpanCodeGenerator.Null; + + if (!headerComplete) + { + CompleteBlock(); + Output(SpanKind.Code); + return; + } + else + { + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + Output(SpanKind.Code); + } + + // We're valid, so parse the nested block + AutoCompleteEditHandler bodyEditHandler = new AutoCompleteEditHandler(Language.TokenizeString); + using (PushSpanConfig(DefaultSpanConfig)) + { + using (Context.StartBlock(BlockType.Statement)) + { + Span.EditHandler = bodyEditHandler; + CodeBlock(false, block); + CompleteBlock(insertMarkerIfNecessary: true); + Output(SpanKind.Code); + } + } + Initialize(Span); + + EnsureCurrent(); + + Span.CodeGenerator = SpanCodeGenerator.Null; // The block will generate the footer code. + if (!Optional(CSharpSymbolType.RightBrace)) + { + // The } is missing, so set the initial signature span to use it as an autocomplete string + bodyEditHandler.AutoCompleteString = "}"; + + // Need to be able to accept anything to properly handle the autocomplete + bodyEditHandler.AcceptedCharacters = AcceptedCharacters.Any; + } + else + { + blockGen.Footer = Span.GetContent(); + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + } + CompleteBlock(); + Output(SpanKind.Code); + } + + protected virtual void SectionDirective() + { + bool nested = Context.IsWithin(BlockType.Section); + bool errorReported = false; + + // Set the block and span type + Context.CurrentBlock.Type = BlockType.Section; + + // Verify we're on "section" and accept + AssertDirective(SyntaxConstants.CSharp.SectionKeyword); + AcceptAndMoveNext(); + + if (nested) + { + Context.OnError(CurrentLocation, String.Format(CultureInfo.CurrentCulture, RazorResources.ParseError_Sections_Cannot_Be_Nested, RazorResources.SectionExample_CS)); + errorReported = true; + } + + IEnumerable ws = ReadWhile(IsSpacingToken(includeNewLines: true, includeComments: false)); + + // Get the section name + string sectionName = String.Empty; + if (!Required(CSharpSymbolType.Identifier, + errorIfNotFound: true, + errorBase: RazorResources.ParseError_Unexpected_Character_At_Section_Name_Start)) + { + if (!errorReported) + { + errorReported = true; + } + + PutCurrentBack(); + PutBack(ws); + AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: false)); + } + else + { + Accept(ws); + sectionName = CurrentSymbol.Content; + AcceptAndMoveNext(); + } + Context.CurrentBlock.CodeGenerator = new SectionCodeGenerator(sectionName); + + SourceLocation errorLocation = CurrentLocation; + ws = ReadWhile(IsSpacingToken(includeNewLines: true, includeComments: false)); + + // Get the starting brace + bool sawStartingBrace = At(CSharpSymbolType.LeftBrace); + if (!sawStartingBrace) + { + if (!errorReported) + { + errorReported = true; + Context.OnError(errorLocation, RazorResources.ParseError_MissingOpenBraceAfterSection); + } + + PutCurrentBack(); + PutBack(ws); + AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: false)); + Optional(CSharpSymbolType.NewLine); + Output(SpanKind.MetaCode); + CompleteBlock(); + return; + } + else + { + Accept(ws); + } + + // Set up edit handler + AutoCompleteEditHandler editHandler = new AutoCompleteEditHandler(Language.TokenizeString) { AutoCompleteAtEndOfSpan = true }; + + Span.EditHandler = editHandler; + Span.Accept(CurrentSymbol); + + // Output Metacode then switch to section parser + Output(SpanKind.MetaCode); + SectionBlock("{", "}", caseSensitive: true); + + Span.CodeGenerator = SpanCodeGenerator.Null; + // Check for the terminating "}" + if (!Optional(CSharpSymbolType.RightBrace)) + { + editHandler.AutoCompleteString = "}"; + Context.OnError(CurrentLocation, + RazorResources.ParseError_Expected_X, + Language.GetSample(CSharpSymbolType.RightBrace)); + } + else + { + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + } + CompleteBlock(insertMarkerIfNecessary: false, captureWhitespaceToEndOfLine: true); + Output(SpanKind.MetaCode); + return; + } + + protected virtual void FunctionsDirective() + { + // Set the block type + Context.CurrentBlock.Type = BlockType.Functions; + + // Verify we're on "functions" and accept + AssertDirective(SyntaxConstants.CSharp.FunctionsKeyword); + Block block = new Block(CurrentSymbol); + AcceptAndMoveNext(); + + AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: false)); + + if (!At(CSharpSymbolType.LeftBrace)) + { + Context.OnError(CurrentLocation, + RazorResources.ParseError_Expected_X, + Language.GetSample(CSharpSymbolType.LeftBrace)); + CompleteBlock(); + Output(SpanKind.MetaCode); + return; + } + else + { + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + } + + // Capture start point and continue + SourceLocation blockStart = CurrentLocation; + AcceptAndMoveNext(); + + // Output what we've seen and continue + Output(SpanKind.MetaCode); + + AutoCompleteEditHandler editHandler = new AutoCompleteEditHandler(Language.TokenizeString); + Span.EditHandler = editHandler; + + Balance(BalancingModes.NoErrorOnFailure, CSharpSymbolType.LeftBrace, CSharpSymbolType.RightBrace, blockStart); + Span.CodeGenerator = new TypeMemberCodeGenerator(); + if (!At(CSharpSymbolType.RightBrace)) + { + editHandler.AutoCompleteString = "}"; + Context.OnError(block.Start, RazorResources.ParseError_Expected_EndOfBlock_Before_EOF, block.Name, "}", "{"); + CompleteBlock(); + Output(SpanKind.Code); + } + else + { + Output(SpanKind.Code); + Assert(CSharpSymbolType.RightBrace); + Span.CodeGenerator = SpanCodeGenerator.Null; + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + AcceptAndMoveNext(); + CompleteBlock(); + Output(SpanKind.MetaCode); + } + } + + protected virtual void InheritsDirective() + { + // Verify we're on the right keyword and accept + AssertDirective(SyntaxConstants.CSharp.InheritsKeyword); + AcceptAndMoveNext(); + + InheritsDirectiveCore(); + } + + [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "directive", Justification = "This only occurs in Release builds, where this method is empty by design")] + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This only occurs in Release builds, where this method is empty by design")] + [Conditional("DEBUG")] + protected void AssertDirective(string directive) + { + Assert(CSharpSymbolType.Identifier); + Debug.Assert(String.Equals(CurrentSymbol.Content, directive, StringComparison.Ordinal)); + } + + protected void InheritsDirectiveCore() + { + BaseTypeDirective(RazorResources.ParseError_InheritsKeyword_Must_Be_Followed_By_TypeName, baseType => new SetBaseTypeCodeGenerator(baseType)); + } + + protected void BaseTypeDirective(string noTypeNameError, Func createCodeGenerator) + { + // Set the block type + Context.CurrentBlock.Type = BlockType.Directive; + + // Accept whitespace + CSharpSymbol remainingWs = AcceptSingleWhiteSpaceCharacter(); + + if (Span.Symbols.Count > 1) + { + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + } + + Output(SpanKind.MetaCode); + + if (remainingWs != null) + { + Accept(remainingWs); + } + AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); + + if (EndOfFile || At(CSharpSymbolType.WhiteSpace) || At(CSharpSymbolType.NewLine)) + { + Context.OnError(CurrentLocation, noTypeNameError); + } + + // Parse to the end of the line + AcceptUntil(CSharpSymbolType.NewLine); + if (!Context.DesignTimeMode) + { + // We want the newline to be treated as code, but it causes issues at design-time. + Optional(CSharpSymbolType.NewLine); + } + + // Pull out the type name + string baseType = Span.GetContent(); + + // Set up code generation + Span.CodeGenerator = createCodeGenerator(baseType.Trim()); + + // Output the span and finish the block + CompleteBlock(); + Output(SpanKind.Code); + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.Statements.cs b/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.Statements.cs new file mode 100644 index 0000000000..cbc9f9a2b9 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.Statements.cs @@ -0,0 +1,683 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using Microsoft.AspNet.Razor.Generator; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Resources; +using Microsoft.AspNet.Razor.Text; +using Microsoft.AspNet.Razor.Tokenizer.Symbols; + +namespace Microsoft.AspNet.Razor.Parser +{ + public partial class CSharpCodeParser + { + private void SetUpKeywords() + { + MapKeywords(ConditionalBlock, CSharpKeyword.For, CSharpKeyword.Foreach, CSharpKeyword.While, CSharpKeyword.Switch, CSharpKeyword.Lock); + MapKeywords(CaseStatement, false, CSharpKeyword.Case, CSharpKeyword.Default); + MapKeywords(IfStatement, CSharpKeyword.If); + MapKeywords(TryStatement, CSharpKeyword.Try); + MapKeywords(UsingKeyword, CSharpKeyword.Using); + MapKeywords(DoStatement, CSharpKeyword.Do); + MapKeywords(ReservedDirective, CSharpKeyword.Namespace, CSharpKeyword.Class); + } + + protected virtual void ReservedDirective(bool topLevel) + { + Context.OnError(CurrentLocation, String.Format(CultureInfo.CurrentCulture, RazorResources.ParseError_ReservedWord, CurrentSymbol.Content)); + AcceptAndMoveNext(); + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + Span.CodeGenerator = SpanCodeGenerator.Null; + Context.CurrentBlock.Type = BlockType.Directive; + CompleteBlock(); + Output(SpanKind.MetaCode); + } + + private void KeywordBlock(bool topLevel) + { + HandleKeyword(topLevel, () => + { + Context.CurrentBlock.Type = BlockType.Expression; + Context.CurrentBlock.CodeGenerator = new ExpressionCodeGenerator(); + ImplicitExpression(); + }); + } + + private void CaseStatement(bool topLevel) + { + Assert(CSharpSymbolType.Keyword); + Debug.Assert(CurrentSymbol.Keyword != null && + (CurrentSymbol.Keyword.Value == CSharpKeyword.Case || + CurrentSymbol.Keyword.Value == CSharpKeyword.Default)); + AcceptUntil(CSharpSymbolType.Colon); + Optional(CSharpSymbolType.Colon); + } + + private void DoStatement(bool topLevel) + { + Assert(CSharpKeyword.Do); + UnconditionalBlock(); + WhileClause(); + if (topLevel) + { + CompleteBlock(); + } + } + + private void WhileClause() + { + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any; + IEnumerable ws = SkipToNextImportantToken(); + + if (At(CSharpKeyword.While)) + { + Accept(ws); + Assert(CSharpKeyword.While); + AcceptAndMoveNext(); + AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + if (AcceptCondition() && Optional(CSharpSymbolType.Semicolon)) + { + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + } + } + else + { + PutCurrentBack(); + PutBack(ws); + } + } + + private void UsingKeyword(bool topLevel) + { + Assert(CSharpKeyword.Using); + Block block = new Block(CurrentSymbol); + AcceptAndMoveNext(); + AcceptWhile(IsSpacingToken(includeNewLines: false, includeComments: true)); + + if (At(CSharpSymbolType.LeftParenthesis)) + { + // using ( ==> Using Statement + UsingStatement(block); + } + else if (At(CSharpSymbolType.Identifier)) + { + // using Identifier ==> Using Declaration + if (!topLevel) + { + Context.OnError(block.Start, RazorResources.ParseError_NamespaceImportAndTypeAlias_Cannot_Exist_Within_CodeBlock); + StandardStatement(); + } + else + { + UsingDeclaration(); + } + } + + if (topLevel) + { + CompleteBlock(); + } + } + + private void UsingDeclaration() + { + // Set block type to directive + Context.CurrentBlock.Type = BlockType.Directive; + + // Parse a type name + Assert(CSharpSymbolType.Identifier); + NamespaceOrTypeName(); + IEnumerable ws = ReadWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + if (At(CSharpSymbolType.Assign)) + { + // Alias + Accept(ws); + Assert(CSharpSymbolType.Assign); + AcceptAndMoveNext(); + + AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + + // One more namespace or type name + NamespaceOrTypeName(); + } + else + { + PutCurrentBack(); + PutBack(ws); + } + + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.AnyExceptNewline; + Span.CodeGenerator = new AddImportCodeGenerator( + Span.GetContent(syms => syms.Skip(1)), // Skip "using" + SyntaxConstants.CSharp.UsingKeywordLength); + + // Optional ";" + if (EnsureCurrent()) + { + Optional(CSharpSymbolType.Semicolon); + } + } + + private bool NamespaceOrTypeName() + { + if (Optional(CSharpSymbolType.Identifier) || Optional(CSharpSymbolType.Keyword)) + { + Optional(CSharpSymbolType.QuestionMark); // Nullable + if (Optional(CSharpSymbolType.DoubleColon)) + { + if (!Optional(CSharpSymbolType.Identifier)) + { + Optional(CSharpSymbolType.Keyword); + } + } + if (At(CSharpSymbolType.LessThan)) + { + TypeArgumentList(); + } + if (Optional(CSharpSymbolType.Dot)) + { + NamespaceOrTypeName(); + } + while (At(CSharpSymbolType.LeftBracket)) + { + Balance(BalancingModes.None); + Optional(CSharpSymbolType.RightBracket); + } + return true; + } + else + { + return false; + } + } + + private void TypeArgumentList() + { + Assert(CSharpSymbolType.LessThan); + Balance(BalancingModes.None); + Optional(CSharpSymbolType.GreaterThan); + } + + private void UsingStatement(Block block) + { + Assert(CSharpSymbolType.LeftParenthesis); + + // Parse condition + if (AcceptCondition()) + { + AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + + // Parse code block + ExpectCodeBlock(block); + } + } + + private void TryStatement(bool topLevel) + { + Assert(CSharpKeyword.Try); + UnconditionalBlock(); + AfterTryClause(); + if (topLevel) + { + CompleteBlock(); + } + } + + private void IfStatement(bool topLevel) + { + Assert(CSharpKeyword.If); + ConditionalBlock(topLevel: false); + AfterIfClause(); + if (topLevel) + { + CompleteBlock(); + } + } + + private void AfterTryClause() + { + // Grab whitespace + IEnumerable ws = SkipToNextImportantToken(); + + // Check for a catch or finally part + if (At(CSharpKeyword.Catch)) + { + Accept(ws); + Assert(CSharpKeyword.Catch); + ConditionalBlock(topLevel: false); + AfterTryClause(); + } + else if (At(CSharpKeyword.Finally)) + { + Accept(ws); + Assert(CSharpKeyword.Finally); + UnconditionalBlock(); + } + else + { + // Return whitespace and end the block + PutCurrentBack(); + PutBack(ws); + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any; + } + } + + private void AfterIfClause() + { + // Grab whitespace and razor comments + IEnumerable ws = SkipToNextImportantToken(); + + // Check for an else part + if (At(CSharpKeyword.Else)) + { + Accept(ws); + Assert(CSharpKeyword.Else); + ElseClause(); + } + else + { + // No else, return whitespace + PutCurrentBack(); + PutBack(ws); + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any; + } + } + + private void ElseClause() + { + if (!At(CSharpKeyword.Else)) + { + return; + } + Block block = new Block(CurrentSymbol); + + AcceptAndMoveNext(); + AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + if (At(CSharpKeyword.If)) + { + // ElseIf + block.Name = SyntaxConstants.CSharp.ElseIfKeyword; + ConditionalBlock(block); + AfterIfClause(); + } + else if (!EndOfFile) + { + // Else + ExpectCodeBlock(block); + } + } + + private void ExpectCodeBlock(Block block) + { + if (!EndOfFile) + { + // Check for "{" to make sure we're at a block + if (!At(CSharpSymbolType.LeftBrace)) + { + Context.OnError(CurrentLocation, + RazorResources.ParseError_SingleLine_ControlFlowStatements_Not_Allowed, + Language.GetSample(CSharpSymbolType.LeftBrace), + CurrentSymbol.Content); + } + + // Parse the statement and then we're done + Statement(block); + } + } + + private void UnconditionalBlock() + { + Assert(CSharpSymbolType.Keyword); + Block block = new Block(CurrentSymbol); + AcceptAndMoveNext(); + AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + ExpectCodeBlock(block); + } + + private void ConditionalBlock(bool topLevel) + { + Assert(CSharpSymbolType.Keyword); + Block block = new Block(CurrentSymbol); + ConditionalBlock(block); + if (topLevel) + { + CompleteBlock(); + } + } + + private void ConditionalBlock(Block block) + { + AcceptAndMoveNext(); + AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + + // Parse the condition, if present (if not present, we'll let the C# compiler complain) + if (AcceptCondition()) + { + AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + ExpectCodeBlock(block); + } + } + + private bool AcceptCondition() + { + if (At(CSharpSymbolType.LeftParenthesis)) + { + bool complete = Balance(BalancingModes.BacktrackOnFailure | BalancingModes.AllowCommentsAndTemplates); + if (!complete) + { + AcceptUntil(CSharpSymbolType.NewLine); + } + else + { + Optional(CSharpSymbolType.RightParenthesis); + } + return complete; + } + return true; + } + + private void Statement() + { + Statement(null); + } + + private void Statement(Block block) + { + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any; + + // Accept whitespace but always keep the last whitespace node so we can put it back if necessary + CSharpSymbol lastWs = AcceptWhiteSpaceInLines(); + Debug.Assert(lastWs == null || (lastWs.Start.AbsoluteIndex + lastWs.Content.Length == CurrentLocation.AbsoluteIndex)); + + if (EndOfFile) + { + if (lastWs != null) + { + Accept(lastWs); + } + return; + } + + CSharpSymbolType type = CurrentSymbol.Type; + SourceLocation loc = CurrentLocation; + + bool isSingleLineMarkup = type == CSharpSymbolType.Transition && NextIs(CSharpSymbolType.Colon); + bool isMarkup = isSingleLineMarkup || + type == CSharpSymbolType.LessThan || + (type == CSharpSymbolType.Transition && NextIs(CSharpSymbolType.LessThan)); + + if (Context.DesignTimeMode || !isMarkup) + { + // CODE owns whitespace, MARKUP owns it ONLY in DesignTimeMode. + if (lastWs != null) + { + Accept(lastWs); + } + } + else + { + // MARKUP owns whitespace EXCEPT in DesignTimeMode. + PutCurrentBack(); + PutBack(lastWs); + } + + if (isMarkup) + { + if (type == CSharpSymbolType.Transition && !isSingleLineMarkup) + { + Context.OnError(loc, RazorResources.ParseError_AtInCode_Must_Be_Followed_By_Colon_Paren_Or_Identifier_Start); + } + + // Markup block + Output(SpanKind.Code); + if (Context.DesignTimeMode && CurrentSymbol != null && (CurrentSymbol.Type == CSharpSymbolType.LessThan || CurrentSymbol.Type == CSharpSymbolType.Transition)) + { + PutCurrentBack(); + } + OtherParserBlock(); + } + else + { + // What kind of statement is this? + HandleStatement(block, type); + } + } + + private void HandleStatement(Block block, CSharpSymbolType type) + { + switch (type) + { + case CSharpSymbolType.RazorCommentTransition: + Output(SpanKind.Code); + RazorComment(); + Statement(block); + break; + case CSharpSymbolType.LeftBrace: + // Verbatim Block + block = block ?? new Block(RazorResources.BlockName_Code, CurrentLocation); + AcceptAndMoveNext(); + CodeBlock(block); + break; + case CSharpSymbolType.Keyword: + // Keyword block + HandleKeyword(false, StandardStatement); + break; + case CSharpSymbolType.Transition: + // Embedded Expression block + EmbeddedExpression(); + break; + case CSharpSymbolType.RightBrace: + // Possible end of Code Block, just run the continuation + break; + case CSharpSymbolType.Comment: + AcceptAndMoveNext(); + break; + default: + // Other statement + StandardStatement(); + break; + } + } + + private void EmbeddedExpression() + { + // First, verify the type of the block + Assert(CSharpSymbolType.Transition); + CSharpSymbol transition = CurrentSymbol; + NextToken(); + + if (At(CSharpSymbolType.Transition)) + { + // Escaped "@" + Output(SpanKind.Code); + + // Output "@" as hidden span + Accept(transition); + Span.CodeGenerator = SpanCodeGenerator.Null; + Output(SpanKind.Code); + + Assert(CSharpSymbolType.Transition); + AcceptAndMoveNext(); + StandardStatement(); + } + else + { + // Throw errors as necessary, but continue parsing + if (At(CSharpSymbolType.Keyword)) + { + Context.OnError(CurrentLocation, + RazorResources.ParseError_Unexpected_Keyword_After_At, + CSharpLanguageCharacteristics.GetKeyword(CurrentSymbol.Keyword.Value)); + } + else if (At(CSharpSymbolType.LeftBrace)) + { + Context.OnError(CurrentLocation, RazorResources.ParseError_Unexpected_Nested_CodeBlock); + } + + // @( or @foo - Nested expression, parse a child block + PutCurrentBack(); + PutBack(transition); + + // Before exiting, add a marker span if necessary + AddMarkerSymbolIfNecessary(); + + NestedBlock(); + } + } + + private void StandardStatement() + { + while (!EndOfFile) + { + int bookmark = CurrentLocation.AbsoluteIndex; + IEnumerable read = ReadWhile(sym => sym.Type != CSharpSymbolType.Semicolon && + sym.Type != CSharpSymbolType.RazorCommentTransition && + sym.Type != CSharpSymbolType.Transition && + sym.Type != CSharpSymbolType.LeftBrace && + sym.Type != CSharpSymbolType.LeftParenthesis && + sym.Type != CSharpSymbolType.LeftBracket && + sym.Type != CSharpSymbolType.RightBrace); + if (At(CSharpSymbolType.LeftBrace) || At(CSharpSymbolType.LeftParenthesis) || At(CSharpSymbolType.LeftBracket)) + { + Accept(read); + if (Balance(BalancingModes.AllowCommentsAndTemplates | BalancingModes.BacktrackOnFailure)) + { + Optional(CSharpSymbolType.RightBrace); + } + else + { + // Recovery + AcceptUntil(CSharpSymbolType.LessThan, CSharpSymbolType.RightBrace); + return; + } + } + else if (At(CSharpSymbolType.Transition) && (NextIs(CSharpSymbolType.LessThan, CSharpSymbolType.Colon))) + { + Accept(read); + Output(SpanKind.Code); + Template(); + } + else if (At(CSharpSymbolType.RazorCommentTransition)) + { + Accept(read); + RazorComment(); + } + else if (At(CSharpSymbolType.Semicolon)) + { + Accept(read); + AcceptAndMoveNext(); + return; + } + else if (At(CSharpSymbolType.RightBrace)) + { + Accept(read); + return; + } + else + { + Context.Source.Position = bookmark; + NextToken(); + AcceptUntil(CSharpSymbolType.LessThan, CSharpSymbolType.RightBrace); + return; + } + } + } + + private void CodeBlock(Block block) + { + CodeBlock(true, block); + } + + private void CodeBlock(bool acceptTerminatingBrace, Block block) + { + EnsureCurrent(); + while (!EndOfFile && !At(CSharpSymbolType.RightBrace)) + { + // Parse a statement, then return here + Statement(); + EnsureCurrent(); + } + + if (EndOfFile) + { + Context.OnError(block.Start, RazorResources.ParseError_Expected_EndOfBlock_Before_EOF, block.Name, '}', '{'); + } + else if (acceptTerminatingBrace) + { + Assert(CSharpSymbolType.RightBrace); + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + AcceptAndMoveNext(); + } + } + + private void HandleKeyword(bool topLevel, Action fallback) + { + Debug.Assert(CurrentSymbol.Type == CSharpSymbolType.Keyword && CurrentSymbol.Keyword != null); + Action handler; + if (_keywordParsers.TryGetValue(CurrentSymbol.Keyword.Value, out handler)) + { + handler(topLevel); + } + else + { + fallback(); + } + } + + private IEnumerable SkipToNextImportantToken() + { + while (!EndOfFile) + { + IEnumerable ws = ReadWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + if (At(CSharpSymbolType.RazorCommentTransition)) + { + Accept(ws); + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any; + RazorComment(); + } + else + { + return ws; + } + } + return Enumerable.Empty(); + } + + // Common code for Parsers, but FxCop REALLY doesn't like it in the base class.. moving it here for now. + protected override void OutputSpanBeforeRazorComment() + { + AddMarkerSymbolIfNecessary(); + Output(SpanKind.Code); + } + + protected class Block + { + public Block(string name, SourceLocation start) + { + Name = name; + Start = start; + } + + public Block(CSharpSymbol symbol) + : this(GetName(symbol), symbol.Start) + { + } + + public string Name { get; set; } + public SourceLocation Start { get; set; } + + private static string GetName(CSharpSymbol sym) + { + if (sym.Type == CSharpSymbolType.Keyword) + { + return CSharpLanguageCharacteristics.GetKeyword(sym.Keyword.Value); + } + return sym.Content; + } + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.cs b/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.cs new file mode 100644 index 0000000000..6417f7dfc2 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Parser/CSharpCodeParser.cs @@ -0,0 +1,578 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNet.Razor.Editor; +using Microsoft.AspNet.Razor.Generator; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Resources; +using Microsoft.AspNet.Razor.Tokenizer; +using Microsoft.AspNet.Razor.Tokenizer.Symbols; + +namespace Microsoft.AspNet.Razor.Parser +{ + public partial class CSharpCodeParser : TokenizerBackedParser + { + internal static readonly int UsingKeywordLength = 5; // using + + internal static ISet DefaultKeywords = new HashSet() + { + "if", + "do", + "try", + "for", + "foreach", + "while", + "switch", + "lock", + "using", + "section", + "inherits", + "helper", + "functions", + "namespace", + "class", + "layout", + "sessionstate" + }; + + private Dictionary _directiveParsers = new Dictionary(); + private Dictionary> _keywordParsers = new Dictionary>(); + + public CSharpCodeParser() + { + Keywords = new HashSet(); + SetUpKeywords(); + SetupDirectives(); + } + + protected internal ISet Keywords { get; private set; } + + public bool IsNested { get; set; } + + protected override ParserBase OtherParser + { + get { return Context.MarkupParser; } + } + + protected override LanguageCharacteristics Language + { + get { return CSharpLanguageCharacteristics.Instance; } + } + + protected void MapDirectives(Action handler, params string[] directives) + { + foreach (string directive in directives) + { + _directiveParsers.Add(directive, handler); + Keywords.Add(directive); + } + } + + protected bool TryGetDirectiveHandler(string directive, out Action handler) + { + return _directiveParsers.TryGetValue(directive, out handler); + } + + private void MapKeywords(Action handler, params CSharpKeyword[] keywords) + { + MapKeywords(handler, topLevel: true, keywords: keywords); + } + + private void MapKeywords(Action handler, bool topLevel, params CSharpKeyword[] keywords) + { + foreach (CSharpKeyword keyword in keywords) + { + _keywordParsers.Add(keyword, handler); + if (topLevel) + { + Keywords.Add(CSharpLanguageCharacteristics.GetKeyword(keyword)); + } + } + } + + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This only occurs in Release builds, where this method is empty by design")] + [Conditional("DEBUG")] + internal void Assert(CSharpKeyword expectedKeyword) + { + Debug.Assert(CurrentSymbol.Type == CSharpSymbolType.Keyword && CurrentSymbol.Keyword.HasValue && CurrentSymbol.Keyword.Value == expectedKeyword); + } + + protected internal bool At(CSharpKeyword keyword) + { + return At(CSharpSymbolType.Keyword) && CurrentSymbol.Keyword.HasValue && CurrentSymbol.Keyword.Value == keyword; + } + + protected internal bool AcceptIf(CSharpKeyword keyword) + { + if (At(keyword)) + { + AcceptAndMoveNext(); + return true; + } + return false; + } + + protected static Func IsSpacingToken(bool includeNewLines, bool includeComments) + { + return sym => sym.Type == CSharpSymbolType.WhiteSpace || + (includeNewLines && sym.Type == CSharpSymbolType.NewLine) || + (includeComments && sym.Type == CSharpSymbolType.Comment); + } + + public override void ParseBlock() + { + using (PushSpanConfig(DefaultSpanConfig)) + { + if (Context == null) + { + throw new InvalidOperationException(RazorResources.Parser_Context_Not_Set); + } + + // Unless changed, the block is a statement block + using (Context.StartBlock(BlockType.Statement)) + { + NextToken(); + + AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true)); + + CSharpSymbol current = CurrentSymbol; + if (At(CSharpSymbolType.StringLiteral) && CurrentSymbol.Content.Length > 0 && CurrentSymbol.Content[0] == SyntaxConstants.TransitionCharacter) + { + Tuple split = Language.SplitSymbol(CurrentSymbol, 1, CSharpSymbolType.Transition); + current = split.Item1; + Context.Source.Position = split.Item2.Start.AbsoluteIndex; + NextToken(); + } + else if (At(CSharpSymbolType.Transition)) + { + NextToken(); + } + + // Accept "@" if we see it, but if we don't, that's OK. We assume we were started for a good reason + if (current.Type == CSharpSymbolType.Transition) + { + if (Span.Symbols.Count > 0) + { + Output(SpanKind.Code); + } + AtTransition(current); + } + else + { + // No "@" => Jump straight to AfterTransition + AfterTransition(); + } + Output(SpanKind.Code); + } + } + } + + private void DefaultSpanConfig(SpanBuilder span) + { + span.EditHandler = SpanEditHandler.CreateDefault(Language.TokenizeString); + span.CodeGenerator = new StatementCodeGenerator(); + } + + private void AtTransition(CSharpSymbol current) + { + Debug.Assert(current.Type == CSharpSymbolType.Transition); + Accept(current); + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + Span.CodeGenerator = SpanCodeGenerator.Null; + + // Output the "@" span and continue here + Output(SpanKind.Transition); + AfterTransition(); + } + + private void AfterTransition() + { + using (PushSpanConfig(DefaultSpanConfig)) + { + EnsureCurrent(); + try + { + // What type of block is this? + if (!EndOfFile) + { + if (CurrentSymbol.Type == CSharpSymbolType.LeftParenthesis) + { + Context.CurrentBlock.Type = BlockType.Expression; + Context.CurrentBlock.CodeGenerator = new ExpressionCodeGenerator(); + ExplicitExpression(); + return; + } + else if (CurrentSymbol.Type == CSharpSymbolType.Identifier) + { + Action handler; + if (TryGetDirectiveHandler(CurrentSymbol.Content, out handler)) + { + Span.CodeGenerator = SpanCodeGenerator.Null; + handler(); + return; + } + else + { + Context.CurrentBlock.Type = BlockType.Expression; + Context.CurrentBlock.CodeGenerator = new ExpressionCodeGenerator(); + ImplicitExpression(); + return; + } + } + else if (CurrentSymbol.Type == CSharpSymbolType.Keyword) + { + KeywordBlock(topLevel: true); + return; + } + else if (CurrentSymbol.Type == CSharpSymbolType.LeftBrace) + { + VerbatimBlock(); + return; + } + } + + // Invalid character + Context.CurrentBlock.Type = BlockType.Expression; + Context.CurrentBlock.CodeGenerator = new ExpressionCodeGenerator(); + AddMarkerSymbolIfNecessary(); + Span.CodeGenerator = new ExpressionCodeGenerator(); + Span.EditHandler = new ImplicitExpressionEditHandler( + Language.TokenizeString, + DefaultKeywords, + acceptTrailingDot: IsNested) + { + AcceptedCharacters = AcceptedCharacters.NonWhiteSpace + }; + if (At(CSharpSymbolType.WhiteSpace) || At(CSharpSymbolType.NewLine)) + { + Context.OnError(CurrentLocation, RazorResources.ParseError_Unexpected_WhiteSpace_At_Start_Of_CodeBlock_CS); + } + else if (EndOfFile) + { + Context.OnError(CurrentLocation, RazorResources.ParseError_Unexpected_EndOfFile_At_Start_Of_CodeBlock); + } + else + { + Context.OnError(CurrentLocation, RazorResources.ParseError_Unexpected_Character_At_Start_Of_CodeBlock_CS, CurrentSymbol.Content); + } + } + finally + { + // Always put current character back in the buffer for the next parser. + PutCurrentBack(); + } + } + } + + private void VerbatimBlock() + { + Assert(CSharpSymbolType.LeftBrace); + Block block = new Block(RazorResources.BlockName_Code, CurrentLocation); + AcceptAndMoveNext(); + + // Set up the "{" span and output + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + Span.CodeGenerator = SpanCodeGenerator.Null; + Output(SpanKind.MetaCode); + + // Set up auto-complete and parse the code block + AutoCompleteEditHandler editHandler = new AutoCompleteEditHandler(Language.TokenizeString); + Span.EditHandler = editHandler; + CodeBlock(false, block); + + Span.CodeGenerator = new StatementCodeGenerator(); + AddMarkerSymbolIfNecessary(); + if (!At(CSharpSymbolType.RightBrace)) + { + editHandler.AutoCompleteString = "}"; + } + Output(SpanKind.Code); + + if (Optional(CSharpSymbolType.RightBrace)) + { + // Set up the "}" span + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + Span.CodeGenerator = SpanCodeGenerator.Null; + } + + if (!At(CSharpSymbolType.WhiteSpace) && !At(CSharpSymbolType.NewLine)) + { + PutCurrentBack(); + } + + CompleteBlock(insertMarkerIfNecessary: false); + Output(SpanKind.MetaCode); + } + + private void ImplicitExpression() + { + Context.CurrentBlock.Type = BlockType.Expression; + Context.CurrentBlock.CodeGenerator = new ExpressionCodeGenerator(); + + using (PushSpanConfig(span => + { + span.EditHandler = new ImplicitExpressionEditHandler(Language.TokenizeString, Keywords, acceptTrailingDot: IsNested); + span.EditHandler.AcceptedCharacters = AcceptedCharacters.NonWhiteSpace; + span.CodeGenerator = new ExpressionCodeGenerator(); + })) + { + do + { + if (AtIdentifier(allowKeywords: true)) + { + AcceptAndMoveNext(); + } + } + while (MethodCallOrArrayIndex()); + + PutCurrentBack(); + Output(SpanKind.Code); + } + } + + private bool MethodCallOrArrayIndex() + { + if (!EndOfFile) + { + if (CurrentSymbol.Type == CSharpSymbolType.LeftParenthesis || CurrentSymbol.Type == CSharpSymbolType.LeftBracket) + { + // If we end within "(", whitespace is fine + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any; + + CSharpSymbolType right; + bool success; + + using (PushSpanConfig((span, prev) => + { + prev(span); + span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any; + })) + { + right = Language.FlipBracket(CurrentSymbol.Type); + success = Balance(BalancingModes.BacktrackOnFailure | BalancingModes.AllowCommentsAndTemplates); + } + + if (!success) + { + AcceptUntil(CSharpSymbolType.LessThan); + } + if (At(right)) + { + AcceptAndMoveNext(); + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.NonWhiteSpace; + } + return MethodCallOrArrayIndex(); + } + if (CurrentSymbol.Type == CSharpSymbolType.Dot) + { + CSharpSymbol dot = CurrentSymbol; + if (NextToken()) + { + if (At(CSharpSymbolType.Identifier) || At(CSharpSymbolType.Keyword)) + { + // Accept the dot and return to the start + Accept(dot); + return true; // continue + } + else + { + // Put the symbol back + PutCurrentBack(); + } + } + if (!IsNested) + { + // Put the "." back + PutBack(dot); + } + else + { + Accept(dot); + } + } + else if (!At(CSharpSymbolType.WhiteSpace) && !At(CSharpSymbolType.NewLine)) + { + PutCurrentBack(); + } + } + + // Implicit Expression is complete + return false; + } + + private void CompleteBlock() + { + CompleteBlock(insertMarkerIfNecessary: true); + } + + private void CompleteBlock(bool insertMarkerIfNecessary) + { + CompleteBlock(insertMarkerIfNecessary, captureWhitespaceToEndOfLine: insertMarkerIfNecessary); + } + + private void CompleteBlock(bool insertMarkerIfNecessary, bool captureWhitespaceToEndOfLine) + { + if (insertMarkerIfNecessary && Context.LastAcceptedCharacters != AcceptedCharacters.Any) + { + AddMarkerSymbolIfNecessary(); + } + + EnsureCurrent(); + + // Read whitespace, but not newlines + // If we're not inserting a marker span, we don't need to capture whitespace + if (!Context.WhiteSpaceIsSignificantToAncestorBlock && + Context.CurrentBlock.Type != BlockType.Expression && + captureWhitespaceToEndOfLine && + !Context.DesignTimeMode && + !IsNested) + { + CaptureWhitespaceAtEndOfCodeOnlyLine(); + } + else + { + PutCurrentBack(); + } + } + + private void CaptureWhitespaceAtEndOfCodeOnlyLine() + { + IEnumerable ws = ReadWhile(sym => sym.Type == CSharpSymbolType.WhiteSpace); + if (At(CSharpSymbolType.NewLine)) + { + Accept(ws); + AcceptAndMoveNext(); + PutCurrentBack(); + } + else + { + PutCurrentBack(); + PutBack(ws); + } + } + + private void ConfigureExplicitExpressionSpan(SpanBuilder sb) + { + sb.EditHandler = SpanEditHandler.CreateDefault(Language.TokenizeString); + sb.CodeGenerator = new ExpressionCodeGenerator(); + } + + private void ExplicitExpression() + { + Block block = new Block(RazorResources.BlockName_ExplicitExpression, CurrentLocation); + Assert(CSharpSymbolType.LeftParenthesis); + AcceptAndMoveNext(); + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + Span.CodeGenerator = SpanCodeGenerator.Null; + Output(SpanKind.MetaCode); + using (PushSpanConfig(ConfigureExplicitExpressionSpan)) + { + bool success = Balance( + BalancingModes.BacktrackOnFailure | BalancingModes.NoErrorOnFailure | BalancingModes.AllowCommentsAndTemplates, + CSharpSymbolType.LeftParenthesis, + CSharpSymbolType.RightParenthesis, + block.Start); + + if (!success) + { + AcceptUntil(CSharpSymbolType.LessThan); + Context.OnError(block.Start, RazorResources.ParseError_Expected_EndOfBlock_Before_EOF, block.Name, ")", "("); + } + + // If necessary, put an empty-content marker symbol here + if (Span.Symbols.Count == 0) + { + Accept(new CSharpSymbol(CurrentLocation, String.Empty, CSharpSymbolType.Unknown)); + } + + // Output the content span and then capture the ")" + Output(SpanKind.Code); + } + Optional(CSharpSymbolType.RightParenthesis); + if (!EndOfFile) + { + PutCurrentBack(); + } + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + Span.CodeGenerator = SpanCodeGenerator.Null; + CompleteBlock(insertMarkerIfNecessary: false); + Output(SpanKind.MetaCode); + } + + private void Template() + { + if (Context.IsWithin(BlockType.Template)) + { + Context.OnError(CurrentLocation, RazorResources.ParseError_InlineMarkup_Blocks_Cannot_Be_Nested); + } + Output(SpanKind.Code); + using (Context.StartBlock(BlockType.Template)) + { + Context.CurrentBlock.CodeGenerator = new TemplateBlockCodeGenerator(); + PutCurrentBack(); + OtherParserBlock(); + } + } + + private void OtherParserBlock() + { + ParseWithOtherParser(p => p.ParseBlock()); + } + + private void SectionBlock(string left, string right, bool caseSensitive) + { + ParseWithOtherParser(p => p.ParseSection(Tuple.Create(left, right), caseSensitive)); + } + + private void NestedBlock() + { + Output(SpanKind.Code); + bool wasNested = IsNested; + IsNested = true; + using (PushSpanConfig()) + { + ParseBlock(); + } + Initialize(Span); + IsNested = wasNested; + NextToken(); + } + + protected override bool IsAtEmbeddedTransition(bool allowTemplatesAndComments, bool allowTransitions) + { + // No embedded transitions in C#, so ignore that param + return allowTemplatesAndComments + && ((Language.IsTransition(CurrentSymbol) + && NextIs(CSharpSymbolType.LessThan, CSharpSymbolType.Colon)) + || Language.IsCommentStart(CurrentSymbol)); + } + + protected override void HandleEmbeddedTransition() + { + if (Language.IsTransition(CurrentSymbol)) + { + PutCurrentBack(); + Template(); + } + else if (Language.IsCommentStart(CurrentSymbol)) + { + RazorComment(); + } + } + + private void ParseWithOtherParser(Action parseAction) + { + using (PushSpanConfig()) + { + Context.SwitchActiveParser(); + parseAction(Context.MarkupParser); + Context.SwitchActiveParser(); + } + Initialize(Span); + NextToken(); + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Parser/CSharpLanguageCharacteristics.cs b/src/Microsoft.AspNet.Razor/Parser/CSharpLanguageCharacteristics.cs new file mode 100644 index 0000000000..ff7984eeef --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Parser/CSharpLanguageCharacteristics.cs @@ -0,0 +1,190 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Resources; +using Microsoft.AspNet.Razor.Text; +using Microsoft.AspNet.Razor.Tokenizer; +using Microsoft.AspNet.Razor.Tokenizer.Symbols; + +namespace Microsoft.AspNet.Razor.Parser +{ + public class CSharpLanguageCharacteristics : LanguageCharacteristics + { + private static readonly CSharpLanguageCharacteristics _instance = new CSharpLanguageCharacteristics(); + + private static Dictionary _symbolSamples = new Dictionary() + { + { CSharpSymbolType.Arrow, "->" }, + { CSharpSymbolType.Minus, "-" }, + { CSharpSymbolType.Decrement, "--" }, + { CSharpSymbolType.MinusAssign, "-=" }, + { CSharpSymbolType.NotEqual, "!=" }, + { CSharpSymbolType.Not, "!" }, + { CSharpSymbolType.Modulo, "%" }, + { CSharpSymbolType.ModuloAssign, "%=" }, + { CSharpSymbolType.AndAssign, "&=" }, + { CSharpSymbolType.And, "&" }, + { CSharpSymbolType.DoubleAnd, "&&" }, + { CSharpSymbolType.LeftParenthesis, "(" }, + { CSharpSymbolType.RightParenthesis, ")" }, + { CSharpSymbolType.Star, "*" }, + { CSharpSymbolType.MultiplyAssign, "*=" }, + { CSharpSymbolType.Comma, "," }, + { CSharpSymbolType.Dot, "." }, + { CSharpSymbolType.Slash, "/" }, + { CSharpSymbolType.DivideAssign, "/=" }, + { CSharpSymbolType.DoubleColon, "::" }, + { CSharpSymbolType.Colon, ":" }, + { CSharpSymbolType.Semicolon, ";" }, + { CSharpSymbolType.QuestionMark, "?" }, + { CSharpSymbolType.NullCoalesce, "??" }, + { CSharpSymbolType.RightBracket, "]" }, + { CSharpSymbolType.LeftBracket, "[" }, + { CSharpSymbolType.XorAssign, "^=" }, + { CSharpSymbolType.Xor, "^" }, + { CSharpSymbolType.LeftBrace, "{" }, + { CSharpSymbolType.OrAssign, "|=" }, + { CSharpSymbolType.DoubleOr, "||" }, + { CSharpSymbolType.Or, "|" }, + { CSharpSymbolType.RightBrace, "}" }, + { CSharpSymbolType.Tilde, "~" }, + { CSharpSymbolType.Plus, "+" }, + { CSharpSymbolType.PlusAssign, "+=" }, + { CSharpSymbolType.Increment, "++" }, + { CSharpSymbolType.LessThan, "<" }, + { CSharpSymbolType.LessThanEqual, "<=" }, + { CSharpSymbolType.LeftShift, "<<" }, + { CSharpSymbolType.LeftShiftAssign, "<<=" }, + { CSharpSymbolType.Assign, "=" }, + { CSharpSymbolType.Equals, "==" }, + { CSharpSymbolType.GreaterThan, ">" }, + { CSharpSymbolType.GreaterThanEqual, ">=" }, + { CSharpSymbolType.RightShift, ">>" }, + { CSharpSymbolType.RightShiftAssign, ">>>" }, + { CSharpSymbolType.Hash, "#" }, + { CSharpSymbolType.Transition, "@" }, + }; + + private CSharpLanguageCharacteristics() + { + } + + public static CSharpLanguageCharacteristics Instance + { + get { return _instance; } + } + + public override CSharpTokenizer CreateTokenizer(ITextDocument source) + { + return new CSharpTokenizer(source); + } + + protected override CSharpSymbol CreateSymbol(SourceLocation location, string content, CSharpSymbolType type, IEnumerable errors) + { + return new CSharpSymbol(location, content, type, errors); + } + + public override string GetSample(CSharpSymbolType type) + { + return GetSymbolSample(type); + } + + public override CSharpSymbol CreateMarkerSymbol(SourceLocation location) + { + return new CSharpSymbol(location, String.Empty, CSharpSymbolType.Unknown); + } + + public override CSharpSymbolType GetKnownSymbolType(KnownSymbolType type) + { + switch (type) + { + case KnownSymbolType.Identifier: + return CSharpSymbolType.Identifier; + case KnownSymbolType.Keyword: + return CSharpSymbolType.Keyword; + case KnownSymbolType.NewLine: + return CSharpSymbolType.NewLine; + case KnownSymbolType.WhiteSpace: + return CSharpSymbolType.WhiteSpace; + case KnownSymbolType.Transition: + return CSharpSymbolType.Transition; + case KnownSymbolType.CommentStart: + return CSharpSymbolType.RazorCommentTransition; + case KnownSymbolType.CommentStar: + return CSharpSymbolType.RazorCommentStar; + case KnownSymbolType.CommentBody: + return CSharpSymbolType.RazorComment; + default: + return CSharpSymbolType.Unknown; + } + } + + public override CSharpSymbolType FlipBracket(CSharpSymbolType bracket) + { + switch (bracket) + { + case CSharpSymbolType.LeftBrace: + return CSharpSymbolType.RightBrace; + case CSharpSymbolType.LeftBracket: + return CSharpSymbolType.RightBracket; + case CSharpSymbolType.LeftParenthesis: + return CSharpSymbolType.RightParenthesis; + case CSharpSymbolType.LessThan: + return CSharpSymbolType.GreaterThan; + case CSharpSymbolType.RightBrace: + return CSharpSymbolType.LeftBrace; + case CSharpSymbolType.RightBracket: + return CSharpSymbolType.LeftBracket; + case CSharpSymbolType.RightParenthesis: + return CSharpSymbolType.LeftParenthesis; + case CSharpSymbolType.GreaterThan: + return CSharpSymbolType.LessThan; + default: + Debug.Fail("FlipBracket must be called with a bracket character"); + return CSharpSymbolType.Unknown; + } + } + + [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "C# Keywords are lower-case")] + public static string GetKeyword(CSharpKeyword keyword) + { + return keyword.ToString().ToLowerInvariant(); + } + + public static string GetSymbolSample(CSharpSymbolType type) + { + string sample; + if (!_symbolSamples.TryGetValue(type, out sample)) + { + switch (type) + { + case CSharpSymbolType.Identifier: + return RazorResources.CSharpSymbol_Identifier; + case CSharpSymbolType.Keyword: + return RazorResources.CSharpSymbol_Keyword; + case CSharpSymbolType.IntegerLiteral: + return RazorResources.CSharpSymbol_IntegerLiteral; + case CSharpSymbolType.NewLine: + return RazorResources.CSharpSymbol_Newline; + case CSharpSymbolType.WhiteSpace: + return RazorResources.CSharpSymbol_Whitespace; + case CSharpSymbolType.Comment: + return RazorResources.CSharpSymbol_Comment; + case CSharpSymbolType.RealLiteral: + return RazorResources.CSharpSymbol_RealLiteral; + case CSharpSymbolType.CharacterLiteral: + return RazorResources.CSharpSymbol_CharacterLiteral; + case CSharpSymbolType.StringLiteral: + return RazorResources.CSharpSymbol_StringLiteral; + default: + return RazorResources.Symbol_Unknown; + } + } + return sample; + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Parser/CallbackVisitor.cs b/src/Microsoft.AspNet.Razor/Parser/CallbackVisitor.cs new file mode 100644 index 0000000000..94b887fa44 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Parser/CallbackVisitor.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Threading; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; + +namespace Microsoft.AspNet.Razor.Parser +{ + public class CallbackVisitor : ParserVisitor + { + private Action _spanCallback; + private Action _errorCallback; + private Action _endBlockCallback; + private Action _startBlockCallback; + private Action _completeCallback; + + public CallbackVisitor(Action spanCallback) + : this(spanCallback, _ => + { + }) + { + } + + public CallbackVisitor(Action spanCallback, Action errorCallback) + : this(spanCallback, errorCallback, _ => + { + }, _ => + { + }) + { + } + + public CallbackVisitor(Action spanCallback, Action errorCallback, Action startBlockCallback, Action endBlockCallback) + : this(spanCallback, errorCallback, startBlockCallback, endBlockCallback, () => + { + }) + { + } + + public CallbackVisitor(Action spanCallback, Action errorCallback, Action startBlockCallback, Action endBlockCallback, Action completeCallback) + { + _spanCallback = spanCallback; + _errorCallback = errorCallback; + _startBlockCallback = startBlockCallback; + _endBlockCallback = endBlockCallback; + _completeCallback = completeCallback; + } + + public SynchronizationContext SynchronizationContext { get; set; } + + public override void VisitStartBlock(Block block) + { + base.VisitStartBlock(block); + RaiseCallback(SynchronizationContext, block.Type, _startBlockCallback); + } + + public override void VisitSpan(Span span) + { + base.VisitSpan(span); + RaiseCallback(SynchronizationContext, span, _spanCallback); + } + + public override void VisitEndBlock(Block block) + { + base.VisitEndBlock(block); + RaiseCallback(SynchronizationContext, block.Type, _endBlockCallback); + } + + public override void VisitError(RazorError err) + { + base.VisitError(err); + RaiseCallback(SynchronizationContext, err, _errorCallback); + } + + public override void OnComplete() + { + base.OnComplete(); + RaiseCallback(SynchronizationContext, null, _ => _completeCallback()); + } + + private static void RaiseCallback(SynchronizationContext syncContext, T param, Action callback) + { + if (callback != null) + { + if (syncContext != null) + { + syncContext.Post(state => callback((T)state), param); + } + else + { + callback(param); + } + } + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Parser/ConditionalAttributeCollapser.cs b/src/Microsoft.AspNet.Razor/Parser/ConditionalAttributeCollapser.cs new file mode 100644 index 0000000000..5dcb5c37a3 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Parser/ConditionalAttributeCollapser.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using System.Linq; +using Microsoft.AspNet.Razor.Editor; +using Microsoft.AspNet.Razor.Generator; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Text; +using Microsoft.AspNet.Razor.Tokenizer; + +namespace Microsoft.AspNet.Razor.Parser +{ + internal class ConditionalAttributeCollapser : MarkupRewriter + { + public ConditionalAttributeCollapser(Action markupSpanFactory) : base(markupSpanFactory) + { + } + + protected override bool CanRewrite(Block block) + { + AttributeBlockCodeGenerator gen = block.CodeGenerator as AttributeBlockCodeGenerator; + return gen != null && block.Children.Any() && block.Children.All(IsLiteralAttributeValue); + } + + protected override SyntaxTreeNode RewriteBlock(BlockBuilder parent, Block block) + { + // Collect the content of this node + string content = String.Concat(block.Children.Cast().Select(s => s.Content)); + + // Create a new span containing this content + SpanBuilder span = new SpanBuilder(); + span.EditHandler = new SpanEditHandler(HtmlTokenizer.Tokenize); + FillSpan(span, block.Children.Cast().First().Start, content); + return span.Build(); + } + + private bool IsLiteralAttributeValue(SyntaxTreeNode node) + { + if (node.IsBlock) + { + return false; + } + Span span = node as Span; + Debug.Assert(span != null); + + LiteralAttributeCodeGenerator litGen = span.CodeGenerator as LiteralAttributeCodeGenerator; + + return span != null && + ((litGen != null && litGen.ValueGenerator == null) || + span.CodeGenerator == SpanCodeGenerator.Null || + span.CodeGenerator is MarkupCodeGenerator); + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Parser/HtmlLanguageCharacteristics.cs b/src/Microsoft.AspNet.Razor/Parser/HtmlLanguageCharacteristics.cs new file mode 100644 index 0000000000..8f44f89ad9 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Parser/HtmlLanguageCharacteristics.cs @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Resources; +using Microsoft.AspNet.Razor.Text; +using Microsoft.AspNet.Razor.Tokenizer; +using Microsoft.AspNet.Razor.Tokenizer.Symbols; + +namespace Microsoft.AspNet.Razor.Parser +{ + public class HtmlLanguageCharacteristics : LanguageCharacteristics + { + private static readonly HtmlLanguageCharacteristics _instance = new HtmlLanguageCharacteristics(); + + private HtmlLanguageCharacteristics() + { + } + + public static HtmlLanguageCharacteristics Instance + { + get { return _instance; } + } + + public override string GetSample(HtmlSymbolType type) + { + switch (type) + { + case HtmlSymbolType.Text: + return RazorResources.HtmlSymbol_Text; + case HtmlSymbolType.WhiteSpace: + return RazorResources.HtmlSymbol_WhiteSpace; + case HtmlSymbolType.NewLine: + return RazorResources.HtmlSymbol_NewLine; + case HtmlSymbolType.OpenAngle: + return "<"; + case HtmlSymbolType.Bang: + return "!"; + case HtmlSymbolType.Solidus: + return "/"; + case HtmlSymbolType.QuestionMark: + return "?"; + case HtmlSymbolType.DoubleHyphen: + return "--"; + case HtmlSymbolType.LeftBracket: + return "["; + case HtmlSymbolType.CloseAngle: + return ">"; + case HtmlSymbolType.RightBracket: + return "]"; + case HtmlSymbolType.Equals: + return "="; + case HtmlSymbolType.DoubleQuote: + return "\""; + case HtmlSymbolType.SingleQuote: + return "'"; + case HtmlSymbolType.Transition: + return "@"; + case HtmlSymbolType.Colon: + return ":"; + case HtmlSymbolType.RazorComment: + return RazorResources.HtmlSymbol_RazorComment; + case HtmlSymbolType.RazorCommentStar: + return "*"; + case HtmlSymbolType.RazorCommentTransition: + return "@"; + default: + return RazorResources.Symbol_Unknown; + } + } + + public override HtmlTokenizer CreateTokenizer(ITextDocument source) + { + return new HtmlTokenizer(source); + } + + public override HtmlSymbolType FlipBracket(HtmlSymbolType bracket) + { + switch (bracket) + { + case HtmlSymbolType.LeftBracket: + return HtmlSymbolType.RightBracket; + case HtmlSymbolType.OpenAngle: + return HtmlSymbolType.CloseAngle; + case HtmlSymbolType.RightBracket: + return HtmlSymbolType.LeftBracket; + case HtmlSymbolType.CloseAngle: + return HtmlSymbolType.OpenAngle; + default: + Debug.Fail("FlipBracket must be called with a bracket character"); + return HtmlSymbolType.Unknown; + } + } + + public override HtmlSymbol CreateMarkerSymbol(SourceLocation location) + { + return new HtmlSymbol(location, String.Empty, HtmlSymbolType.Unknown); + } + + public override HtmlSymbolType GetKnownSymbolType(KnownSymbolType type) + { + switch (type) + { + case KnownSymbolType.CommentStart: + return HtmlSymbolType.RazorCommentTransition; + case KnownSymbolType.CommentStar: + return HtmlSymbolType.RazorCommentStar; + case KnownSymbolType.CommentBody: + return HtmlSymbolType.RazorComment; + case KnownSymbolType.Identifier: + return HtmlSymbolType.Text; + case KnownSymbolType.Keyword: + return HtmlSymbolType.Text; + case KnownSymbolType.NewLine: + return HtmlSymbolType.NewLine; + case KnownSymbolType.Transition: + return HtmlSymbolType.Transition; + case KnownSymbolType.WhiteSpace: + return HtmlSymbolType.WhiteSpace; + default: + return HtmlSymbolType.Unknown; + } + } + + protected override HtmlSymbol CreateSymbol(SourceLocation location, string content, HtmlSymbolType type, IEnumerable errors) + { + return new HtmlSymbol(location, content, type, errors); + } + } +} diff --git a/src/Microsoft.AspNet.Razor/Parser/HtmlMarkupParser.Block.cs b/src/Microsoft.AspNet.Razor/Parser/HtmlMarkupParser.Block.cs new file mode 100644 index 0000000000..28f66c4cd0 --- /dev/null +++ b/src/Microsoft.AspNet.Razor/Parser/HtmlMarkupParser.Block.cs @@ -0,0 +1,832 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Microsoft.AspNet.Razor.Editor; +using Microsoft.AspNet.Razor.Generator; +using Microsoft.AspNet.Razor.Parser.SyntaxTree; +using Microsoft.AspNet.Razor.Resources; +using Microsoft.AspNet.Razor.Text; +using Microsoft.AspNet.Razor.Tokenizer.Symbols; + +namespace Microsoft.AspNet.Razor.Parser +{ + public partial class HtmlMarkupParser + { + private SourceLocation _lastTagStart = SourceLocation.Zero; + private HtmlSymbol _bufferedOpenAngle; + + public override void ParseBlock() + { + if (Context == null) + { + throw new InvalidOperationException(RazorResources.Parser_Context_Not_Set); + } + + using (PushSpanConfig(DefaultMarkupSpan)) + { + using (Context.StartBlock(BlockType.Markup)) + { + if (!NextToken()) + { + return; + } + + AcceptWhile(IsSpacingToken(includeNewLines: true)); + + if (CurrentSymbol.Type == HtmlSymbolType.OpenAngle) + { + // "<" => Implicit Tag Block + TagBlock(new Stack>()); + } + else if (CurrentSymbol.Type == HtmlSymbolType.Transition) + { + // "@" => Explicit Tag/Single Line Block OR Template + Output(SpanKind.Markup); + + // Definitely have a transition span + Assert(HtmlSymbolType.Transition); + AcceptAndMoveNext(); + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + Span.CodeGenerator = SpanCodeGenerator.Null; + Output(SpanKind.Transition); + if (At(HtmlSymbolType.Transition)) + { + Span.CodeGenerator = SpanCodeGenerator.Null; + AcceptAndMoveNext(); + Output(SpanKind.MetaCode); + } + AfterTransition(); + } + else + { + Context.OnError(CurrentSymbol.Start, RazorResources.ParseError_MarkupBlock_Must_Start_With_Tag); + } + Output(SpanKind.Markup); + } + } + } + + private void DefaultMarkupSpan(SpanBuilder span) + { + span.CodeGenerator = new MarkupCodeGenerator(); + span.EditHandler = new SpanEditHandler(Language.TokenizeString, AcceptedCharacters.Any); + } + + private void AfterTransition() + { + // "@:" => Explicit Single Line Block + if (CurrentSymbol.Type == HtmlSymbolType.Text && CurrentSymbol.Content.Length > 0 && CurrentSymbol.Content[0] == ':') + { + // Split the token + Tuple split = Language.SplitSymbol(CurrentSymbol, 1, HtmlSymbolType.Colon); + + // The first part (left) is added to this span and we return a MetaCode span + Accept(split.Item1); + Span.CodeGenerator = SpanCodeGenerator.Null; + Output(SpanKind.MetaCode); + if (split.Item2 != null) + { + Accept(split.Item2); + } + NextToken(); + SingleLineMarkup(); + } + else if (CurrentSymbol.Type == HtmlSymbolType.OpenAngle) + { + TagBlock(new Stack>()); + } + } + + private void SingleLineMarkup() + { + // Parse until a newline, it's that simple! + // First, signal to code parser that whitespace is significant to us. + bool old = Context.WhiteSpaceIsSignificantToAncestorBlock; + Context.WhiteSpaceIsSignificantToAncestorBlock = true; + Span.EditHandler = new SingleLineMarkupEditHandler(Language.TokenizeString); + SkipToAndParseCode(HtmlSymbolType.NewLine); + if (!EndOfFile && CurrentSymbol.Type == HtmlSymbolType.NewLine) + { + AcceptAndMoveNext(); + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + } + PutCurrentBack(); + Context.WhiteSpaceIsSignificantToAncestorBlock = old; + Output(SpanKind.Markup); + } + + private void TagBlock(Stack> tags) + { + // Skip Whitespace and Text + bool complete = false; + do + { + SkipToAndParseCode(HtmlSymbolType.OpenAngle); + if (EndOfFile) + { + EndTagBlock(tags, complete: true); + } + else + { + _bufferedOpenAngle = null; + _lastTagStart = CurrentLocation; + Assert(HtmlSymbolType.OpenAngle); + _bufferedOpenAngle = CurrentSymbol; + SourceLocation tagStart = CurrentLocation; + if (!NextToken()) + { + Accept(_bufferedOpenAngle); + EndTagBlock(tags, complete: false); + } + else + { + complete = AfterTagStart(tagStart, tags); + } + } + } + while (tags.Count > 0); + + EndTagBlock(tags, complete); + } + + private bool AfterTagStart(SourceLocation tagStart, Stack> tags) + { + if (!EndOfFile) + { + switch (CurrentSymbol.Type) + { + case HtmlSymbolType.Solidus: + // End Tag + return EndTag(tagStart, tags); + case HtmlSymbolType.Bang: + // Comment + Accept(_bufferedOpenAngle); + return BangTag(); + case HtmlSymbolType.QuestionMark: + // XML PI + Accept(_bufferedOpenAngle); + return XmlPI(); + default: + // Start Tag + return StartTag(tags); + } + } + if (tags.Count == 0) + { + Context.OnError(CurrentLocation, RazorResources.ParseError_OuterTagMissingName); + } + return false; + } + + private bool XmlPI() + { + // Accept "?" + Assert(HtmlSymbolType.QuestionMark); + AcceptAndMoveNext(); + return AcceptUntilAll(HtmlSymbolType.QuestionMark, HtmlSymbolType.CloseAngle); + } + + private bool BangTag() + { + // Accept "!" + Assert(HtmlSymbolType.Bang); + + if (AcceptAndMoveNext()) + { + if (CurrentSymbol.Type == HtmlSymbolType.DoubleHyphen) + { + AcceptAndMoveNext(); + return AcceptUntilAll(HtmlSymbolType.DoubleHyphen, HtmlSymbolType.CloseAngle); + } + else if (CurrentSymbol.Type == HtmlSymbolType.LeftBracket) + { + if (AcceptAndMoveNext()) + { + return CData(); + } + } + else + { + AcceptAndMoveNext(); + return AcceptUntilAll(HtmlSymbolType.CloseAngle); + } + } + + return false; + } + + private bool CData() + { + if (CurrentSymbol.Type == HtmlSymbolType.Text && String.Equals(CurrentSymbol.Content, "cdata", StringComparison.OrdinalIgnoreCase)) + { + if (AcceptAndMoveNext()) + { + if (CurrentSymbol.Type == HtmlSymbolType.LeftBracket) + { + return AcceptUntilAll(HtmlSymbolType.RightBracket, HtmlSymbolType.RightBracket, HtmlSymbolType.CloseAngle); + } + } + } + + return false; + } + + private bool EndTag(SourceLocation tagStart, Stack> tags) + { + // Accept "/" and move next + Assert(HtmlSymbolType.Solidus); + HtmlSymbol solidus = CurrentSymbol; + if (!NextToken()) + { + Accept(_bufferedOpenAngle); + Accept(solidus); + return false; + } + else + { + string tagName = String.Empty; + if (At(HtmlSymbolType.Text)) + { + tagName = CurrentSymbol.Content; + } + bool matched = RemoveTag(tags, tagName, tagStart); + + if (tags.Count == 0 && + String.Equals(tagName, SyntaxConstants.TextTagName, StringComparison.OrdinalIgnoreCase) && + matched) + { + Output(SpanKind.Markup); + return EndTextTag(solidus); + } + Accept(_bufferedOpenAngle); + Accept(solidus); + + AcceptUntil(HtmlSymbolType.CloseAngle); + + // Accept the ">" + return Optional(HtmlSymbolType.CloseAngle); + } + } + + private bool EndTextTag(HtmlSymbol solidus) + { + SourceLocation start = _bufferedOpenAngle.Start; + + Accept(_bufferedOpenAngle); + Accept(solidus); + + Assert(HtmlSymbolType.Text); + AcceptAndMoveNext(); + + bool seenCloseAngle = Optional(HtmlSymbolType.CloseAngle); + + if (!seenCloseAngle) + { + Context.OnError(start, RazorResources.ParseError_TextTagCannotContainAttributes); + } + else + { + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + } + + Span.CodeGenerator = SpanCodeGenerator.Null; + Output(SpanKind.Transition); + return seenCloseAngle; + } + + private bool IsTagRecoveryStopPoint(HtmlSymbol sym) + { + return sym.Type == HtmlSymbolType.CloseAngle || + sym.Type == HtmlSymbolType.Solidus || + sym.Type == HtmlSymbolType.OpenAngle || + sym.Type == HtmlSymbolType.SingleQuote || + sym.Type == HtmlSymbolType.DoubleQuote; + } + + private void TagContent() + { + if (!At(HtmlSymbolType.WhiteSpace)) + { + // We should be right after the tag name, so if there's no whitespace, something is wrong + RecoverToEndOfTag(); + } + else + { + // We are here ($): + while (!EndOfFile && !IsEndOfTag()) + { + BeforeAttribute(); + } + } + } + + private bool IsEndOfTag() + { + if (At(HtmlSymbolType.Solidus)) + { + if (NextIs(HtmlSymbolType.CloseAngle)) + { + return true; + } + else + { + AcceptAndMoveNext(); + } + } + return At(HtmlSymbolType.CloseAngle) || At(HtmlSymbolType.OpenAngle); + } + + private void BeforeAttribute() + { + // http://dev.w3.org/html5/spec/tokenization.html#before-attribute-name-state + // Capture whitespace + var whitespace = ReadWhile(sym => sym.Type == HtmlSymbolType.WhiteSpace || sym.Type == HtmlSymbolType.NewLine); + + if (At(HtmlSymbolType.Transition)) + { + // Transition outside of attribute value => Switch to recovery mode + Accept(whitespace); + RecoverToEndOfTag(); + return; + } + + // http://dev.w3.org/html5/spec/tokenization.html#attribute-name-state + // Read the 'name' (i.e. read until the '=' or whitespace/newline) + var name = Enumerable.Empty(); + if (At(HtmlSymbolType.Text)) + { + name = ReadWhile(sym => + sym.Type != HtmlSymbolType.WhiteSpace && + sym.Type != HtmlSymbolType.NewLine && + sym.Type != HtmlSymbolType.Equals && + sym.Type != HtmlSymbolType.CloseAngle && + sym.Type != HtmlSymbolType.OpenAngle && + (sym.Type != HtmlSymbolType.Solidus || !NextIs(HtmlSymbolType.CloseAngle))); + } + else + { + // Unexpected character in tag, enter recovery + Accept(whitespace); + RecoverToEndOfTag(); + return; + } + + if (!At(HtmlSymbolType.Equals)) + { + // Saw a space or newline after the name, so just skip this attribute and continue around the loop + Accept(whitespace); + Accept(name); + return; + } + + Output(SpanKind.Markup); + + // Start a new markup block for the attribute + using (Context.StartBlock(BlockType.Markup)) + { + AttributePrefix(whitespace, name); + } + } + + private void AttributePrefix(IEnumerable whitespace, IEnumerable nameSymbols) + { + // First, determine if this is a 'data-' attribute (since those can't use conditional attributes) + LocationTagged name = nameSymbols.GetContent(Span.Start); + bool attributeCanBeConditional = !name.Value.StartsWith("data-", StringComparison.OrdinalIgnoreCase); + + // Accept the whitespace and name + Accept(whitespace); + Accept(nameSymbols); + Assert(HtmlSymbolType.Equals); // We should be at "=" + AcceptAndMoveNext(); + HtmlSymbolType quote = HtmlSymbolType.Unknown; + if (At(HtmlSymbolType.SingleQuote) || At(HtmlSymbolType.DoubleQuote)) + { + quote = CurrentSymbol.Type; + AcceptAndMoveNext(); + } + + // We now have the prefix: (i.e. ' foo="') + LocationTagged prefix = Span.GetContent(); + + if (attributeCanBeConditional) + { + Span.CodeGenerator = SpanCodeGenerator.Null; // The block code generator will render the prefix + Output(SpanKind.Markup); + + // Read the values + while (!EndOfFile && !IsEndOfAttributeValue(quote, CurrentSymbol)) + { + AttributeValue(quote); + } + + // Capture the suffix + LocationTagged suffix = new LocationTagged(String.Empty, CurrentLocation); + if (quote != HtmlSymbolType.Unknown && At(quote)) + { + suffix = CurrentSymbol.GetContent(); + AcceptAndMoveNext(); + } + + if (Span.Symbols.Count > 0) + { + Span.CodeGenerator = SpanCodeGenerator.Null; // Again, block code generator will render the suffix + Output(SpanKind.Markup); + } + + // Create the block code generator + Context.CurrentBlock.CodeGenerator = new AttributeBlockCodeGenerator( + name, prefix, suffix); + } + else + { + // Not a "conditional" attribute, so just read the value + SkipToAndParseCode(sym => IsEndOfAttributeValue(quote, sym)); + if (quote != HtmlSymbolType.Unknown) + { + Optional(quote); + } + Output(SpanKind.Markup); + } + } + + private void AttributeValue(HtmlSymbolType quote) + { + SourceLocation prefixStart = CurrentLocation; + var prefix = ReadWhile(sym => sym.Type == HtmlSymbolType.WhiteSpace || sym.Type == HtmlSymbolType.NewLine); + Accept(prefix); + + if (At(HtmlSymbolType.Transition)) + { + SourceLocation valueStart = CurrentLocation; + PutCurrentBack(); + + // Output the prefix but as a null-span. DynamicAttributeBlockCodeGenerator will render it + Span.CodeGenerator = SpanCodeGenerator.Null; + + // Dynamic value, start a new block and set the code generator + using (Context.StartBlock(BlockType.Markup)) + { + Context.CurrentBlock.CodeGenerator = new DynamicAttributeBlockCodeGenerator(prefix.GetContent(prefixStart), valueStart); + + OtherParserBlock(); + } + } + else if (At(HtmlSymbolType.Text) && CurrentSymbol.Content.Length > 0 && CurrentSymbol.Content[0] == '~' && NextIs(HtmlSymbolType.Solidus)) + { + // Virtual Path value + SourceLocation valueStart = CurrentLocation; + VirtualPath(); + Span.CodeGenerator = new LiteralAttributeCodeGenerator( + prefix.GetContent(prefixStart), + new LocationTagged(new ResolveUrlCodeGenerator(), valueStart)); + } + else + { + // Literal value + // 'quote' should be "Unknown" if not quoted and symbols coming from the tokenizer should never have "Unknown" type. + var value = ReadWhile(sym => + // These three conditions find separators which break the attribute value into portions + sym.Type != HtmlSymbolType.WhiteSpace && + sym.Type != HtmlSymbolType.NewLine && + sym.Type != HtmlSymbolType.Transition && + // This condition checks for the end of the attribute value (it repeats some of the checks above but for now that's ok) + !IsEndOfAttributeValue(quote, sym)); + Accept(value); + Span.CodeGenerator = new LiteralAttributeCodeGenerator(prefix.GetContent(prefixStart), value.GetContent(prefixStart)); + } + Output(SpanKind.Markup); + } + + private bool IsEndOfAttributeValue(HtmlSymbolType quote, HtmlSymbol sym) + { + return EndOfFile || sym == null || + (quote != HtmlSymbolType.Unknown + ? sym.Type == quote // If quoted, just wait for the quote + : IsUnquotedEndOfAttributeValue(sym)); + } + + private bool IsUnquotedEndOfAttributeValue(HtmlSymbol sym) + { + // If unquoted, we have a larger set of terminating characters: + // http://dev.w3.org/html5/spec/tokenization.html#attribute-value-unquoted-state + // Also we need to detect "/" and ">" + return sym.Type == HtmlSymbolType.DoubleQuote || + sym.Type == HtmlSymbolType.SingleQuote || + sym.Type == HtmlSymbolType.OpenAngle || + sym.Type == HtmlSymbolType.Equals || + (sym.Type == HtmlSymbolType.Solidus && NextIs(HtmlSymbolType.CloseAngle)) || + sym.Type == HtmlSymbolType.CloseAngle || + sym.Type == HtmlSymbolType.WhiteSpace || + sym.Type == HtmlSymbolType.NewLine; + } + + private void VirtualPath() + { + Assert(HtmlSymbolType.Text); + Debug.Assert(CurrentSymbol.Content.Length > 0 && CurrentSymbol.Content[0] == '~'); + + // Parse until a transition symbol, whitespace, newline or quote. We support only a fairly minimal subset of Virtual Paths + AcceptUntil(HtmlSymbolType.Transition, HtmlSymbolType.WhiteSpace, HtmlSymbolType.NewLine, HtmlSymbolType.SingleQuote, HtmlSymbolType.DoubleQuote); + + // Output a Virtual Path span + Span.EditHandler.EditorHints = EditorHints.VirtualPath; + } + + private void RecoverToEndOfTag() + { + // Accept until ">", "/" or "<", but parse code + while (!EndOfFile) + { + SkipToAndParseCode(IsTagRecoveryStopPoint); + if (!EndOfFile) + { + EnsureCurrent(); + switch (CurrentSymbol.Type) + { + case HtmlSymbolType.SingleQuote: + case HtmlSymbolType.DoubleQuote: + ParseQuoted(); + break; + case HtmlSymbolType.OpenAngle: + // Another "<" means this tag is invalid. + case HtmlSymbolType.Solidus: + // Empty tag + case HtmlSymbolType.CloseAngle: + // End of tag + return; + default: + AcceptAndMoveNext(); + break; + } + } + } + } + + private void ParseQuoted() + { + HtmlSymbolType type = CurrentSymbol.Type; + AcceptAndMoveNext(); + ParseQuoted(type); + } + + private void ParseQuoted(HtmlSymbolType type) + { + SkipToAndParseCode(type); + if (!EndOfFile) + { + Assert(type); + AcceptAndMoveNext(); + } + } + + private bool StartTag(Stack> tags) + { + // If we're at text, it's the name, otherwise the name is "" + HtmlSymbol tagName; + if (At(HtmlSymbolType.Text)) + { + tagName = CurrentSymbol; + } + else + { + tagName = new HtmlSymbol(CurrentLocation, String.Empty, HtmlSymbolType.Unknown); + } + + Tuple tag = Tuple.Create(tagName, _lastTagStart); + + if (tags.Count == 0 && String.Equals(tag.Item1.Content, SyntaxConstants.TextTagName, StringComparison.OrdinalIgnoreCase)) + { + Output(SpanKind.Markup); + Span.CodeGenerator = SpanCodeGenerator.Null; + + Accept(_bufferedOpenAngle); + Assert(HtmlSymbolType.Text); + + AcceptAndMoveNext(); + + int bookmark = CurrentLocation.AbsoluteIndex; + IEnumerable tokens = ReadWhile(IsSpacingToken(includeNewLines: true)); + bool empty = At(HtmlSymbolType.Solidus); + if (empty) + { + Accept(tokens); + Assert(HtmlSymbolType.Solidus); + AcceptAndMoveNext(); + bookmark = CurrentLocation.AbsoluteIndex; + tokens = ReadWhile(IsSpacingToken(includeNewLines: true)); + } + + if (!Optional(HtmlSymbolType.CloseAngle)) + { + Context.Source.Position = bookmark; + NextToken(); + Context.OnError(tag.Item2, RazorResources.ParseError_TextTagCannotContainAttributes); + } + else + { + Accept(tokens); + Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None; + } + + if (!empty) + { + tags.Push(tag); + } + Output(SpanKind.Transition); + return true; + } + Accept(_bufferedOpenAngle); + Optional(HtmlSymbolType.Text); + return RestOfTag(tag, tags); + } + + private bool RestOfTag(Tuple tag, Stack> 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 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 void SkipToEndScriptAndParseCode() + { + // Special case for