aspnetcore/src/Microsoft.AspNet.Razor/RazorTemplateEngine.cs

376 lines
16 KiB
C#

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using Microsoft.AspNet.Razor.Chunks.Generators;
using Microsoft.AspNet.Razor.CodeGeneration;
using Microsoft.AspNet.Razor.Parser;
using Microsoft.AspNet.Razor.Text;
using Microsoft.Framework.Internal;
namespace Microsoft.AspNet.Razor
{
/// <summary>
/// Entry-point to the Razor Template Engine
/// </summary>
public class RazorTemplateEngine
{
private const int BufferSize = 1024;
public static readonly string DefaultClassName = "Template";
public static readonly string DefaultNamespace = string.Empty;
/// <summary>
/// Constructs a new RazorTemplateEngine with the specified host
/// </summary>
/// <param name="host">
/// The host which defines the environment in which the generated template code will live.
/// </param>
public RazorTemplateEngine([NotNull] RazorEngineHost host)
{
Host = host;
}
/// <summary>
/// The RazorEngineHost which defines the environment in which the generated template code will live
/// </summary>
public RazorEngineHost Host { get; }
public ParserResults ParseTemplate([NotNull] ITextBuffer input)
{
return ParseTemplate(input, cancelToken: null);
}
/// <summary>
/// Parses the template specified by the TextBuffer and returns it's result
/// </summary>
/// <remarks>
/// <para>
/// IMPORTANT: This does NOT need to be called before GeneratedCode! GenerateCode will automatically
/// parse the document first.
/// </para>
/// <para>
/// The cancel token provided can be used to cancel the parse. However, please note
/// that the parse occurs _synchronously_, on the callers thread. This parameter is
/// provided so that if the caller is in a background thread with a CancellationToken,
/// it can pass it along to the parser.
/// </para>
/// </remarks>
/// <param name="input">The input text to parse.</param>
/// <param name="cancelToken">A token used to cancel the parser.</param>
/// <returns>The resulting parse tree.</returns>
[SuppressMessage(
"Microsoft.Reliability", "CA2000:Dispose objects before losing scope",
Justification = "Input object would be disposed if we dispose the wrapper. We don't own the input so " +
"we don't want to dispose it")]
public ParserResults ParseTemplate([NotNull] ITextBuffer input, CancellationToken? cancelToken)
{
return ParseTemplateCore(input.ToDocument(), sourceFileName: null, cancelToken: cancelToken);
}
// See ParseTemplate(ITextBuffer, CancellationToken?),
// this overload simply wraps a TextReader in a TextBuffer (see ITextBuffer and BufferingTextReader)
public ParserResults ParseTemplate([NotNull] TextReader input, string sourceFileName)
{
return ParseTemplateCore(new SeekableTextReader(input), sourceFileName, cancelToken: null);
}
[SuppressMessage(
"Microsoft.Reliability",
"CA2000:Dispose objects before losing scope",
Justification = "Input object would be disposed if we dispose the wrapper. We don't own the input so " +
"we don't want to dispose it")]
public ParserResults ParseTemplate([NotNull] TextReader input, CancellationToken? cancelToken)
{
return ParseTemplateCore(new SeekableTextReader(input), sourceFileName: null, cancelToken: cancelToken);
}
protected internal virtual ParserResults ParseTemplateCore(
[NotNull] ITextDocument input,
string sourceFileName,
CancellationToken? cancelToken)
{
// Construct the parser
var parser = CreateParser(sourceFileName);
Debug.Assert(parser != null);
return parser.Parse(input);
}
public GeneratorResults GenerateCode([NotNull] ITextBuffer input)
{
return GenerateCode(input, className: null, rootNamespace: null, sourceFileName: null, cancelToken: null);
}
public GeneratorResults GenerateCode([NotNull] ITextBuffer input, CancellationToken? cancelToken)
{
return GenerateCode(
input,
className: null,
rootNamespace: null,
sourceFileName: null,
cancelToken: cancelToken);
}
public GeneratorResults GenerateCode(
[NotNull] ITextBuffer input,
string className,
string rootNamespace,
string sourceFileName)
{
return GenerateCode(input, className, rootNamespace, sourceFileName, cancelToken: null);
}
/// <summary>
/// Parses the template specified by the TextBuffer, generates code for it, and returns the constructed code.
/// </summary>
/// <remarks>
/// <para>
/// The cancel token provided can be used to cancel the parse. However, please note
/// that the parse occurs _synchronously_, on the callers thread. This parameter is
/// provided so that if the caller is in a background thread with a CancellationToken,
/// it can pass it along to the parser.
/// </para>
/// <para>
/// The className, rootNamespace and sourceFileName parameters are optional and override the default
/// specified by the Host. For example, the WebPageRazorHost in System.Web.WebPages.Razor configures the
/// Class Name, Root Namespace and Source File Name based on the virtual path of the page being compiled.
/// However, the built-in RazorEngineHost class uses constant defaults, so the caller will likely want to
/// change them using these parameters.
/// </para>
/// </remarks>
/// <param name="input">The input text to parse.</param>
/// <param name="cancelToken">A token used to cancel the parser.</param>
/// <param name="className">
/// The name of the generated class, overriding whatever is specified in the Host. The default value (defined
/// in the Host) can be used by providing null for this argument.
/// </param>
/// <param name="rootNamespace">The namespace in which the generated class will reside, overriding whatever is
/// specified in the Host. The default value (defined in the Host) can be used by providing null for this
/// argument.
/// </param>
/// <param name="sourceFileName">
/// The file name to use in line pragmas, usually the original Razor file, overriding whatever is specified in
/// the Host. The default value (defined in the Host) can be used by providing null for this argument.
/// </param>
/// <returns>The resulting parse tree AND generated code.</returns>
[SuppressMessage(
"Microsoft.Reliability",
"CA2000:Dispose objects before losing scope",
Justification = "Input object would be disposed if we dispose the wrapper. We don't own the input so " +
"we don't want to dispose it")]
public GeneratorResults GenerateCode(
[NotNull] ITextBuffer input,
string className,
string rootNamespace,
string sourceFileName,
CancellationToken? cancelToken)
{
return GenerateCodeCore(
input.ToDocument(),
className,
rootNamespace,
sourceFileName,
checksum: null,
cancelToken: cancelToken);
}
// See GenerateCode override which takes ITextBuffer, and BufferingTextReader for details.
public GeneratorResults GenerateCode([NotNull] TextReader input)
{
return GenerateCode(input, className: null, rootNamespace: null, sourceFileName: null, cancelToken: null);
}
public GeneratorResults GenerateCode([NotNull] TextReader input, CancellationToken? cancelToken)
{
return GenerateCode(
input,
className: null,
rootNamespace: null,
sourceFileName: null,
cancelToken: cancelToken);
}
public GeneratorResults GenerateCode(
[NotNull] TextReader input,
string className,
string rootNamespace, string sourceFileName)
{
return GenerateCode(input, className, rootNamespace, sourceFileName, cancelToken: null);
}
/// <summary>
/// Parses the contents specified by the <paramref name="inputStream"/> and returns the generated code.
/// </summary>
/// <param name="inputStream">A <see cref="Stream"/> that represents the contents to be parsed.</param>
/// <param name="className">The name of the generated class. When <c>null</c>, defaults to
/// <see cref="RazorEngineHost.DefaultClassName"/> (<c>Host.DefaultClassName</c>).</param>
/// <param name="rootNamespace">The namespace in which the generated class will reside. When <c>null</c>,
/// defaults to <see cref="RazorEngineHost.DefaultNamespace"/> (<c>Host.DefaultNamespace</c>).</param>
/// <param name="sourceFileName">
/// The file name to use in line pragmas, usually the original Razor file.
/// </param>
/// <returns>A <see cref="GeneratorResults"/> that represents the results of parsing the content.</returns>
/// <remarks>
/// This overload calculates the checksum of the contents of <paramref name="inputStream"/> prior to code
/// generation. The checksum is used for producing the <c>#pragma checksum</c> line pragma required for
/// debugging.
/// </remarks>
public GeneratorResults GenerateCode(
[NotNull] Stream inputStream,
string className,
string rootNamespace,
string sourceFileName)
{
MemoryStream memoryStream = null;
string checksum = null;
try
{
if (!Host.DesignTimeMode)
{
// We don't need to calculate the checksum in design time.
if (!inputStream.CanSeek)
{
memoryStream = new MemoryStream();
inputStream.CopyTo(memoryStream);
// We don't have to dispose the input stream since it is owned externally.
inputStream = memoryStream;
}
inputStream.Position = 0;
checksum = ComputeChecksum(inputStream);
inputStream.Position = 0;
}
using (var reader =
new StreamReader(
inputStream,
Encoding.UTF8,
detectEncodingFromByteOrderMarks: true,
bufferSize: BufferSize,
leaveOpen: true))
{
var seekableStream = new SeekableTextReader(reader);
return GenerateCodeCore(
seekableStream,
className,
rootNamespace,
sourceFileName,
checksum,
cancelToken: null);
}
}
finally
{
if (memoryStream != null)
{
memoryStream.Dispose();
}
}
}
[SuppressMessage(
"Microsoft.Reliability",
"CA2000:Dispose objects before losing scope",
Justification = "Input object would be disposed if we dispose the wrapper. We don't own the input so " +
"we don't want to dispose it")]
public GeneratorResults GenerateCode(
[NotNull] TextReader input,
string className,
string rootNamespace,
string sourceFileName,
CancellationToken? cancelToken)
{
return GenerateCodeCore(
new SeekableTextReader(input),
className,
rootNamespace,
sourceFileName,
checksum: null,
cancelToken: cancelToken);
}
protected internal virtual GeneratorResults GenerateCodeCore(
[NotNull] ITextDocument input,
string className,
string rootNamespace,
string sourceFileName,
string checksum,
CancellationToken? cancelToken)
{
className = (className ?? Host.DefaultClassName) ?? DefaultClassName;
rootNamespace = (rootNamespace ?? Host.DefaultNamespace) ?? DefaultNamespace;
// Run the parser
var parser = CreateParser(sourceFileName);
Debug.Assert(parser != null);
var results = parser.Parse(input);
// Generate code
var chunkGenerator = CreateChunkGenerator(className, rootNamespace, sourceFileName);
chunkGenerator.DesignTimeMode = Host.DesignTimeMode;
chunkGenerator.Visit(results);
var codeGeneratorContext = new CodeGeneratorContext(chunkGenerator.Context, results.ErrorSink);
codeGeneratorContext.Checksum = checksum;
var codeGenerator = CreateCodeGenerator(codeGeneratorContext);
var codeGeneratorResult = codeGenerator.Generate();
// Collect results and return
return new GeneratorResults(results, codeGeneratorResult, codeGeneratorContext.ChunkTreeBuilder.ChunkTree);
}
protected internal virtual RazorChunkGenerator CreateChunkGenerator(
string className,
string rootNamespace,
string sourceFileName)
{
return Host.DecorateChunkGenerator(
Host.CodeLanguage.CreateChunkGenerator(className, rootNamespace, sourceFileName, Host));
}
protected internal virtual RazorParser CreateParser(string sourceFileName)
{
var codeParser = Host.CodeLanguage.CreateCodeParser();
var markupParser = Host.CreateMarkupParser();
var parser = new RazorParser(
Host.DecorateCodeParser(codeParser),
Host.DecorateMarkupParser(markupParser),
Host.TagHelperDescriptorResolver)
{
DesignTimeMode = Host.DesignTimeMode
};
return Host.DecorateRazorParser(parser, sourceFileName);
}
protected internal virtual CodeGenerator CreateCodeGenerator(CodeGeneratorContext context)
{
return Host.DecorateCodeGenerator(Host.CodeLanguage.CreateCodeGenerator(context), context);
}
private static string ComputeChecksum(Stream inputStream)
{
byte[] hashedBytes;
using (var hashAlgorithm = SHA1.Create())
{
hashedBytes = hashAlgorithm.ComputeHash(inputStream);
}
var fileHashBuilder = new StringBuilder(hashedBytes.Length * 2);
foreach (var value in hashedBytes)
{
fileHashBuilder.Append(value.ToString("x2"));
}
return fileHashBuilder.ToString();
}
}
}