Export to Markdown Support (#1705)

* Adding file writer for Markdown tables. No testing yet.

* Unit tests for the markdown writer

* Wiring up the factory and and request types

* Wiring up changes for Markdown serialization in serialization service

* Couple last minute tweaks

* Changes as per PR comments

* Revert temp testing code. 🙈

* Fluent assertions in SerializationServiceTests.cs

Co-authored-by: Ben Russell <russellben@microsoft.com>
This commit is contained in:
Benjamin Russell
2022-09-27 13:55:43 -05:00
committed by GitHub
parent 5c20f92312
commit af2c0c77e7
10 changed files with 905 additions and 54 deletions

View File

@@ -0,0 +1,101 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage
{
/// <summary>
/// Writer for exporting results to a Markdown table.
/// </summary>
public class SaveAsMarkdownFileStreamWriter : SaveAsStreamWriter
{
private const string Delimiter = "|";
private static readonly Regex NewlineRegex = new Regex(@"(\r\n|\n|\r)", RegexOptions.Compiled);
private readonly Encoding _encoding;
private readonly string _lineSeparator;
public SaveAsMarkdownFileStreamWriter(
Stream stream,
SaveResultsAsMarkdownRequestParams requestParams,
IReadOnlyList<DbColumnWrapper> columns)
: base(stream, requestParams, columns)
{
// Parse the request params
this._lineSeparator = string.IsNullOrEmpty(requestParams.LineSeparator)
? Environment.NewLine
: requestParams.LineSeparator;
this._encoding = ParseEncoding(requestParams.Encoding, Encoding.UTF8);
// Output the header if requested
if (requestParams.IncludeHeaders)
{
// Write the column header
IEnumerable<string> selectedColumnNames = columns.Skip(this.ColumnStartIndex)
.Take(this.ColumnCount)
.Select(c => EncodeMarkdownField(c.ColumnName));
string headerLine = string.Join(Delimiter, selectedColumnNames);
this.WriteLine($"{Delimiter}{headerLine}{Delimiter}");
// Write the separator row
var separatorBuilder = new StringBuilder(Delimiter);
for (int i = 0; i < this.ColumnCount; i++)
{
separatorBuilder.Append($"---{Delimiter}");
}
this.WriteLine(separatorBuilder.ToString());
}
}
/// <inheritdoc />
public override void WriteRow(IList<DbCellValue> row, IReadOnlyList<DbColumnWrapper> columns)
{
IEnumerable<string> selectedCells = row.Skip(this.ColumnStartIndex)
.Take(this.ColumnCount)
.Select(c => EncodeMarkdownField(c.DisplayValue));
string rowLine = string.Join(Delimiter, selectedCells);
this.WriteLine($"{Delimiter}{rowLine}{Delimiter}");
}
internal static string EncodeMarkdownField(string? field)
{
// Special case for nulls
if (field == null)
{
return "NULL";
}
// Escape HTML entities, since Markdown supports inline HTML
field = HttpUtility.HtmlEncode(field);
// Escape pipe delimiters
field = field.Replace(@"|", @"\|");
// @TODO: Allow option to encode multiple whitespace characters as &nbsp;
// Replace newlines with br tags, since cell values must be single line
field = NewlineRegex.Replace(field, @"<br />");
return field;
}
private void WriteLine(string line)
{
byte[] bytes = this._encoding.GetBytes(line + this._lineSeparator);
this.FileStream.Write(bytes);
}
}
}