Add line-mapping support to RazorSourceDocument
This commit is contained in:
parent
b473101927
commit
b8e1fb8011
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue