Add line-mapping support to RazorSourceDocument

This commit is contained in:
Ryan Nowak 2016-12-16 18:07:10 -08:00
parent b473101927
commit b8e1fb8011
4 changed files with 429 additions and 0 deletions

View File

@ -2,13 +2,17 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using Microsoft.AspNetCore.Razor.Evolution.Legacy;
namespace Microsoft.AspNetCore.Razor.Evolution
{
internal class DefaultRazorSourceDocument : RazorSourceDocument
{
private readonly string _content;
private readonly RazorSourceLineCollection _lines;
public DefaultRazorSourceDocument(string content, Encoding encoding, string filename)
{
@ -25,6 +29,8 @@ namespace Microsoft.AspNetCore.Razor.Evolution
_content = content;
Encoding = encoding;
Filename = filename;
_lines = new LineCollection(this, LineCollection.GetLineStarts(content));
}
public override char this[int position] => _content[position];
@ -35,6 +41,8 @@ namespace Microsoft.AspNetCore.Razor.Evolution
public override int Length => _content.Length;
public override RazorSourceLineCollection Lines => _lines;
public override void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count)
{
if (destination == null)
@ -64,5 +72,118 @@ namespace Microsoft.AspNetCore.Razor.Evolution
_content.CopyTo(sourceIndex, destination, destinationIndex, count);
}
private class LineCollection : RazorSourceLineCollection
{
private readonly DefaultRazorSourceDocument _document;
private readonly int[] _lineStarts;
public LineCollection(DefaultRazorSourceDocument document, int[] lineStarts)
{
_document = document;
_lineStarts = lineStarts;
}
public override int Count => _lineStarts.Length;
public override int GetLineLength(int index)
{
if (index < 0 || index >= _lineStarts.Length)
{
throw new IndexOutOfRangeException(nameof(index));
}
if (index == _lineStarts.Length - 1)
{
// Last line is special.
return _document.Length - _lineStarts[index];
}
return _lineStarts[index + 1] - _lineStarts[index];
}
internal override SourceLocation GetLocation(int position)
{
if (position < 0 || position >= _document.Length)
{
throw new IndexOutOfRangeException(nameof(position));
}
var index = Array.BinarySearch<int>(_lineStarts, position);
if (index >= 0)
{
// We have an exact match for the start of a line.
Debug.Assert(_lineStarts[index] == position);
return new SourceLocation(_document.Filename, position, index, characterIndex: 0);
}
// Index is the complement of the line *after* the one we want, because BinarySearch tells
// us where we'd put position *if* it were the start of a line.
index = (~index) - 1;
if (index == -1)
{
// There's no preceding line, so it's based on the start of the string
return new SourceLocation(_document.Filename, position, 0, position);
}
else
{
var characterIndex = position - _lineStarts[index];
return new SourceLocation(_document.Filename, position, index, characterIndex);
}
}
public static int[] GetLineStarts(string text)
{
var starts = new List<int>();
// We always consider a document to have at least a 0th line, even if it's empty.
starts.Add(0);
var unprocessedCR = false;
// Length - 1 because we don't care if there was a linebreak as the last character.
for (var i = 0; i < text.Length - 1; i++)
{
var c = text[i];
var isLineBreak = false;
switch (c)
{
case '\r':
unprocessedCR = true;
continue;
case '\n':
unprocessedCR = false;
isLineBreak = true;
break;
case '\u0085':
case '\u2028':
case '\u2029':
isLineBreak = true;
break;
}
if (unprocessedCR)
{
// If we get here it means that we had a CR followed by something other than an LF.
// Add the CR as a line break.
starts.Add(i);
unprocessedCR = false;
}
if (isLineBreak)
{
starts.Add(i + 1);
}
}
return starts.ToArray();
}
}
}
}

View File

@ -17,6 +17,8 @@ namespace Microsoft.AspNetCore.Razor.Evolution
public abstract int Length { get; }
public abstract RazorSourceLineCollection Lines { get; }
public abstract void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count);
public static RazorSourceDocument ReadFrom(Stream stream, string filename)

View File

@ -0,0 +1,16 @@
// 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 Microsoft.AspNetCore.Razor.Evolution.Legacy;
namespace Microsoft.AspNetCore.Razor.Evolution
{
public abstract class RazorSourceLineCollection
{
public abstract int Count { get; }
public abstract int GetLineLength(int index);
internal abstract SourceLocation GetLocation(int position);
}
}

View File

@ -151,5 +151,295 @@ namespace Microsoft.AspNetCore.Razor.Evolution
Assert.Equal("Hi", copiedContent);
}
}
[Fact]
public void Lines_Count_EmptyDocument()
{
// Arrange
var content = string.Empty;
var document = new DefaultRazorSourceDocument(content, Encoding.UTF8, filename: null);
// Act
var actual = document.Lines.Count;
// Assert
Assert.Equal(1, actual);
}
[Fact]
public void Lines_GetLineLength_EmptyDocument()
{
// Arrange
var content = string.Empty;
var document = new DefaultRazorSourceDocument(content, Encoding.UTF8, filename: null);
// Act
var actual = document.Lines.GetLineLength(0);
// Assert
Assert.Equal(0, actual);
}
[Fact]
public void Lines_GetLineLength_TrailingNewlineDoesNotStartNewLine()
{
// Arrange
var content = "hello\n";
var document = new DefaultRazorSourceDocument(content, Encoding.UTF8, filename: null);
// Act
var actual = document.Lines.GetLineLength(0);
// Assert
Assert.Equal(6, actual);
}
[Fact]
public void Lines_GetLineLength_TrailingNewlineDoesNotStartNewLine_CRLF()
{
// Arrange
var content = "hello\r\n";
var document = new DefaultRazorSourceDocument(content, Encoding.UTF8, filename: null);
// Act
var actual = document.Lines.GetLineLength(0);
// Assert
Assert.Equal(7, actual);
}
[Fact]
public void Lines_Simple_Document()
{
// Arrange
var content = new StringBuilder()
.Append("The quick brown").Append('\n')
.Append("fox").Append("\r\n")
.Append("jumps over the lazy dog.")
.ToString();
var document = new DefaultRazorSourceDocument(content, Encoding.UTF8, filename: null);
// Act
var actual = GetAllLineMappings(document);
// Assert
Assert.Equal(new int[]{ 16, 5, 24 }, actual);
}
[Fact]
public void Lines_CRLF_OnlyCountsAsASingleNewLine()
{
// Arrange
var content = "Hello\r\nWorld!";
var document = new DefaultRazorSourceDocument(content, Encoding.UTF8, filename: null);
// Act
var actual = GetAllLineMappings(document);
// Assert
Assert.Equal(new int[] { 7, 6 }, actual);
}
[Fact]
public void Lines_CR_IsNewLine()
{
// Arrange
var content = "Hello\rWorld!";
var document = new DefaultRazorSourceDocument(content, Encoding.UTF8, filename: null);
// Act
var actual = GetAllLineMappings(document);
// Assert
Assert.Equal(new int[] { 6, 6 }, actual);
}
// CR handling is stateful in the parser, making sure we properly reset the state.
[Fact]
public void Lines_CR_IsNewLine_MultipleCRs()
{
// Arrange
var content = "Hello\rBig\r\nWorld!";
var document = new DefaultRazorSourceDocument(content, Encoding.UTF8, filename: null);
// Act
var actual = GetAllLineMappings(document);
// Assert
Assert.Equal(new int[] { 6, 5, 6 }, actual);
}
[Fact]
public void Lines_LF_IsNewLine()
{
// Arrange
var content = "Hello\nWorld!";
var document = new DefaultRazorSourceDocument(content, Encoding.UTF8, filename: null);
// Act
var actual = GetAllLineMappings(document);
// Assert
Assert.Equal(new int[] { 6, 6 }, actual);
}
[Fact]
public void Lines_Unicode0085_IsNewLine()
{
// Arrange
var content = "Hello\u0085World!";
var document = new DefaultRazorSourceDocument(content, Encoding.UTF8, filename: null);
// Act
var actual = GetAllLineMappings(document);
// Assert
Assert.Equal(new int[] { 6, 6 }, actual);
}
[Fact]
public void Lines_Unicode2028_IsNewLine()
{
// Arrange
var content = "Hello\u2028World!";
var document = new DefaultRazorSourceDocument(content, Encoding.UTF8, filename: null);
// Act
var actual = GetAllLineMappings(document);
// Assert
Assert.Equal(new int[] { 6, 6 }, actual);
}
[Fact]
public void Lines_Unicode2029_IsNewLine()
{
// Arrange
var content = "Hello\u2029World!";
var document = new DefaultRazorSourceDocument(content, Encoding.UTF8, filename: null);
// Act
var actual = GetAllLineMappings(document);
// Assert
Assert.Equal(new int[] { 6, 6 }, actual);
}
[Fact]
public void Lines_GetLocation_IncludesAbsoluteIndexAndDocument()
{
// Arrange
var content = "Hello, World!";
var document = new DefaultRazorSourceDocument(content, Encoding.UTF8, filename: "Hi.cshtml");
// Act
var actual = document.Lines.GetLocation(1);
// Assert
Assert.Equal("Hi.cshtml", actual.FilePath);
Assert.Equal(1, actual.AbsoluteIndex);
}
// Beginnings of lines are special because the BinarySearch in the implementation
// will succeed. It's a different code path.
[Fact]
public void Lines_GetLocation_FirstCharacter()
{
// Arrange
var content = "Hello\nBig\r\nWorld!";
var document = new DefaultRazorSourceDocument(content, Encoding.UTF8, filename: null);
// Act
var actual = document.Lines.GetLocation(0);
// Assert
Assert.Equal(0, actual.LineIndex);
Assert.Equal(0, actual.CharacterIndex);
}
[Fact]
public void Lines_GetLocation_EndOfFirstLine()
{
// Arrange
var content = "Hello\nBig\r\nWorld!";
var document = new DefaultRazorSourceDocument(content, Encoding.UTF8, filename: null);
// Act
var actual = document.Lines.GetLocation(5);
// Assert
Assert.Equal(0, actual.LineIndex);
Assert.Equal(5, actual.CharacterIndex);
}
[Fact]
public void Lines_GetLocation_InteriorLine()
{
// Arrange
var content = "Hello\nBig\r\nWorld!";
var document = new DefaultRazorSourceDocument(content, Encoding.UTF8, filename: null);
// Act
var actual = document.Lines.GetLocation(7);
// Assert
Assert.Equal(1, actual.LineIndex);
Assert.Equal(1, actual.CharacterIndex);
}
[Fact]
public void Lines_GetLocation_StartOfLastLine()
{
// Arrange
var content = "Hello\nBig\r\nWorld!";
var document = new DefaultRazorSourceDocument(content, Encoding.UTF8, filename: null);
// Act
var actual = document.Lines.GetLocation(11);
// Assert
Assert.Equal(2, actual.LineIndex);
Assert.Equal(0, actual.CharacterIndex);
}
[Fact]
public void Lines_GetLocation_EndOfLastLine()
{
// Arrange
var content = "Hello\nBig\r\nWorld!";
var document = new DefaultRazorSourceDocument(content, Encoding.UTF8, filename: null);
// Act
var actual = document.Lines.GetLocation(16);
// Assert
Assert.Equal(2, actual.LineIndex);
Assert.Equal(5, actual.CharacterIndex);
}
private static int[] GetAllLineMappings(RazorSourceDocument source)
{
var lines = new int[source.Lines.Count];
for (var i = 0; i < lines.Length; i++)
{
lines[i] = source.Lines.GetLineLength(i);
}
return lines;
}
}
}