mirror of
https://github.com/radzenhq/radzen-blazor.git
synced 2026-02-04 05:35:44 +00:00
Compare commits
140 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c456891a11 | ||
|
|
6b044d8086 | ||
|
|
3162de84ed | ||
|
|
641768240a | ||
|
|
8b7ade4b9c | ||
|
|
c7db9394c8 | ||
|
|
513e63329b | ||
|
|
cfd104385d | ||
|
|
2d9641eecf | ||
|
|
65a78125b2 | ||
|
|
28572ba4d3 | ||
|
|
28a603ca1e | ||
|
|
b9fa303f7f | ||
|
|
756dde90ab | ||
|
|
c2a396167e | ||
|
|
c1fd207723 | ||
|
|
8f1fc0a164 | ||
|
|
538ec3c744 | ||
|
|
6fed13bf12 | ||
|
|
104cc7c900 | ||
|
|
8fa699a92e | ||
|
|
1ab7059830 | ||
|
|
377b2613db | ||
|
|
bc7b0a9bdb | ||
|
|
8665a351db | ||
|
|
dd02bf8b8d | ||
|
|
487c423eef | ||
|
|
87bcfa729c | ||
|
|
0332e8c671 | ||
|
|
7264354ce6 | ||
|
|
bde3315994 | ||
|
|
7ecd08b0d8 | ||
|
|
93feb382ca | ||
|
|
186d9b3798 | ||
|
|
1f3e44819d | ||
|
|
4b942f2f45 | ||
|
|
7214cd7179 | ||
|
|
90c11b5c04 | ||
|
|
4007339d26 | ||
|
|
8bc43443a7 | ||
|
|
a389dc702b | ||
|
|
1b39fa37e0 | ||
|
|
bfa18f72fa | ||
|
|
82c2ec0c43 | ||
|
|
3810d088b5 | ||
|
|
7b34b096fe | ||
|
|
831b0bd2d1 | ||
|
|
5875057282 | ||
|
|
b78df8df2a | ||
|
|
7fa3d08e61 | ||
|
|
549303a34c | ||
|
|
9d2cbae115 | ||
|
|
d1a76922c5 | ||
|
|
648889d2d2 | ||
|
|
984e566fe2 | ||
|
|
145296ee10 | ||
|
|
91b91ca96f | ||
|
|
5ea1e9d6d5 | ||
|
|
78f83fe103 | ||
|
|
1b0ee6a757 | ||
|
|
c185853405 | ||
|
|
f50b8bceb6 | ||
|
|
559a10603a | ||
|
|
defe38daaa | ||
|
|
6acdaf0603 | ||
|
|
e9991fc995 | ||
|
|
7ad14174d7 | ||
|
|
a638387dd4 | ||
|
|
3818d0e607 | ||
|
|
3ce4f8da3a | ||
|
|
9d272a1b19 | ||
|
|
3b9224b4da | ||
|
|
b254152746 | ||
|
|
a2d796476e | ||
|
|
898e744767 | ||
|
|
9ef9c5b3de | ||
|
|
b734eeb252 | ||
|
|
92d69d9053 | ||
|
|
4879c49476 | ||
|
|
e81ea15bb0 | ||
|
|
647f174b53 | ||
|
|
d79e6a7606 | ||
|
|
3fd3916ef9 | ||
|
|
b9b73d44c2 | ||
|
|
afa7c2030c | ||
|
|
f668a7a629 | ||
|
|
958fb43ac2 | ||
|
|
9ef8f592ba | ||
|
|
e6feae6c68 | ||
|
|
9dc5810eaa | ||
|
|
c960d6d96a | ||
|
|
45d95d7b45 | ||
|
|
3d1c32a5b4 | ||
|
|
6a67580c33 | ||
|
|
dbafa0a473 | ||
|
|
d348e73b55 | ||
|
|
3f3c1bd6e3 | ||
|
|
1a3f907c62 | ||
|
|
4f8027c6f7 | ||
|
|
86cd2a976b | ||
|
|
52a935bf09 | ||
|
|
ea7c3c4a5b | ||
|
|
9639d98dc3 | ||
|
|
3bfc16b6a8 | ||
|
|
1e54619e81 | ||
|
|
fc4ac98246 | ||
|
|
534e3ee98f | ||
|
|
9b8b8f020d | ||
|
|
aea7fb8069 | ||
|
|
b933633cc0 | ||
|
|
a822eb521d | ||
|
|
b6bb19538f | ||
|
|
c0a1f86801 | ||
|
|
439d79c677 | ||
|
|
b6ee0f118e | ||
|
|
ed037ca6d7 | ||
|
|
58f6239cb2 | ||
|
|
5e7bfcb591 | ||
|
|
03cd99c43c | ||
|
|
dc1742aca6 | ||
|
|
a23142cc3f | ||
|
|
de47319b00 | ||
|
|
c9a2549018 | ||
|
|
966253f7b6 | ||
|
|
b0ce7fb3cf | ||
|
|
f1f85a6563 | ||
|
|
c10ee5e0a8 | ||
|
|
7e86d3128c | ||
|
|
4f3da0ea2e | ||
|
|
406c7d8c6b | ||
|
|
809f1192f4 | ||
|
|
ff280d27f1 | ||
|
|
c566eb95a2 | ||
|
|
393d80600c | ||
|
|
1e44165d18 | ||
|
|
03f4774810 | ||
|
|
b64e0446a3 | ||
|
|
e51cab2418 | ||
|
|
f533d106c1 | ||
|
|
2557691053 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -341,3 +341,4 @@ Radzen.DocFX/_exported_templates
|
||||
Radzen.DocFX/api/*.yml
|
||||
!Radzen.DocFX/api/index.md
|
||||
Radzen.DocFX/api/.manifest
|
||||
Radzen.Blazor.min.js
|
||||
|
||||
341
Radzen.Blazor.Tests/DialogServiceTests.cs
Normal file
341
Radzen.Blazor.Tests/DialogServiceTests.cs
Normal file
@@ -0,0 +1,341 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Tests
|
||||
{
|
||||
public class DialogServiceTests
|
||||
{
|
||||
public class OpenDialogTests
|
||||
{
|
||||
[Fact(DisplayName = "DialogOptions default values are set correctly")]
|
||||
public void DialogOptions_DefaultValues_AreSetCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var options = new DialogOptions();
|
||||
var dialogService = new DialogService(null, null);
|
||||
|
||||
// Act
|
||||
dialogService.OpenDialog<DialogServiceTests>("Test", [], options);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("600px", options.Width);
|
||||
Assert.Equal("", options.Left);
|
||||
Assert.Equal("", options.Top);
|
||||
Assert.Equal("", options.Bottom);
|
||||
Assert.Equal("", options.Height);
|
||||
Assert.Equal("", options.Style);
|
||||
Assert.Equal("", options.CssClass);
|
||||
Assert.Equal("", options.WrapperCssClass);
|
||||
Assert.Equal("", options.ContentCssClass);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "DialogOptions values are retained after OpenDialog call")]
|
||||
public void DialogOptions_Values_AreRetained_AfterOpenDialogCall()
|
||||
{
|
||||
// Arrange
|
||||
var options = new DialogOptions
|
||||
{
|
||||
Width = "800px",
|
||||
Left = "10px",
|
||||
Top = "20px",
|
||||
Bottom = "30px",
|
||||
Height = "400px",
|
||||
Style = "background-color: red;",
|
||||
CssClass = "custom-class",
|
||||
WrapperCssClass = "wrapper-class",
|
||||
ContentCssClass = "content-class"
|
||||
};
|
||||
var dialogService = new DialogService(null, null);
|
||||
|
||||
// Act
|
||||
dialogService.OpenDialog<DialogServiceTests>("Test", [], options);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("800px", options.Width);
|
||||
Assert.Equal("10px", options.Left);
|
||||
Assert.Equal("20px", options.Top);
|
||||
Assert.Equal("30px", options.Bottom);
|
||||
Assert.Equal("400px", options.Height);
|
||||
Assert.Equal("background-color: red;", options.Style);
|
||||
Assert.Equal("custom-class", options.CssClass);
|
||||
Assert.Equal("wrapper-class", options.WrapperCssClass);
|
||||
Assert.Equal("content-class", options.ContentCssClass);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "DialogOptions is null and default values are set correctly")]
|
||||
public void DialogOptions_IsNull_DefaultValues_AreSetCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
DialogOptions resultingOptions = null;
|
||||
var dialogService = new DialogService(null, null);
|
||||
dialogService.OnOpen += (title, type, parameters, options) => resultingOptions = options;
|
||||
|
||||
// Act
|
||||
dialogService.OpenDialog<DialogServiceTests>("Test", [], null);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(resultingOptions);
|
||||
Assert.Equal("600px", resultingOptions.Width);
|
||||
Assert.Equal("", resultingOptions.Left);
|
||||
Assert.Equal("", resultingOptions.Top);
|
||||
Assert.Equal("", resultingOptions.Bottom);
|
||||
Assert.Equal("", resultingOptions.Height);
|
||||
Assert.Equal("", resultingOptions.Style);
|
||||
Assert.Equal("", resultingOptions.CssClass);
|
||||
Assert.Equal("", resultingOptions.WrapperCssClass);
|
||||
Assert.Equal("", resultingOptions.ContentCssClass);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Open with dynamic component type reflective calls are resolved without exception")]
|
||||
public void Open_DynamicComponentType_Reflective_Calls_Resolve()
|
||||
{
|
||||
// Arrange
|
||||
string resultingTitle = null;
|
||||
Type resultingType = null;
|
||||
var dialogService = new DialogService(null, null);
|
||||
dialogService.OnOpen += (title, type, _, _) =>
|
||||
{
|
||||
resultingTitle = title;
|
||||
resultingType = type;
|
||||
};
|
||||
|
||||
dialogService.Open("Dynamic Open", typeof(RadzenButton), []);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Dynamic Open", resultingTitle);
|
||||
Assert.Equal(typeof(RadzenButton), resultingType);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "OpenAsync with dynamic component type reflective calls are resolved without exception")]
|
||||
public async Task OpenAsync_DynamicComponentType_Reflective_Calls_Resolve()
|
||||
{
|
||||
// Arrange
|
||||
string resultingTitle = null;
|
||||
Type resultingType = null;
|
||||
var dialogService = new DialogService(null, null);
|
||||
dialogService.OnOpen += (title, type, _, _) =>
|
||||
{
|
||||
resultingTitle = title;
|
||||
resultingType = type;
|
||||
};
|
||||
|
||||
var openTask = dialogService.OpenAsync("Dynamic Open", typeof(RadzenButton), []);
|
||||
dialogService.Close();
|
||||
await openTask;
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Dynamic Open", resultingTitle);
|
||||
Assert.Equal(typeof(RadzenButton), resultingType);
|
||||
}
|
||||
}
|
||||
|
||||
public class ConfirmTests
|
||||
{
|
||||
[Fact(DisplayName = "ConfirmOptions is null and default values are set correctly")]
|
||||
public async Task ConfirmOptions_IsNull_AreSetCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var dialogService = new DialogService(null, null);
|
||||
ConfirmOptions resultingOptions = null;
|
||||
dialogService.OnOpen += (title, type, parameters, options) => resultingOptions = options as ConfirmOptions;
|
||||
|
||||
using var cancellationTokenSource = new CancellationTokenSource();
|
||||
cancellationTokenSource.Cancel();
|
||||
|
||||
// Act
|
||||
try
|
||||
{
|
||||
await dialogService.Confirm(cancellationToken: cancellationTokenSource.Token);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// this is expected
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(resultingOptions);
|
||||
Assert.Equal("Ok", resultingOptions.OkButtonText);
|
||||
Assert.Equal("Cancel", resultingOptions.CancelButtonText);
|
||||
Assert.Equal("600px", resultingOptions.Width);
|
||||
Assert.Equal("", resultingOptions.Style);
|
||||
Assert.Equal("rz-dialog-confirm", resultingOptions.CssClass);
|
||||
Assert.Equal("rz-dialog-wrapper", resultingOptions.WrapperCssClass);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "ConfirmOptions default values are set correctly")]
|
||||
public async Task ConfirmOptions_DefaultValues_AreSetCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var dialogService = new DialogService(null, null);
|
||||
ConfirmOptions resultingOptions = null;
|
||||
dialogService.OnOpen += (title, type, parameters, options) => resultingOptions = options as ConfirmOptions;
|
||||
|
||||
using var cancellationTokenSource = new CancellationTokenSource();
|
||||
cancellationTokenSource.Cancel();
|
||||
|
||||
// Act
|
||||
try
|
||||
{
|
||||
await dialogService.Confirm(options: new(), cancellationToken: cancellationTokenSource.Token);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// this is expected
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(resultingOptions);
|
||||
Assert.Equal("Ok", resultingOptions.OkButtonText);
|
||||
Assert.Equal("Cancel", resultingOptions.CancelButtonText);
|
||||
Assert.Equal("600px", resultingOptions.Width);
|
||||
Assert.Equal("", resultingOptions.Style);
|
||||
Assert.Equal("rz-dialog-confirm", resultingOptions.CssClass);
|
||||
Assert.Equal("rz-dialog-wrapper", resultingOptions.WrapperCssClass);
|
||||
}
|
||||
[Fact(DisplayName = "ConfirmOptions values are retained after Confirm call")]
|
||||
public async Task Confirm_ProvidedValues_AreRetained()
|
||||
{
|
||||
// Arrange
|
||||
var dialogService = new DialogService(null, null);
|
||||
var options = new ConfirmOptions
|
||||
{
|
||||
OkButtonText = "XXX",
|
||||
CancelButtonText = "YYY",
|
||||
Width = "800px",
|
||||
Style = "background-color: red;",
|
||||
CssClass = "custom-class",
|
||||
WrapperCssClass = "wrapper-class"
|
||||
};
|
||||
ConfirmOptions resultingOptions = null;
|
||||
dialogService.OnOpen += (title, type, parameters, options) => resultingOptions = options as ConfirmOptions;
|
||||
|
||||
// We break out of the dialog immediately, but the options should still be set
|
||||
using var cancellationTokenSource = new CancellationTokenSource();
|
||||
cancellationTokenSource.Cancel();
|
||||
|
||||
// Act
|
||||
try
|
||||
{
|
||||
await dialogService.Confirm("Confirm?", "Confirm", options, cancellationToken: cancellationTokenSource.Token);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// this is expected
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(resultingOptions);
|
||||
Assert.Equal("XXX", resultingOptions.OkButtonText);
|
||||
Assert.Equal("YYY", resultingOptions.CancelButtonText);
|
||||
Assert.Equal("800px", resultingOptions.Width);
|
||||
Assert.Equal("background-color: red;", resultingOptions.Style);
|
||||
Assert.Equal("rz-dialog-confirm custom-class", resultingOptions.CssClass);
|
||||
Assert.Equal("rz-dialog-wrapper wrapper-class", resultingOptions.WrapperCssClass);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class AlertTests
|
||||
{
|
||||
[Fact(DisplayName = "AlertOptions is null and default values are set correctly")]
|
||||
public async Task AlertOptions_IsNull_AreSetCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var dialogService = new DialogService(null, null);
|
||||
AlertOptions resultingOptions = null;
|
||||
dialogService.OnOpen += (title, type, parameters, options) => resultingOptions = options as AlertOptions;
|
||||
|
||||
using var cancellationTokenSource = new CancellationTokenSource();
|
||||
cancellationTokenSource.Cancel();
|
||||
|
||||
// Act
|
||||
try
|
||||
{
|
||||
await dialogService.Alert(cancellationToken: cancellationTokenSource.Token);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// this is expected
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(resultingOptions);
|
||||
Assert.Equal("Ok", resultingOptions.OkButtonText);
|
||||
Assert.Equal("600px", resultingOptions.Width);
|
||||
Assert.Equal("", resultingOptions.Style);
|
||||
Assert.Equal("rz-dialog-alert", resultingOptions.CssClass);
|
||||
Assert.Equal("rz-dialog-wrapper", resultingOptions.WrapperCssClass);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "AlertOptions default values are set correctly")]
|
||||
public async Task AlertOptions_DefaultValues_AreSetCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var dialogService = new DialogService(null, null);
|
||||
AlertOptions resultingOptions = null;
|
||||
dialogService.OnOpen += (title, type, parameters, options) => resultingOptions = options as AlertOptions;
|
||||
|
||||
using var cancellationTokenSource = new CancellationTokenSource();
|
||||
cancellationTokenSource.Cancel();
|
||||
|
||||
// Act
|
||||
try
|
||||
{
|
||||
await dialogService.Alert(options: new(), cancellationToken: cancellationTokenSource.Token);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// this is expected
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(resultingOptions);
|
||||
Assert.Equal("Ok", resultingOptions.OkButtonText);
|
||||
Assert.Equal("600px", resultingOptions.Width);
|
||||
Assert.Equal("", resultingOptions.Style);
|
||||
Assert.Equal("rz-dialog-alert", resultingOptions.CssClass);
|
||||
Assert.Equal("rz-dialog-wrapper", resultingOptions.WrapperCssClass);
|
||||
}
|
||||
[Fact(DisplayName = "AlertOptions values are retained after Alert call")]
|
||||
public async Task Alert_ProvidedValues_AreRetained()
|
||||
{
|
||||
// Arrange
|
||||
var dialogService = new DialogService(null, null);
|
||||
var options = new AlertOptions
|
||||
{
|
||||
OkButtonText = "XXX",
|
||||
Width = "800px",
|
||||
Style = "background-color: red;",
|
||||
CssClass = "custom-class",
|
||||
WrapperCssClass = "wrapper-class"
|
||||
};
|
||||
AlertOptions resultingOptions = null;
|
||||
dialogService.OnOpen += (title, type, parameters, options) => resultingOptions = options as AlertOptions;
|
||||
|
||||
// We break out of the dialog immediately, but the options should still be set
|
||||
using var cancellationTokenSource = new CancellationTokenSource();
|
||||
cancellationTokenSource.Cancel();
|
||||
|
||||
// Act
|
||||
try
|
||||
{
|
||||
await dialogService.Alert("Alert?", "Alert", options, cancellationToken: cancellationTokenSource.Token);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// this is expected
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(resultingOptions);
|
||||
Assert.Equal("XXX", resultingOptions.OkButtonText);
|
||||
Assert.Equal("800px", resultingOptions.Width);
|
||||
Assert.Equal("background-color: red;", resultingOptions.Style);
|
||||
Assert.Equal("rz-dialog-alert custom-class", resultingOptions.CssClass);
|
||||
Assert.Equal("rz-dialog-wrapper wrapper-class", resultingOptions.WrapperCssClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
304
Radzen.Blazor.Tests/ExpressionSerializerTests.cs
Normal file
304
Radzen.Blazor.Tests/ExpressionSerializerTests.cs
Normal file
@@ -0,0 +1,304 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Tests
|
||||
{
|
||||
class TestEntity
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public int Age { get; set; }
|
||||
public double Salary { get; set; }
|
||||
public float Score { get; set; }
|
||||
public decimal Balance { get; set; }
|
||||
public short Level { get; set; }
|
||||
public long Population { get; set; }
|
||||
public Status AccountStatus { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTimeOffset LastUpdated { get; set; }
|
||||
public Guid Id { get; set; }
|
||||
public TimeOnly StartTime { get; set; }
|
||||
public DateOnly BirthDate { get; set; }
|
||||
public int[] Scores { get; set; }
|
||||
public List<string> Tags { get; set; }
|
||||
public List<TestEntity> Children { get; set; }
|
||||
public Address Address { get; set; }
|
||||
public double[] Salaries { get; set; }
|
||||
public float[] Heights { get; set; }
|
||||
public decimal[] Balances { get; set; }
|
||||
public short[] Levels { get; set; }
|
||||
public long[] Populations { get; set; }
|
||||
public string[] Names { get; set; }
|
||||
public Guid[] Ids { get; set; }
|
||||
public DateTime[] CreatedDates { get; set; }
|
||||
public DateTimeOffset[] UpdatedDates { get; set; }
|
||||
public TimeOnly[] StartTimes { get; set; }
|
||||
public DateOnly[] BirthDates { get; set; }
|
||||
public Status[] Statuses { get; set; }
|
||||
}
|
||||
|
||||
enum Status
|
||||
{
|
||||
Active,
|
||||
Inactive,
|
||||
Suspended
|
||||
}
|
||||
|
||||
class Address
|
||||
{
|
||||
public string City { get; set; }
|
||||
public string Country { get; set; }
|
||||
}
|
||||
|
||||
public class ExpressionSerializerTests
|
||||
{
|
||||
private readonly ExpressionSerializer _serializer = new ExpressionSerializer();
|
||||
|
||||
[Fact]
|
||||
public void Serializes_SimpleBinaryExpression()
|
||||
{
|
||||
Expression<Func<int, bool>> expr = e => e > 10;
|
||||
Assert.Equal("e => (e > 10)", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_StringEquality()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => e.Name == "John";
|
||||
Assert.Equal("e => (e.Name == \"John\")", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_IntComparison()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => e.Age > 18;
|
||||
Assert.Equal("e => (e.Age > 18)", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_DoubleComparison()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => e.Salary < 50000.50;
|
||||
Assert.Equal("e => (e.Salary < 50000.5)", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_FloatComparison()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => e.Score >= 85.3f;
|
||||
Assert.Equal("e => (e.Score >= 85.3)", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_DecimalComparison()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => e.Balance <= 1000.75m;
|
||||
Assert.Equal("e => (e.Balance <= 1000.75)", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_ShortComparison()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => e.Level == 3;
|
||||
Assert.Equal("e => (e.Level == 3)", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_LongComparison()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => e.Population > 1000000L;
|
||||
Assert.Equal("e => (e.Population > 1000000)", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_EnumComparison()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => e.AccountStatus == Status.Inactive;
|
||||
Assert.Equal("e => (e.AccountStatus == 1)", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_ArrayContainsValue()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => e.Scores.Contains(100);
|
||||
Assert.Equal("e => e.Scores.Contains(100)", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_ArrayNotContainsValue()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => !e.Scores.Contains(100);
|
||||
Assert.Equal("e => (!e.Scores.Contains(100))", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_ArrayInValue()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => e.Scores.Intersect(new [] { 100 }).Any();
|
||||
Assert.Equal("e => e.Scores.Intersect(new [] { 100 }).Any()", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_ArrayNotInValue()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => e.Scores.Except(new[] { 100 }).Any();
|
||||
Assert.Equal("e => e.Scores.Except(new [] { 100 }).Any()", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_ArrayInValueOposite()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => new[] { 100 }.Intersect(e.Scores).Any();
|
||||
Assert.Equal("e => new [] { 100 }.Intersect(e.Scores).Any()", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_ArrayNotInValueOposite()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => new[] { 100 }.Except(e.Scores).Any();
|
||||
Assert.Equal("e => new [] { 100 }.Except(e.Scores).Any()", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_IntArrayInValueOposite()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => new[] { 100 }.Intersect(e.Scores).Any();
|
||||
Assert.Equal("e => new [] { 100 }.Intersect(e.Scores).Any()", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_IntArrayNotInValueOposite()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => !new[] { 100 }.Intersect(e.Scores).Any();
|
||||
Assert.Equal("e => (!new [] { 100 }.Intersect(e.Scores).Any())", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_DoubleArrayInValueOposite()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => new[] { 99.99 }.Intersect(e.Salaries).Any();
|
||||
Assert.Equal("e => new [] { 99.99 }.Intersect(e.Salaries).Any()", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_FloatArrayInValueOposite()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => new[] { 5.5f }.Intersect(e.Heights).Any();
|
||||
Assert.Equal("e => new [] { 5.5 }.Intersect(e.Heights).Any()", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_DecimalArrayInValueOposite()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => new[] { 1000.75m }.Intersect(e.Balances).Any();
|
||||
Assert.Equal("e => new [] { 1000.75 }.Intersect(e.Balances).Any()", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_ShortArrayInValueOposite()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => new [] { (short)3 }.Intersect(e.Levels).Any();
|
||||
Assert.Equal("e => new [] { 3 }.Intersect(e.Levels).Any()", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_LongArrayInValueOposite()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => new [] { 1000000L }.Intersect(e.Populations).Any();
|
||||
Assert.Equal("e => new [] { 1000000 }.Intersect(e.Populations).Any()", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_StringArrayInValueOposite()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => new[] { "Alice", "Bob" }.Intersect(e.Names).Any();
|
||||
Assert.Equal("e => (new [] { \"Alice\", \"Bob\" }).Intersect(e.Names).Any()", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_GuidArrayInValueOposite()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => new[] { Guid.Parse("12345678-1234-1234-1234-123456789abc") }.Intersect(e.Ids).Any();
|
||||
Assert.Equal("e => (new [] { Guid.Parse(\"12345678-1234-1234-1234-123456789abc\") }).Intersect(e.Ids).Any()", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_DateTimeArrayInValueOposite()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => new[] { DateTime.Parse("2023-01-01T00:00:00.000Z") }.Intersect(e.CreatedDates).Any();
|
||||
Assert.Equal("e => (new [] { DateTime.Parse(\"2023-01-01T00:00:00.000Z\") }).Intersect(e.CreatedDates).Any()", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_DateTimeOffsetArrayInValueOposite()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => new[] { DateTimeOffset.Parse("2023-01-01T10:30:00.000+00:00") }.Intersect(e.UpdatedDates).Any();
|
||||
Assert.Equal("e => (new [] { DateTimeOffset.Parse(\"2023-01-01T10:30:00.000+00:00\") }).Intersect(e.UpdatedDates).Any()", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_TimeOnlyArrayInValueOposite()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => new[] { TimeOnly.Parse("12:00:00") }.Intersect(e.StartTimes).Any();
|
||||
Assert.Equal("e => (new [] { TimeOnly.Parse(\"12:00:00\") }).Intersect(e.StartTimes).Any()", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_DateOnlyArrayInValueOposite()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => new[] { DateOnly.Parse("2000-01-01") }.Intersect(e.BirthDates).Any();
|
||||
Assert.Equal("e => (new [] { DateOnly.Parse(\"2000-01-01\") }).Intersect(e.BirthDates).Any()", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_EnumArrayInValueOposite()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => new[] { Status.Active, Status.Inactive }.Intersect(e.Statuses).Any();
|
||||
Assert.Equal("e => (new [] { (Radzen.Blazor.Tests.Status)0, (Radzen.Blazor.Tests.Status)1 }).Intersect(e.Statuses).Any()", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_ListContainsValue()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => e.Tags.Contains("VIP");
|
||||
Assert.Equal("e => e.Tags.Contains(\"VIP\")", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_ListNotContainsValue()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => !e.Tags.Contains("VIP");
|
||||
Assert.Equal("e => (!e.Tags.Contains(\"VIP\"))", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_ListAnyCheck()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => e.Children.Any(c => c.Age > 18);
|
||||
Assert.Equal("e => e.Children.Any(c => (c.Age > 18))", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_ListNotAnyCheck()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => !e.Children.Any(c => c.Age > 18);
|
||||
Assert.Equal("e => (!e.Children.Any(c => (c.Age > 18)))", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_EntitySubPropertyCheck()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => e.Address.City == "New York";
|
||||
Assert.Equal("e => (e.Address.City == \"New York\")", _serializer.Serialize(expr));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serializes_ComplexExpressionWithProperties()
|
||||
{
|
||||
Expression<Func<TestEntity, bool>> expr = e => e.Age > 18 && e.Tags.Contains("Member") || e.Address.City == "London";
|
||||
Assert.Equal("e => (((e.Age > 18) && e.Tags.Contains(\"Member\")) || (e.Address.City == \"London\"))", _serializer.Serialize(expr));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -184,13 +184,13 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
Assert.Contains("SummaryContent", component.Markup);
|
||||
Assert.Equal(
|
||||
"",
|
||||
component.Find(".rz-fieldset-content-summary").ParentElement.Attributes.First(attr => attr.Name == "style").Value
|
||||
"false",
|
||||
component.Find(".rz-fieldset-content-summary").ParentElement.ParentElement.Attributes.First(attr => attr.Name == "aria-hidden").Value
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fieldset_DontRenders_SummaryWhenOpen()
|
||||
public void Fieldset_DoesNotRender_SummaryWhenOpen()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
var component = ctx.RenderComponent<RadzenFieldset>();
|
||||
@@ -210,8 +210,8 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
Assert.Contains("SummaryContent", component.Markup);
|
||||
Assert.Equal(
|
||||
"display: none",
|
||||
component.Find(".rz-fieldset-content-summary").ParentElement.Attributes.First(attr => attr.Name == "style").Value
|
||||
"true",
|
||||
component.Find(".rz-fieldset-content-summary").ParentElement.ParentElement.Attributes.First(attr => attr.Name == "aria-hidden").Value
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
314
Radzen.Blazor.Tests/Markdown/BlockQuoteTests.cs
Normal file
314
Radzen.Blazor.Tests/Markdown/BlockQuoteTests.cs
Normal file
@@ -0,0 +1,314 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class BlockQuoteTests
|
||||
{
|
||||
private static string ToXml(string markdown)
|
||||
{
|
||||
var document = MarkdownParser.Parse(markdown);
|
||||
|
||||
return XmlVisitor.ToXml(document);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_BasicBlockQuote()
|
||||
{
|
||||
|
||||
Assert.Equal(@"<document>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</document>", ToXml(@"> foo"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"> # Foo
|
||||
> bar
|
||||
> baz", @"<document>
|
||||
<block_quote>
|
||||
<heading level=""1"">
|
||||
<text>Foo</text>
|
||||
</heading>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
<softbreak />
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@"># Foo
|
||||
>bar
|
||||
> baz", @"<document>
|
||||
<block_quote>
|
||||
<heading level=""1"">
|
||||
<text>Foo</text>
|
||||
</heading>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
<softbreak />
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@" > # Foo
|
||||
> bar
|
||||
> baz", @"<document>
|
||||
<block_quote>
|
||||
<heading level=""1"">
|
||||
<text>Foo</text>
|
||||
</heading>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
<softbreak />
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@" > # Foo
|
||||
> bar
|
||||
> baz", @"<document>
|
||||
<code_block>> # Foo
|
||||
> bar
|
||||
> baz
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"> # Foo
|
||||
> bar
|
||||
baz", @"<document>
|
||||
<block_quote>
|
||||
<heading level=""1"">
|
||||
<text>Foo</text>
|
||||
</heading>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
<softbreak />
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@"> bar
|
||||
baz
|
||||
> foo", @"<document>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
<softbreak />
|
||||
<text>baz</text>
|
||||
<softbreak />
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@"> foo
|
||||
---", @"<document>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
[InlineData(@"> - foo
|
||||
- bar", @"<document>
|
||||
<block_quote>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</block_quote>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"> foo
|
||||
bar", @"<document>
|
||||
<block_quote>
|
||||
<code_block>foo
|
||||
</code_block>
|
||||
</block_quote>
|
||||
<code_block>bar
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"> ```
|
||||
foo
|
||||
```", @"<document>
|
||||
<block_quote>
|
||||
<code_block></code_block>
|
||||
</block_quote>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
<code_block></code_block>
|
||||
</document>")]
|
||||
[InlineData(@"> foo
|
||||
- bar", @"<document>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<softbreak />
|
||||
<text>- bar</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@">", @"<document>
|
||||
<block_quote />
|
||||
</document>")]
|
||||
[InlineData(@">
|
||||
>
|
||||
> ", @"<document>
|
||||
<block_quote />
|
||||
</document>")]
|
||||
[InlineData(@">
|
||||
> foo
|
||||
> ", @"<document>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@"> foo
|
||||
|
||||
> bar", @"<document>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@"> foo
|
||||
>
|
||||
> bar", @"<document>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@"foo
|
||||
> bar", @"<document>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@"> aaa
|
||||
***
|
||||
> bbb", @"<document>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>aaa</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
<thematic_break />
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>bbb</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@"> bar
|
||||
baz", @"<document>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
<softbreak />
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@"> bar
|
||||
|
||||
baz", @"<document>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"> bar
|
||||
>
|
||||
baz", @"<document>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"> > > foo
|
||||
bar", @"<document>
|
||||
<block_quote>
|
||||
<block_quote>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<softbreak />
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</block_quote>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@">>> foo
|
||||
> bar
|
||||
>>baz", @"<document>
|
||||
<block_quote>
|
||||
<block_quote>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<softbreak />
|
||||
<text>bar</text>
|
||||
<softbreak />
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</block_quote>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@"> code
|
||||
|
||||
> not code", @"<document>
|
||||
<block_quote>
|
||||
<code_block>code
|
||||
</code_block>
|
||||
</block_quote>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>not code</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
public void Parse_BlockQuote(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
}
|
||||
175
Radzen.Blazor.Tests/Markdown/CodeTests.cs
Normal file
175
Radzen.Blazor.Tests/Markdown/CodeTests.cs
Normal file
@@ -0,0 +1,175 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class CodeTests
|
||||
{
|
||||
private static string ToXml(string markdown)
|
||||
{
|
||||
var document = MarkdownParser.Parse(markdown);
|
||||
|
||||
return XmlVisitor.ToXml(document);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("`foo`",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<code>foo</code>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("`` foo ` bar ``",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<code>foo ` bar</code>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("` `` `",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<code>``</code>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_BasicCode_ReturnsCodeNode(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("` `` `",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<code> `` </code>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("` a`",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<code> a</code>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"` `
|
||||
` `",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<code> </code>
|
||||
<softbreak />
|
||||
<code> </code>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_CodeWithSpaces_PreservesSpaces(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"``
|
||||
foo
|
||||
bar
|
||||
baz
|
||||
``",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<code>foo bar baz</code>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"``
|
||||
foo
|
||||
``",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<code>foo </code>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"`foo bar
|
||||
baz`",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<code>foo bar baz</code>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
|
||||
public void Parse_CodeWithLineBreaks_ConvertsToSpace(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("`foo\\`bar`", @"<document>
|
||||
<paragraph>
|
||||
<code>foo\</code>
|
||||
<text>bar</text>
|
||||
<text>`</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("``foo`bar``", @"<document>
|
||||
<paragraph>
|
||||
<code>foo`bar</code>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("` foo `` bar `", @"<document>
|
||||
<paragraph>
|
||||
<code>foo `` bar</code>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_CodeWithBacktics(string mardown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(mardown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("*foo`*`", @"<document>
|
||||
<paragraph>
|
||||
<text>*</text>
|
||||
<text>foo</text>
|
||||
<code>*</code>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("[not a `link](/foo`)",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>not a </text>
|
||||
<code>link](/foo</code>
|
||||
<text>)</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("`<https://foo.bar.`baz>`",@"<document>
|
||||
<paragraph>
|
||||
<code><https://foo.bar.</code>
|
||||
<text>baz></text>
|
||||
<text>`</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_CodePrecedence(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("```foo``", @"<document>
|
||||
<paragraph>
|
||||
<text>```</text>
|
||||
<text>foo</text>
|
||||
<text>``</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("`foo", @"<document>
|
||||
<paragraph>
|
||||
<text>`</text>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("`foo``bar``", @"<document>
|
||||
<paragraph>
|
||||
<text>`</text>
|
||||
<text>foo</text>
|
||||
<code>bar</code>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_UnmatchingBacktics(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
}
|
||||
272
Radzen.Blazor.Tests/Markdown/EmphasisTests.cs
Normal file
272
Radzen.Blazor.Tests/Markdown/EmphasisTests.cs
Normal file
@@ -0,0 +1,272 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class EmphasisTests
|
||||
{
|
||||
private static string ToXml(string markdown)
|
||||
{
|
||||
var document = MarkdownParser.Parse(markdown);
|
||||
return XmlVisitor.ToXml(document);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"**foo** bar
|
||||
baz",@"<document>
|
||||
<paragraph>
|
||||
<strong>
|
||||
<text>foo</text>
|
||||
</strong>
|
||||
<text> bar</text>
|
||||
<softbreak />
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("*foo bar*",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>foo bar</text>
|
||||
</emph>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("a * foo bar*",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>a </text>
|
||||
<text>*</text>
|
||||
<text> foo bar</text>
|
||||
<text>*</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("a*\"foo\"*",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
<text>*</text>
|
||||
<text>""foo""</text>
|
||||
<text>*</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("* a *",
|
||||
@"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>a </text>
|
||||
<text>*</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData("foo*bar*",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<emph>
|
||||
<text>bar</text>
|
||||
</emph>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("5*6*78",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>5</text>
|
||||
<emph>
|
||||
<text>6</text>
|
||||
</emph>
|
||||
<text>78</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("_foo bar_",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>foo bar</text>
|
||||
</emph>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("_ foo bar_",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>_</text>
|
||||
<text> foo bar</text>
|
||||
<text>_</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("a_\"foo\"_",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
<text>_</text>
|
||||
<text>""foo""</text>
|
||||
<text>_</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("foo_bar_",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<text>_</text>
|
||||
<text>bar</text>
|
||||
<text>_</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("5_6_78",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>5</text>
|
||||
<text>_</text>
|
||||
<text>6</text>
|
||||
<text>_</text>
|
||||
<text>78</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("пристаням_стремятся_",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>пристаням</text>
|
||||
<text>_</text>
|
||||
<text>стремятся</text>
|
||||
<text>_</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("aa_\"bb\"_cc",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>aa</text>
|
||||
<text>_</text>
|
||||
<text>""bb""</text>
|
||||
<text>_</text>
|
||||
<text>cc</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("foo-_(bar)_",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>foo-</text>
|
||||
<emph>
|
||||
<text>(bar)</text>
|
||||
</emph>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("_foo*",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>_</text>
|
||||
<text>foo</text>
|
||||
<text>*</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("*foo bar *",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>*</text>
|
||||
<text>foo bar </text>
|
||||
<text>*</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("*foo bar\nbaz*",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>foo bar</text>
|
||||
<softbreak />
|
||||
<text>baz</text>
|
||||
</emph>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("*(*foo)",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>*</text>
|
||||
<text>(</text>
|
||||
<text>*</text>
|
||||
<text>foo)</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("*(*foo*)*",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>(</text>
|
||||
<emph>
|
||||
<text>foo</text>
|
||||
</emph>
|
||||
<text>)</text>
|
||||
</emph>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("*foo*bar",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>foo</text>
|
||||
</emph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("_foo bar _",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>_</text>
|
||||
<text>foo bar </text>
|
||||
<text>_</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("_(_foo_)_",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>(</text>
|
||||
<emph>
|
||||
<text>foo</text>
|
||||
</emph>
|
||||
<text>)</text>
|
||||
</emph>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("_foo_bar",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>_</text>
|
||||
<text>foo</text>
|
||||
<text>_</text>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("_пристаням_стремятся",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>_</text>
|
||||
<text>пристаням</text>
|
||||
<text>_</text>
|
||||
<text>стремятся</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("_foo_bar_baz_",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>foo</text>
|
||||
<text>_</text>
|
||||
<text>bar</text>
|
||||
<text>_</text>
|
||||
<text>baz</text>
|
||||
</emph>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("_(bar)_.",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>(bar)</text>
|
||||
</emph>
|
||||
<text>.</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_EmphasisRules_AdheresToCommonMarkSpec(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
}
|
||||
264
Radzen.Blazor.Tests/Markdown/FencedCodeBlockTests.cs
Normal file
264
Radzen.Blazor.Tests/Markdown/FencedCodeBlockTests.cs
Normal file
@@ -0,0 +1,264 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class FencedCodeBlockTests
|
||||
{
|
||||
private static string ToXml(string markdown)
|
||||
{
|
||||
var document = MarkdownParser.Parse(markdown);
|
||||
|
||||
return XmlVisitor.ToXml(document);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_BasicFencedCodeBlock()
|
||||
{
|
||||
Assert.Equal(@"<document>
|
||||
<code_block>foo
|
||||
</code_block>
|
||||
</document>", ToXml(@"```
|
||||
foo
|
||||
```"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"```
|
||||
<
|
||||
>
|
||||
```", @"<document>
|
||||
<code_block><
|
||||
>
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"~~~
|
||||
<
|
||||
>
|
||||
~~~", @"<document>
|
||||
<code_block><
|
||||
>
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"``
|
||||
foo
|
||||
``", @"<document>
|
||||
<paragraph>
|
||||
<code>foo</code>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"```
|
||||
aaa
|
||||
~~~
|
||||
```", @"<document>
|
||||
<code_block>aaa
|
||||
~~~
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"~~~
|
||||
aaa
|
||||
```
|
||||
~~~", @"<document>
|
||||
<code_block>aaa
|
||||
```
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"````
|
||||
aaa
|
||||
```
|
||||
``````", @"<document>
|
||||
<code_block>aaa
|
||||
```
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"~~~~
|
||||
aaa
|
||||
~~~
|
||||
~~~~", @"<document>
|
||||
<code_block>aaa
|
||||
~~~
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"```", @"<document>
|
||||
<code_block></code_block>
|
||||
</document>")]
|
||||
[InlineData(@"`````
|
||||
|
||||
```
|
||||
aaa", @"<document>
|
||||
<code_block>
|
||||
```
|
||||
aaa
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"> ```
|
||||
> aaa
|
||||
|
||||
bbb", @"<document>
|
||||
<block_quote>
|
||||
<code_block>aaa
|
||||
</code_block>
|
||||
</block_quote>
|
||||
<paragraph>
|
||||
<text>bbb</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"```
|
||||
|
||||
|
||||
```", @"<document>
|
||||
<code_block>
|
||||
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"```
|
||||
```", @"<document>
|
||||
<code_block></code_block>
|
||||
</document>")]
|
||||
[InlineData(@" ```
|
||||
aaa
|
||||
aaa
|
||||
```", @"<document>
|
||||
<code_block>aaa
|
||||
aaa
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@" ```
|
||||
aaa
|
||||
aaa
|
||||
aaa
|
||||
```", @"<document>
|
||||
<code_block>aaa
|
||||
aaa
|
||||
aaa
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@" ```
|
||||
aaa
|
||||
aaa
|
||||
aaa
|
||||
```", @"<document>
|
||||
<code_block>aaa
|
||||
aaa
|
||||
aaa
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@" ```
|
||||
aaa
|
||||
```", @"<document>
|
||||
<code_block>```
|
||||
aaa
|
||||
```
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"```
|
||||
aaa
|
||||
```", @"<document>
|
||||
<code_block>aaa
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@" ```
|
||||
aaa
|
||||
````", @"<document>
|
||||
<code_block>aaa
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"```
|
||||
aaa
|
||||
```", @"<document>
|
||||
<code_block>aaa
|
||||
```
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"``` ```
|
||||
aaa", @"<document>
|
||||
<paragraph>
|
||||
<code> </code>
|
||||
<softbreak />
|
||||
<text>aaa</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"~~~~~~
|
||||
aaa
|
||||
~~~ ~~", @"<document>
|
||||
<code_block>aaa
|
||||
~~~ ~~
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"foo
|
||||
```
|
||||
bar
|
||||
```
|
||||
baz", @"<document>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
<code_block>bar
|
||||
</code_block>
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"foo
|
||||
---
|
||||
~~~
|
||||
bar
|
||||
~~~
|
||||
# baz", @"<document>
|
||||
<heading level=""2"">
|
||||
<text>foo</text>
|
||||
</heading>
|
||||
<code_block>bar
|
||||
</code_block>
|
||||
<heading level=""1"">
|
||||
<text>baz</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@"```ruby
|
||||
def foo(x)
|
||||
return 3
|
||||
end
|
||||
```", @"<document>
|
||||
<code_block info=""ruby"">def foo(x)
|
||||
return 3
|
||||
end
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"~~~~ ruby startline=3 $%@#$
|
||||
def foo(x)
|
||||
return 3
|
||||
end
|
||||
~~~~~~~", @"<document>
|
||||
<code_block info=""ruby startline=3 $%@#$"">def foo(x)
|
||||
return 3
|
||||
end
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"````;
|
||||
````", @"<document>
|
||||
<code_block info="";""></code_block>
|
||||
</document>")]
|
||||
[InlineData(@"``` aa ```
|
||||
foo", @"<document>
|
||||
<paragraph>
|
||||
<code>aa</code>
|
||||
<softbreak />
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"~~~ aa ``` ~~~
|
||||
foo
|
||||
~~~", @"<document>
|
||||
<code_block info=""aa ``` ~~~"">foo
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"```
|
||||
``` aaa
|
||||
```", @"<document>
|
||||
<code_block>``` aaa
|
||||
</code_block>
|
||||
</document>")]
|
||||
public void Parse_FencedCodeBlock(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
}
|
||||
132
Radzen.Blazor.Tests/Markdown/HardLineBreakTests.cs
Normal file
132
Radzen.Blazor.Tests/Markdown/HardLineBreakTests.cs
Normal file
@@ -0,0 +1,132 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class HardLineBreakTests
|
||||
{
|
||||
private static string ToXml(string markdown)
|
||||
{
|
||||
var document = MarkdownParser.Parse(markdown);
|
||||
|
||||
return XmlVisitor.ToXml(document);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"foo
|
||||
baz", @"<document>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<linebreak />
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("foo \r\nbaz", @"<document>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<linebreak />
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"foo\
|
||||
baz", @"<document>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<linebreak />
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"foo
|
||||
baz", @"<document>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<linebreak />
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"foo
|
||||
bar", @"<document>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<linebreak />
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"foo\
|
||||
bar", @"<document>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<linebreak />
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"*foo
|
||||
bar*", @"<document>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>foo</text>
|
||||
<linebreak />
|
||||
<text>bar</text>
|
||||
</emph>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"*foo\
|
||||
bar*", @"<document>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>foo</text>
|
||||
<linebreak />
|
||||
<text>bar</text>
|
||||
</emph>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"`code\
|
||||
span`", @"<document>
|
||||
<paragraph>
|
||||
<code>code\ span</code>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"`code
|
||||
span`", @"<document>
|
||||
<paragraph>
|
||||
<code>code span</code>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<a href=""foo
|
||||
bar"">", @"<document>
|
||||
<paragraph>
|
||||
<html_inline><a href=""foo
|
||||
bar""></html_inline>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<a href=""foo\
|
||||
bar"">", @"<document>
|
||||
<paragraph>
|
||||
<html_inline><a href=""foo\
|
||||
bar""></html_inline>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"foo\", @"<document>
|
||||
<paragraph>
|
||||
<text>foo\</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"foo ", @"<document>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"### foo\", @"<document>
|
||||
<heading level=""3"">
|
||||
<text>foo\</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@"### foo ", @"<document>
|
||||
<heading level=""3"">
|
||||
<text>foo</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
public void Parse_HardLineBreak(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
}
|
||||
511
Radzen.Blazor.Tests/Markdown/HeadingTests.cs
Normal file
511
Radzen.Blazor.Tests/Markdown/HeadingTests.cs
Normal file
@@ -0,0 +1,511 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class HeadingTests
|
||||
{
|
||||
private static string ToXml(string markdown)
|
||||
{
|
||||
var document = MarkdownParser.Parse(markdown);
|
||||
|
||||
return XmlVisitor.ToXml(document);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_BasicAtxHeading()
|
||||
{
|
||||
Assert.Equal(@"<document>
|
||||
<heading level=""1"">
|
||||
<text>foo</text>
|
||||
</heading>
|
||||
</document>", ToXml("# foo"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"# foo
|
||||
## foo
|
||||
### foo
|
||||
#### foo
|
||||
##### foo
|
||||
###### foo", @"<document>
|
||||
<heading level=""1"">
|
||||
<text>foo</text>
|
||||
</heading>
|
||||
<heading level=""2"">
|
||||
<text>foo</text>
|
||||
</heading>
|
||||
<heading level=""3"">
|
||||
<text>foo</text>
|
||||
</heading>
|
||||
<heading level=""4"">
|
||||
<text>foo</text>
|
||||
</heading>
|
||||
<heading level=""5"">
|
||||
<text>foo</text>
|
||||
</heading>
|
||||
<heading level=""6"">
|
||||
<text>foo</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@"####### foo", @"<document>
|
||||
<paragraph>
|
||||
<text>####### foo</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"#5 bolt
|
||||
|
||||
#hashtag", @"<document>
|
||||
<paragraph>
|
||||
<text>#5 bolt</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>#hashtag</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"\## foo", @"<document>
|
||||
<paragraph>
|
||||
<text>#</text>
|
||||
<text># foo</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"# foo *bar* \*baz\*", @"<document>
|
||||
<heading level=""1"">
|
||||
<text>foo </text>
|
||||
<emph>
|
||||
<text>bar</text>
|
||||
</emph>
|
||||
<text> </text>
|
||||
<text>*</text>
|
||||
<text>baz</text>
|
||||
<text>*</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@"# foo ", @"<document>
|
||||
<heading level=""1"">
|
||||
<text>foo</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@" ### foo
|
||||
## foo
|
||||
# foo", @"<document>
|
||||
<heading level=""3"">
|
||||
<text>foo</text>
|
||||
</heading>
|
||||
<heading level=""2"">
|
||||
<text>foo</text>
|
||||
</heading>
|
||||
<heading level=""1"">
|
||||
<text>foo</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@" # foo", @"<document>
|
||||
<code_block># foo
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"foo
|
||||
# bar", @"<document>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<softbreak />
|
||||
<text># bar</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"## foo ##
|
||||
### bar ###", @"<document>
|
||||
<heading level=""2"">
|
||||
<text>foo</text>
|
||||
</heading>
|
||||
<heading level=""3"">
|
||||
<text>bar</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@"# foo ##################################
|
||||
##### foo ##", @"<document>
|
||||
<heading level=""1"">
|
||||
<text>foo</text>
|
||||
</heading>
|
||||
<heading level=""5"">
|
||||
<text>foo</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@"### foo ### ", @"<document>
|
||||
<heading level=""3"">
|
||||
<text>foo</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@"### foo ### b", @"<document>
|
||||
<heading level=""3"">
|
||||
<text>foo ### b</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@"# foo#", @"<document>
|
||||
<heading level=""1"">
|
||||
<text>foo#</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@"### foo \###
|
||||
## foo #\##
|
||||
# foo \#", @"<document>
|
||||
<heading level=""3"">
|
||||
<text>foo </text>
|
||||
<text>#</text>
|
||||
<text>##</text>
|
||||
</heading>
|
||||
<heading level=""2"">
|
||||
<text>foo #</text>
|
||||
<text>#</text>
|
||||
<text>#</text>
|
||||
</heading>
|
||||
<heading level=""1"">
|
||||
<text>foo </text>
|
||||
<text>#</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@"****
|
||||
## foo
|
||||
****", @"<document>
|
||||
<thematic_break />
|
||||
<heading level=""2"">
|
||||
<text>foo</text>
|
||||
</heading>
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
[InlineData(@"Foo bar
|
||||
# baz
|
||||
Bar foo", @"<document>
|
||||
<paragraph>
|
||||
<text>Foo bar</text>
|
||||
</paragraph>
|
||||
<heading level=""1"">
|
||||
<text>baz</text>
|
||||
</heading>
|
||||
<paragraph>
|
||||
<text>Bar foo</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"##
|
||||
#
|
||||
### ###", @"<document>
|
||||
<heading level=""2"" />
|
||||
<heading level=""1"" />
|
||||
<heading level=""3"" />
|
||||
</document>")]
|
||||
public void Parse_AtxHeading(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"Foo *bar*
|
||||
=========
|
||||
|
||||
Foo *baz*
|
||||
---------", @"<document>
|
||||
<heading level=""1"">
|
||||
<text>Foo </text>
|
||||
<emph>
|
||||
<text>bar</text>
|
||||
</emph>
|
||||
</heading>
|
||||
<heading level=""2"">
|
||||
<text>Foo </text>
|
||||
<emph>
|
||||
<text>baz</text>
|
||||
</emph>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@"Foo *bar
|
||||
baz*
|
||||
====", @"<document>
|
||||
<heading level=""1"">
|
||||
<text>Foo </text>
|
||||
<emph>
|
||||
<text>bar</text>
|
||||
<softbreak />
|
||||
<text>baz</text>
|
||||
</emph>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@" Foo *bar
|
||||
baz*
|
||||
====", @"<document>
|
||||
<heading level=""1"">
|
||||
<text>Foo </text>
|
||||
<emph>
|
||||
<text>bar</text>
|
||||
<softbreak />
|
||||
<text>baz</text>
|
||||
</emph>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@"Foo
|
||||
-------------------------
|
||||
|
||||
Foo
|
||||
=", @"<document>
|
||||
<heading level=""2"">
|
||||
<text>Foo</text>
|
||||
</heading>
|
||||
<heading level=""1"">
|
||||
<text>Foo</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@" Foo
|
||||
---
|
||||
|
||||
Foo
|
||||
-----
|
||||
|
||||
Foo
|
||||
===", @"<document>
|
||||
<heading level=""2"">
|
||||
<text>Foo</text>
|
||||
</heading>
|
||||
<heading level=""2"">
|
||||
<text>Foo</text>
|
||||
</heading>
|
||||
<heading level=""1"">
|
||||
<text>Foo</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@" Foo
|
||||
---
|
||||
|
||||
Foo
|
||||
---", @"<document>
|
||||
<code_block>Foo
|
||||
---
|
||||
|
||||
Foo
|
||||
</code_block>
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
[InlineData(@"Foo
|
||||
---- ", @"<document>
|
||||
<heading level=""2"">
|
||||
<text>Foo</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@"Foo
|
||||
---", @"<document>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
<softbreak />
|
||||
<text>---</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"Foo
|
||||
= =
|
||||
|
||||
Foo
|
||||
--- -", @"<document>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
<softbreak />
|
||||
<text>= =</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
</paragraph>
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
[InlineData(@"Foo
|
||||
-----", @"<document>
|
||||
<heading level=""2"">
|
||||
<text>Foo</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@"Foo\
|
||||
----", @"<document>
|
||||
<heading level=""2"">
|
||||
<text>Foo\</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@"`Foo
|
||||
----
|
||||
`
|
||||
|
||||
<a title=""a lot
|
||||
---
|
||||
of dashes""/>", @"<document>
|
||||
<heading level=""2"">
|
||||
<text>`</text>
|
||||
<text>Foo</text>
|
||||
</heading>
|
||||
<paragraph>
|
||||
<text>`</text>
|
||||
</paragraph>
|
||||
<heading level=""2"">
|
||||
<text><a title=""a lot</text>
|
||||
</heading>
|
||||
<paragraph>
|
||||
<text>of dashes""/></text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"> Foo
|
||||
---", @"<document>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
[InlineData(@"> foo
|
||||
bar
|
||||
===", @"<document>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<softbreak />
|
||||
<text>bar</text>
|
||||
<softbreak />
|
||||
<text>===</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@"- Foo
|
||||
---", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
[InlineData(@"Foo
|
||||
Bar
|
||||
---", @"<document>
|
||||
<heading level=""2"">
|
||||
<text>Foo</text>
|
||||
<softbreak />
|
||||
<text>Bar</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@"---
|
||||
Foo
|
||||
---
|
||||
Bar
|
||||
---
|
||||
Baz", @"<document>
|
||||
<thematic_break />
|
||||
<heading level=""2"">
|
||||
<text>Foo</text>
|
||||
</heading>
|
||||
<heading level=""2"">
|
||||
<text>Bar</text>
|
||||
</heading>
|
||||
<paragraph>
|
||||
<text>Baz</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"
|
||||
====", @"<document>
|
||||
<paragraph>
|
||||
<text>====</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"---
|
||||
---", @"<document>
|
||||
<thematic_break />
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
[InlineData(@"- foo
|
||||
-----", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
[InlineData(@" foo
|
||||
---", @"<document>
|
||||
<code_block>foo
|
||||
</code_block>
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
[InlineData(@"> foo
|
||||
-----", @"<document>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
[InlineData(@"\> foo
|
||||
------", @"<document>
|
||||
<heading level=""2"">
|
||||
<text>> foo</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@"Foo
|
||||
|
||||
bar
|
||||
---
|
||||
baz", @"<document>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
</paragraph>
|
||||
<heading level=""2"">
|
||||
<text>bar</text>
|
||||
</heading>
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"Foo
|
||||
bar
|
||||
|
||||
---
|
||||
|
||||
baz", @"<document>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
<softbreak />
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
<thematic_break />
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"Foo
|
||||
bar
|
||||
* * *
|
||||
baz", @"<document>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
<softbreak />
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
<thematic_break />
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"Foo
|
||||
bar
|
||||
\---
|
||||
baz", @"<document>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
<softbreak />
|
||||
<text>bar</text>
|
||||
<softbreak />
|
||||
<text>-</text>
|
||||
<text>--</text>
|
||||
<softbreak />
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_SetExtHeading(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
}
|
||||
487
Radzen.Blazor.Tests/Markdown/HtmlBlockTests.cs
Normal file
487
Radzen.Blazor.Tests/Markdown/HtmlBlockTests.cs
Normal file
@@ -0,0 +1,487 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class HtmlBlockTests
|
||||
{
|
||||
private static string ToXml(string markdown)
|
||||
{
|
||||
var document = MarkdownParser.Parse(markdown);
|
||||
|
||||
return XmlVisitor.ToXml(document);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"<table><tr><td>
|
||||
<pre>
|
||||
**Hello**,
|
||||
|
||||
_world_.
|
||||
</pre>
|
||||
</td></tr></table>", @"<document>
|
||||
<html_block><table><tr><td>
|
||||
<pre>
|
||||
**Hello**,</html_block>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>world</text>
|
||||
</emph>
|
||||
<text>.</text>
|
||||
<softbreak />
|
||||
<html_inline></pre></html_inline>
|
||||
</paragraph>
|
||||
<html_block></td></tr></table></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<table>
|
||||
<tr>
|
||||
<td>
|
||||
hi
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
okay.", @"<document>
|
||||
<html_block><table>
|
||||
<tr>
|
||||
<td>
|
||||
hi
|
||||
</td>
|
||||
</tr>
|
||||
</table></html_block>
|
||||
<paragraph>
|
||||
<text>okay.</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@" <div>
|
||||
*hello*
|
||||
<foo><a>", @"<document>
|
||||
<html_block> <div>
|
||||
*hello*
|
||||
<foo><a></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"</div>
|
||||
*foo*", @"<document>
|
||||
<html_block></div>
|
||||
*foo*</html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<DIV CLASS=""foo"">
|
||||
|
||||
*Markdown*
|
||||
|
||||
</DIV>", @"<document>
|
||||
<html_block><DIV CLASS=""foo""></html_block>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>Markdown</text>
|
||||
</emph>
|
||||
</paragraph>
|
||||
<html_block></DIV></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<div id=""foo""
|
||||
class=""bar"">
|
||||
</div>", @"<document>
|
||||
<html_block><div id=""foo""
|
||||
class=""bar"">
|
||||
</div></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<div id=""foo"" class=""bar
|
||||
baz"">
|
||||
</div>", @"<document>
|
||||
<html_block><div id=""foo"" class=""bar
|
||||
baz"">
|
||||
</div></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<div>
|
||||
*foo*
|
||||
|
||||
*bar*", @"<document>
|
||||
<html_block><div>
|
||||
*foo*</html_block>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>bar</text>
|
||||
</emph>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<div id=""foo""
|
||||
*hi*", @"<document>
|
||||
<html_block><div id=""foo""
|
||||
*hi*</html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<div class
|
||||
foo", @"<document>
|
||||
<html_block><div class
|
||||
foo</html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<div *???-&&&-<---
|
||||
*foo*", @"<document>
|
||||
<html_block><div *???-&&&-<---
|
||||
*foo*</html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<div><a href=""bar"">*foo*</a></div>", @"<document>
|
||||
<html_block><div><a href=""bar"">*foo*</a></div></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<table><tr><td>
|
||||
foo
|
||||
</td></tr></table>", @"<document>
|
||||
<html_block><table><tr><td>
|
||||
foo
|
||||
</td></tr></table></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<div></div>
|
||||
``` c
|
||||
int x = 33;
|
||||
```", @"<document>
|
||||
<html_block><div></div>
|
||||
``` c
|
||||
int x = 33;
|
||||
```</html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<a href=""foo"">
|
||||
*bar*
|
||||
</a>", @"<document>
|
||||
<html_block><a href=""foo"">
|
||||
*bar*
|
||||
</a></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<Warning>
|
||||
*bar*
|
||||
</Warning>", @"<document>
|
||||
<html_block><Warning>
|
||||
*bar*
|
||||
</Warning></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<i class=""foo"">
|
||||
*bar*
|
||||
</i>", @"<document>
|
||||
<html_block><i class=""foo"">
|
||||
*bar*
|
||||
</i></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"</ins>
|
||||
*bar*", @"<document>
|
||||
<html_block></ins>
|
||||
*bar*</html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<del>
|
||||
*foo*
|
||||
</del>", @"<document>
|
||||
<html_block><del>
|
||||
*foo*
|
||||
</del></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<del>
|
||||
|
||||
*foo*
|
||||
|
||||
</del>", @"<document>
|
||||
<html_block><del></html_block>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>foo</text>
|
||||
</emph>
|
||||
</paragraph>
|
||||
<html_block></del></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<del>*foo*</del>", @"<document>
|
||||
<paragraph>
|
||||
<html_inline><del></html_inline>
|
||||
<emph>
|
||||
<text>foo</text>
|
||||
</emph>
|
||||
<html_inline></del></html_inline>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<pre language=""haskell""><code>
|
||||
import Text.HTML.TagSoup
|
||||
|
||||
main :: IO ()
|
||||
main = print $ parseTags tags
|
||||
</code></pre>
|
||||
okay", @"<document>
|
||||
<html_block><pre language=""haskell""><code>
|
||||
import Text.HTML.TagSoup
|
||||
|
||||
main :: IO ()
|
||||
main = print $ parseTags tags
|
||||
</code></pre></html_block>
|
||||
<paragraph>
|
||||
<text>okay</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<script type=""text/javascript"">
|
||||
// JavaScript example
|
||||
|
||||
document.getElementById(""demo"").innerHTML = ""Hello JavaScript!"";
|
||||
</script>
|
||||
okay", @"<document>
|
||||
<html_block><script type=""text/javascript"">
|
||||
// JavaScript example
|
||||
|
||||
document.getElementById(""demo"").innerHTML = ""Hello JavaScript!"";
|
||||
</script></html_block>
|
||||
<paragraph>
|
||||
<text>okay</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<textarea>
|
||||
|
||||
*foo*
|
||||
|
||||
_bar_
|
||||
|
||||
</textarea>", @"<document>
|
||||
<html_block><textarea>
|
||||
|
||||
*foo*
|
||||
|
||||
_bar_
|
||||
|
||||
</textarea></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<style
|
||||
type=""text/css"">
|
||||
h1 {color:red;}
|
||||
|
||||
p {color:blue;}
|
||||
</style>
|
||||
okay",@"<document>
|
||||
<html_block><style
|
||||
type=""text/css"">
|
||||
h1 {color:red;}
|
||||
|
||||
p {color:blue;}
|
||||
</style></html_block>
|
||||
<paragraph>
|
||||
<text>okay</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<style
|
||||
type=""text/css"">
|
||||
|
||||
foo", @"<document>
|
||||
<html_block><style
|
||||
type=""text/css"">
|
||||
|
||||
foo</html_block>
|
||||
</document>")]
|
||||
[InlineData(@"> <div>
|
||||
> foo
|
||||
|
||||
bar", @"<document>
|
||||
<block_quote>
|
||||
<html_block><div>
|
||||
foo</html_block>
|
||||
</block_quote>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"- <div>
|
||||
- foo", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<html_block><div></html_block>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"<style>p{color:red;}</style>
|
||||
*foo*", @"<document>
|
||||
<html_block><style>p{color:red;}</style></html_block>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>foo</text>
|
||||
</emph>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<!-- foo -->*bar*
|
||||
*baz*", @"<document>
|
||||
<html_block><!-- foo -->*bar*</html_block>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>baz</text>
|
||||
</emph>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<script>
|
||||
foo
|
||||
</script>1. *bar*", @"<document>
|
||||
<html_block><script>
|
||||
foo
|
||||
</script>1. *bar*</html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<!-- Foo
|
||||
|
||||
bar
|
||||
baz -->
|
||||
okay", @"<document>
|
||||
<html_block><!-- Foo
|
||||
|
||||
bar
|
||||
baz --></html_block>
|
||||
<paragraph>
|
||||
<text>okay</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<?php
|
||||
|
||||
echo '>';
|
||||
|
||||
?>
|
||||
okay", @"<document>
|
||||
<html_block><?php
|
||||
|
||||
echo '>';
|
||||
|
||||
?></html_block>
|
||||
<paragraph>
|
||||
<text>okay</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<!DOCTYPE html>", @"<document>
|
||||
<html_block><!DOCTYPE html></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<![CDATA[
|
||||
function matchwo(a,b)
|
||||
{
|
||||
if (a < b && a < 0) then {
|
||||
return 1;
|
||||
|
||||
} else {
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
]]>
|
||||
okay", @"<document>
|
||||
<html_block><![CDATA[
|
||||
function matchwo(a,b)
|
||||
{
|
||||
if (a < b && a < 0) then {
|
||||
return 1;
|
||||
|
||||
} else {
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
]]></html_block>
|
||||
<paragraph>
|
||||
<text>okay</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@" <!-- foo -->
|
||||
|
||||
<!-- foo -->", @"<document>
|
||||
<html_block> <!-- foo --></html_block>
|
||||
<code_block><!-- foo -->
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@" <div>
|
||||
|
||||
<div>", @"<document>
|
||||
<html_block> <div></html_block>
|
||||
<code_block><div>
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"Foo
|
||||
<div>
|
||||
bar
|
||||
</div>", @"<document>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
</paragraph>
|
||||
<html_block><div>
|
||||
bar
|
||||
</div></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<div>
|
||||
bar
|
||||
</div>
|
||||
*foo*", @"<document>
|
||||
<html_block><div>
|
||||
bar
|
||||
</div>
|
||||
*foo*</html_block>
|
||||
</document>")]
|
||||
[InlineData(@"Foo
|
||||
<a href=""bar"">
|
||||
baz", @"<document>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
<softbreak />
|
||||
<html_inline><a href=""bar""></html_inline>
|
||||
<softbreak />
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<div>
|
||||
|
||||
*Emphasized* text.
|
||||
|
||||
</div>", @"<document>
|
||||
<html_block><div></html_block>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>Emphasized</text>
|
||||
</emph>
|
||||
<text> text.</text>
|
||||
</paragraph>
|
||||
<html_block></div></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<div>
|
||||
*Emphasized* text.
|
||||
</div>", @"<document>
|
||||
<html_block><div>
|
||||
*Emphasized* text.
|
||||
</div></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<table>
|
||||
|
||||
<tr>
|
||||
|
||||
<td>
|
||||
Hi
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>", @"<document>
|
||||
<html_block><table></html_block>
|
||||
<html_block><tr></html_block>
|
||||
<html_block><td>
|
||||
Hi
|
||||
</td></html_block>
|
||||
<html_block></tr></html_block>
|
||||
<html_block></table></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"<table>
|
||||
|
||||
<tr>
|
||||
|
||||
<td>
|
||||
Hi
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
||||
</table>", @"<document>
|
||||
<html_block><table></html_block>
|
||||
<html_block> <tr></html_block>
|
||||
<code_block><td>
|
||||
Hi
|
||||
</td>
|
||||
</code_block>
|
||||
<html_block> </tr></html_block>
|
||||
<html_block></table></html_block>
|
||||
</document>")]
|
||||
public void Parse_HtmlBlock(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
}
|
||||
144
Radzen.Blazor.Tests/Markdown/HtmlInlineTests.cs
Normal file
144
Radzen.Blazor.Tests/Markdown/HtmlInlineTests.cs
Normal file
@@ -0,0 +1,144 @@
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class HtmlInlineTests
|
||||
{
|
||||
private static string ToXml(string markdown)
|
||||
{
|
||||
var document = MarkdownParser.Parse(markdown);
|
||||
|
||||
return XmlVisitor.ToXml(document);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"<a><bab><c2c>", @"<document>
|
||||
<paragraph>
|
||||
<html_inline><a></html_inline>
|
||||
<html_inline><bab></html_inline>
|
||||
<html_inline><c2c></html_inline>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<a/><b2/>", @"<document>
|
||||
<paragraph>
|
||||
<html_inline><a/></html_inline>
|
||||
<html_inline><b2/></html_inline>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<a /><b2
|
||||
data=""foo"" >", @"<document>
|
||||
<paragraph>
|
||||
<html_inline><a /></html_inline>
|
||||
<html_inline><b2
|
||||
data=""foo"" ></html_inline>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<a foo=""bar"" bam = 'baz <em>""</em>'
|
||||
_boolean zoop:33=zoop:33 />", @"<document>
|
||||
<paragraph>
|
||||
<html_inline><a foo=""bar"" bam = 'baz <em>""</em>'
|
||||
_boolean zoop:33=zoop:33 /></html_inline>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"Foo <responsive-image src=""foo.jpg"" />", @"<document>
|
||||
<paragraph>
|
||||
<text>Foo </text>
|
||||
<html_inline><responsive-image src=""foo.jpg"" /></html_inline>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("<33> <__>", @"<document>
|
||||
<paragraph>
|
||||
<text><33> <</text>
|
||||
<text>__</text>
|
||||
<text>></text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<a h*#ref=""hi"">", @"<document>
|
||||
<paragraph>
|
||||
<text><a h</text>
|
||||
<text>*</text>
|
||||
<text>#ref=""hi""></text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<a href='bar'title=title>", @"<document>
|
||||
<paragraph>
|
||||
<text><a href='bar'title=title></text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"</a></foo >", @"<document>
|
||||
<paragraph>
|
||||
<html_inline></a></html_inline>
|
||||
<html_inline></foo ></html_inline>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"</a href=""foo"">", @"<document>
|
||||
<paragraph>
|
||||
<text></a href=""foo""></text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"foo <!-- this is a --
|
||||
comment - with hyphens -->", @"<document>
|
||||
<paragraph>
|
||||
<text>foo </text>
|
||||
<html_inline><!-- this is a --
|
||||
comment - with hyphens --></html_inline>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"foo <!--> foo -->
|
||||
|
||||
foo <!---> foo -->
|
||||
", @"<document>
|
||||
<paragraph>
|
||||
<text>foo </text>
|
||||
<html_inline><!--></html_inline>
|
||||
<text> foo --></text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>foo </text>
|
||||
<html_inline><!---></html_inline>
|
||||
<text> foo --></text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"foo <?php echo $a; ?>", @"<document>
|
||||
<paragraph>
|
||||
<text>foo </text>
|
||||
<html_inline><?php echo $a; ?></html_inline>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"foo <!ELEMENT br EMPTY>", @"<document>
|
||||
<paragraph>
|
||||
<text>foo </text>
|
||||
<html_inline><!ELEMENT br EMPTY></html_inline>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"foo <![CDATA[>&<]]>", @"<document>
|
||||
<paragraph>
|
||||
<text>foo </text>
|
||||
<html_inline><![CDATA[>&<]]></html_inline>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"foo <a href=""ö"">", @"<document>
|
||||
<paragraph>
|
||||
<text>foo </text>
|
||||
<html_inline><a href=""&ouml;""></html_inline>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"foo <a href=""\*"">", @"<document>
|
||||
<paragraph>
|
||||
<text>foo </text>
|
||||
<html_inline><a href=""\*""></html_inline>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"foo <a href=""\"""">", @"<document>
|
||||
<paragraph>
|
||||
<text>foo <a href=""</text>
|
||||
<text>""</text>
|
||||
<text>""></text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_Html(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
}
|
||||
85
Radzen.Blazor.Tests/Markdown/ImageTests.cs
Normal file
85
Radzen.Blazor.Tests/Markdown/ImageTests.cs
Normal file
@@ -0,0 +1,85 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class ImageTests
|
||||
{
|
||||
|
||||
private static string ToXml(string markdown)
|
||||
{
|
||||
var document = MarkdownParser.Parse(markdown);
|
||||
|
||||
return XmlVisitor.ToXml(document);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"", @"<document>
|
||||
<paragraph>
|
||||
<image destination=""/url"" title=""title"">
|
||||
<text>foo</text>
|
||||
</image>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"](/url2)", @"<document>
|
||||
<paragraph>
|
||||
<image destination=""/url2"" title="""">
|
||||
<text>foo </text>
|
||||
<image destination=""/url"" title="""">
|
||||
<text>bar</text>
|
||||
</image>
|
||||
</image>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"](/url2)", @"<document>
|
||||
<paragraph>
|
||||
<image destination=""/url2"" title="""">
|
||||
<text>foo </text>
|
||||
<link destination=""/url"" title="""">
|
||||
<text>bar</text>
|
||||
</link>
|
||||
</image>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"", @"<document>
|
||||
<paragraph>
|
||||
<image destination=""train.jpg"" title="""">
|
||||
<text>foo</text>
|
||||
</image>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"My ", @"<document>
|
||||
<paragraph>
|
||||
<text>My </text>
|
||||
<image destination=""/path/to/train.jpg"" title=""title"">
|
||||
<text>foo bar</text>
|
||||
</image>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"", @"<document>
|
||||
<paragraph>
|
||||
<image destination=""url"" title="""">
|
||||
<text>foo</text>
|
||||
</image>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"", @"<document>
|
||||
<paragraph>
|
||||
<image destination=""/url"" title="""" />
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"__Applications__ ", @"<document>
|
||||
<paragraph>
|
||||
<strong>
|
||||
<text>Applications</text>
|
||||
</strong>
|
||||
<text> </text>
|
||||
<image destination=""/assets/img/macOS-drag-and-drop.png"" title="""">
|
||||
<text>macOS DMG</text>
|
||||
</image>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_BasicImages_ReturnsImageElement(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
}
|
||||
145
Radzen.Blazor.Tests/Markdown/IndentedCodeBlockTests.cs
Normal file
145
Radzen.Blazor.Tests/Markdown/IndentedCodeBlockTests.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class IndentedCodeBlockTests
|
||||
{
|
||||
private static string ToXml(string markdown)
|
||||
{
|
||||
var document = MarkdownParser.Parse(markdown);
|
||||
|
||||
return XmlVisitor.ToXml(document);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@" a simple
|
||||
indented code block
|
||||
", @"<document>
|
||||
<code_block>a simple
|
||||
indented code block
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@" - foo
|
||||
|
||||
bar", @"<document>
|
||||
<list type=""bullet"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(
|
||||
@"
|
||||
1. foo
|
||||
|
||||
- bar", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@" <a/>
|
||||
*hi*
|
||||
|
||||
- one", @"<document>
|
||||
<code_block><a/>
|
||||
*hi*
|
||||
|
||||
- one
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@" chunk1
|
||||
|
||||
chunk2
|
||||
|
||||
|
||||
|
||||
chunk3", @"<document>
|
||||
<code_block>chunk1
|
||||
|
||||
chunk2
|
||||
|
||||
|
||||
|
||||
chunk3
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@" chunk1
|
||||
|
||||
chunk2", @"<document>
|
||||
<code_block>chunk1
|
||||
|
||||
chunk2
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"Foo
|
||||
bar", @"<document>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
<softbreak />
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@" foo
|
||||
bar", @"<document>
|
||||
<code_block>foo
|
||||
</code_block>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"# Heading
|
||||
foo
|
||||
Heading
|
||||
------
|
||||
foo
|
||||
----", @"<document>
|
||||
<heading level=""1"">
|
||||
<text>Heading</text>
|
||||
</heading>
|
||||
<code_block>foo
|
||||
</code_block>
|
||||
<heading level=""2"">
|
||||
<text>Heading</text>
|
||||
</heading>
|
||||
<code_block>foo
|
||||
</code_block>
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
[InlineData(@" foo
|
||||
bar", @"<document>
|
||||
<code_block> foo
|
||||
bar
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"
|
||||
|
||||
foo
|
||||
", @"<document>
|
||||
<code_block>foo
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@" foo ", @"<document>
|
||||
<code_block>foo
|
||||
</code_block>
|
||||
</document>")]
|
||||
public void Parse_IndentedCodeBlock(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
}
|
||||
852
Radzen.Blazor.Tests/Markdown/LinkTests.cs
Normal file
852
Radzen.Blazor.Tests/Markdown/LinkTests.cs
Normal file
@@ -0,0 +1,852 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class LinkTests
|
||||
{
|
||||
private static string ToXml(string markdown)
|
||||
{
|
||||
var document = MarkdownParser.Parse(markdown);
|
||||
|
||||
return XmlVisitor.ToXml(document);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("[link](/uri \"title\")",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<link destination=""/uri"" title=""title"">
|
||||
<text>link</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("[link](/uri)", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""/uri"" title="""">
|
||||
<text>link</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("[](./target.md)", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""./target.md"" title="""" />
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("[link]()", @"<document>
|
||||
<paragraph>
|
||||
<link destination="""" title="""">
|
||||
<text>link</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("[link](<>)", @"<document>
|
||||
<paragraph>
|
||||
<link destination="""" title="""">
|
||||
<text>link</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("[]()", @"<document>
|
||||
<paragraph>
|
||||
<link destination="""" title="""" />
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_BasicLinks_ReturnsLinkNode(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("[link](/my uri)", @"<document>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>link</text>
|
||||
<text>](/my uri)</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("[link](</my uri>)", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""/my uri"" title="""">
|
||||
<text>link</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_LinkDestinationWithSpaces(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"[link](foo
|
||||
bar)", @"<document>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>link</text>
|
||||
<text>](foo</text>
|
||||
<softbreak />
|
||||
<text>bar)</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
|
||||
public void Parse_LinkDestinationWithNewLines(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"[a](<b)c>)", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""b)c"" title="""">
|
||||
<text>a</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_LinkDestinationWithCloseParenthesis(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"[link](<foo\>)", @"<document>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>link</text>
|
||||
<text>](<foo>)</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[a](<b)c
|
||||
[a](<b)c>
|
||||
[a](<b>c)", @"<document>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>a</text>
|
||||
<text>](<b)c</text>
|
||||
<softbreak />
|
||||
<text>[</text>
|
||||
<text>a</text>
|
||||
<text>](<b)c></text>
|
||||
<softbreak />
|
||||
<text>[</text>
|
||||
<text>a</text>
|
||||
<text>](</text>
|
||||
<html_inline><b></html_inline>
|
||||
<text>c)</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_LinkDestinationUnclosedPointyBracket(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"[link](\(foo\))", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""(foo)"" title="""">
|
||||
<text>link</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[link](foo\)\:)", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""foo):"" title="""">
|
||||
<text>link</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[link](foo\bar)", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""foo\bar"" title="""">
|
||||
<text>link</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_Escapes(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"[link](foo(and(bar)))", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""foo(and(bar))"" title="""">
|
||||
<text>link</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[link](foo(and(bar))", @"<document>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>link</text>
|
||||
<text>](foo(and(bar))</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[link](foo\(and\(bar\))", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""foo(and(bar)"" title="""">
|
||||
<text>link</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[link](<foo(and(bar)>)", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""foo(and(bar)"" title="""">
|
||||
<text>link</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_BallancedParenthesisInLinkDestination(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"[link](#fragment)
|
||||
[link](https://example.com#fragment)
|
||||
[link](https://example.com?foo=3#frag)", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""#fragment"" title="""">
|
||||
<text>link</text>
|
||||
</link>
|
||||
<softbreak />
|
||||
<link destination=""https://example.com#fragment"" title="""">
|
||||
<text>link</text>
|
||||
</link>
|
||||
<softbreak />
|
||||
<link destination=""https://example.com?foo=3#frag"" title="""">
|
||||
<text>link</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_FragmentAndQueryStringInLinkDestination(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"[link](""title"")", @"<document>
|
||||
<paragraph>
|
||||
<link destination="""title""" title="""">
|
||||
<text>link</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_QuotesInDestination(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"[link](/url ""title"")
|
||||
[link](/url 'title')
|
||||
[link](/url (title))", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""/url"" title=""title"">
|
||||
<text>link</text>
|
||||
</link>
|
||||
<softbreak />
|
||||
<link destination=""/url"" title=""title"">
|
||||
<text>link</text>
|
||||
</link>
|
||||
<softbreak />
|
||||
<link destination=""/url"" title=""title"">
|
||||
<text>link</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[link](/url ""title ""and"" title"")", @"<document>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>link</text>
|
||||
<text>](/url ""title ""and"" title"")</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[link](/url 'title ""and"" title')", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""/url"" title=""title "and" title"">
|
||||
<text>link</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_Title(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"[link]( /url
|
||||
""title"" )", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""/url"" title=""title"">
|
||||
<text>link</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[link] (/uri)", @"<document>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>link</text>
|
||||
<text>] (/uri)</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_SpacesInLink(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"[link [foo [bar]]](/uri)", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""/uri"" title="""">
|
||||
<text>link </text>
|
||||
<text>[</text>
|
||||
<text>foo </text>
|
||||
<text>[</text>
|
||||
<text>bar</text>
|
||||
<text>]</text>
|
||||
<text>]</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[link] bar](/uri)", @"<document>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>link</text>
|
||||
<text>] bar](/uri)</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[link [bar](/uri)", @"<document>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>link </text>
|
||||
<link destination=""/uri"" title="""">
|
||||
<text>bar</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[link \[bar](/uri)", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""/uri"" title="""">
|
||||
<text>link </text>
|
||||
<text>[</text>
|
||||
<text>bar</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_BracketsInText(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"[link *foo **bar** `#`*](/uri)", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""/uri"" title="""">
|
||||
<text>link </text>
|
||||
<emph>
|
||||
<text>foo </text>
|
||||
<strong>
|
||||
<text>bar</text>
|
||||
</strong>
|
||||
<text> </text>
|
||||
<code>#</code>
|
||||
</emph>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[](url)", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""url"" title="""">
|
||||
<image destination=""img"" title="""">
|
||||
<text>alt</text>
|
||||
</image>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
|
||||
public void Parse_LinkTextIsInlineContent(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"[foo [bar](/uri)](/uri)", @"<document>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>foo </text>
|
||||
<link destination=""/uri"" title="""">
|
||||
<text>bar</text>
|
||||
</link>
|
||||
<text>](/uri)</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[foo *[bar [baz](/uri)](/uri)*](/uri)", @"<document>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>foo </text>
|
||||
<emph>
|
||||
<text>[</text>
|
||||
<text>bar </text>
|
||||
<link destination=""/uri"" title="""">
|
||||
<text>baz</text>
|
||||
</link>
|
||||
<text>](/uri)</text>
|
||||
</emph>
|
||||
<text>](/uri)</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_NestedLinks(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"*[foo*](/uri)", @"<document>
|
||||
<paragraph>
|
||||
<text>*</text>
|
||||
<link destination=""/uri"" title="""">
|
||||
<text>foo</text>
|
||||
<text>*</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[foo *bar](baz*)", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""baz*"" title="""">
|
||||
<text>foo </text>
|
||||
<text>*</text>
|
||||
<text>bar</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"*foo [bar* baz]", @"<document>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>foo </text>
|
||||
<text>[</text>
|
||||
<text>bar</text>
|
||||
</emph>
|
||||
<text> baz</text>
|
||||
<text>]</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[foo`](/uri)`", @"<document>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>foo</text>
|
||||
<code>](/uri)</code>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_Precedence(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"<http://foo.bar.baz>", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""http://foo.bar.baz"" title="""">
|
||||
<text>http://foo.bar.baz</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<https://foo.bar.baz/test?q=hello&id=22&boolean>", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""https://foo.bar.baz/test?q=hello&id=22&boolean"" title="""">
|
||||
<text>https://foo.bar.baz/test?q=hello&id=22&boolean</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<irc://foo.bar:2233/baz>", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""irc://foo.bar:2233/baz"" title="""">
|
||||
<text>irc://foo.bar:2233/baz</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<MAILTO:FOO@BAR.BAZ>", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""MAILTO:FOO@BAR.BAZ"" title="""">
|
||||
<text>MAILTO:FOO@BAR.BAZ</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<https://foo.bar/baz bim>", @"<document>
|
||||
<paragraph>
|
||||
<text><https://foo.bar/baz bim></text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<https://example.com/\[\>", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""https://example.com/\[\"" title="""">
|
||||
<text>https://example.com/\[\</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<foo@bar.example.com>", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""mailto:foo@bar.example.com"" title="""">
|
||||
<text>foo@bar.example.com</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<foo+special@Bar.baz-bar0.com>", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""mailto:foo+special@Bar.baz-bar0.com"" title="""">
|
||||
<text>foo+special@Bar.baz-bar0.com</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<foo\+@bar.example.com>", @"<document>
|
||||
<paragraph>
|
||||
<text><foo+@bar.example.com></text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<>", @"<document>
|
||||
<paragraph>
|
||||
<text><></text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"< https://foo.bar >", @"<document>
|
||||
<paragraph>
|
||||
<text>< https://foo.bar ></text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<m:abc>", @"<document>
|
||||
<paragraph>
|
||||
<text><m:abc></text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"<foo.bar.baz>", @"<document>
|
||||
<paragraph>
|
||||
<text><foo.bar.baz></text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"https://example.com", @"<document>
|
||||
<paragraph>
|
||||
<text>https://example.com</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"foo@bar.example.com", @"<document>
|
||||
<paragraph>
|
||||
<text>foo@bar.example.com</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_AutoLink(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"[foo]: /url ""title""
|
||||
|
||||
[foo]", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""/url"" title=""title"">
|
||||
<text>foo</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@" [foo]:
|
||||
/url
|
||||
'the title'
|
||||
|
||||
[foo]", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""/url"" title=""the title"">
|
||||
<text>foo</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[Foo*bar\]]:my_(url) 'title (with parens)'
|
||||
|
||||
[Foo*bar\]]", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""my_(url)"" title=""title (with parens)"">
|
||||
<text>Foo</text>
|
||||
<text>*</text>
|
||||
<text>bar</text>
|
||||
<text>]</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[Foo bar]:
|
||||
<my url>
|
||||
'title'
|
||||
|
||||
[Foo bar]", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""my url"" title=""title"">
|
||||
<text>Foo bar</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[foo]: /url '
|
||||
title
|
||||
line1
|
||||
line2
|
||||
'
|
||||
|
||||
[foo]", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""/url"" title=""
title
line1
line2
"">
|
||||
<text>foo</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[foo]: /url 'title
|
||||
|
||||
with blank line'
|
||||
|
||||
[foo]", @"<document>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>foo</text>
|
||||
<text>]: /url 'title</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>with blank line'</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>foo</text>
|
||||
<text>]</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[foo]:
|
||||
/url
|
||||
|
||||
[foo]", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""/url"" title="""">
|
||||
<text>foo</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[foo]:
|
||||
|
||||
[foo]", @"<document>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>foo</text>
|
||||
<text>]:</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>foo</text>
|
||||
<text>]</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[foo]: <>
|
||||
|
||||
[foo]", @"<document>
|
||||
<paragraph>
|
||||
<link destination="""" title="""">
|
||||
<text>foo</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[foo]: <bar>(baz)
|
||||
|
||||
[foo]", @"<document>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>foo</text>
|
||||
<text>]: </text>
|
||||
<html_inline><bar></html_inline>
|
||||
<text>(baz)</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>foo</text>
|
||||
<text>]</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[foo]: /url\bar\*baz ""foo\""bar\baz""
|
||||
|
||||
[foo]
|
||||
", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""/url\bar*baz"" title=""foo\"bar\baz"">
|
||||
<text>foo</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[foo]
|
||||
|
||||
[foo]: url", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""url"" title="""">
|
||||
<text>foo</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[foo]
|
||||
|
||||
[foo]: first
|
||||
[foo]: second", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""first"" title="""">
|
||||
<text>foo</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[FOO]: /url
|
||||
|
||||
[Foo]", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""/url"" title="""">
|
||||
<text>Foo</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[ΑΓΩ]: /φου
|
||||
|
||||
[αγω]", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""/φου"" title="""">
|
||||
<text>αγω</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[foo]: /url", @"<document />")]
|
||||
[InlineData(@"[
|
||||
foo
|
||||
]: /url
|
||||
bar", @"<document>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[foo]: /url ""title"" ok", @"<document>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>foo</text>
|
||||
<text>]: /url ""title"" ok</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[foo]: /url
|
||||
""title"" ok", @"<document>
|
||||
<paragraph>
|
||||
<text>""title"" ok</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@" [foo]: /url ""title""
|
||||
|
||||
[foo]", @"<document>
|
||||
<code_block>[foo]: /url ""title""
|
||||
</code_block>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>foo</text>
|
||||
<text>]</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"```
|
||||
[foo]: /url
|
||||
```
|
||||
|
||||
[foo]", @"<document>
|
||||
<code_block>[foo]: /url
|
||||
</code_block>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>foo</text>
|
||||
<text>]</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"Foo
|
||||
[bar]: /baz
|
||||
|
||||
[bar]", @"<document>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
<softbreak />
|
||||
<text>[</text>
|
||||
<text>bar</text>
|
||||
<text>]: /baz</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>[</text>
|
||||
<text>bar</text>
|
||||
<text>]</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"# [Foo]
|
||||
[foo]: /url
|
||||
> bar", @"<document>
|
||||
<heading level=""1"">
|
||||
<link destination=""/url"" title="""">
|
||||
<text>Foo</text>
|
||||
</link>
|
||||
</heading>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@"[foo]: /url
|
||||
bar
|
||||
===
|
||||
[foo]", @"<document>
|
||||
<heading level=""1"">
|
||||
<text>bar</text>
|
||||
</heading>
|
||||
<paragraph>
|
||||
<link destination=""/url"" title="""">
|
||||
<text>foo</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[foo]: /url
|
||||
===
|
||||
[foo]", @"<document>
|
||||
<paragraph>
|
||||
<text>===</text>
|
||||
<softbreak />
|
||||
<link destination=""/url"" title="""">
|
||||
<text>foo</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[foo]: /foo-url ""foo""
|
||||
[bar]: /bar-url
|
||||
""bar""
|
||||
[baz]: /baz-url
|
||||
|
||||
[foo],
|
||||
[bar],
|
||||
[baz]", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""/foo-url"" title=""foo"">
|
||||
<text>foo</text>
|
||||
</link>
|
||||
<text>,</text>
|
||||
<softbreak />
|
||||
<link destination=""/bar-url"" title=""bar"">
|
||||
<text>bar</text>
|
||||
</link>
|
||||
<text>,</text>
|
||||
<softbreak />
|
||||
<link destination=""/baz-url"" title="""">
|
||||
<text>baz</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"[foo]
|
||||
|
||||
> [foo]: /url
|
||||
", @"<document>
|
||||
<paragraph>
|
||||
<link destination=""/url"" title="""">
|
||||
<text>foo</text>
|
||||
</link>
|
||||
</paragraph>
|
||||
<block_quote />
|
||||
</document>")]
|
||||
public void Parse_LinkReference(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
}
|
||||
880
Radzen.Blazor.Tests/Markdown/ListItemTests.cs
Normal file
880
Radzen.Blazor.Tests/Markdown/ListItemTests.cs
Normal file
@@ -0,0 +1,880 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class ListItemTests
|
||||
{
|
||||
private static string ToXml(string markdown)
|
||||
{
|
||||
var document = MarkdownParser.Parse(markdown);
|
||||
|
||||
return XmlVisitor.ToXml(document);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"1. A paragraph
|
||||
with two lines.
|
||||
|
||||
indented code
|
||||
|
||||
> A block quote.", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>A paragraph</text>
|
||||
<softbreak />
|
||||
<text>with two lines.</text>
|
||||
</paragraph>
|
||||
<code_block>indented code
|
||||
</code_block>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>A block quote.</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- one
|
||||
|
||||
two", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>one</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
<paragraph>
|
||||
<text>two</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"- one
|
||||
|
||||
two", @"<document>
|
||||
<list type=""bullet"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>one</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>two</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@" - one
|
||||
|
||||
two", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>one</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
<code_block> two
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@" - one
|
||||
|
||||
two", @"<document>
|
||||
<list type=""bullet"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>one</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>two</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@" > > 1. one
|
||||
>>
|
||||
>> two
|
||||
", @"<document>
|
||||
<block_quote>
|
||||
<block_quote>
|
||||
<list type=""ordered"" start=""1"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>one</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>two</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</block_quote>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@">>- one
|
||||
>>
|
||||
> > two
|
||||
", @"<document>
|
||||
<block_quote>
|
||||
<block_quote>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>one</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
<paragraph>
|
||||
<text>two</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@"-one
|
||||
|
||||
2.two
|
||||
", @"<document>
|
||||
<paragraph>
|
||||
<text>-one</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>2.two</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"- foo
|
||||
|
||||
|
||||
bar", @"<document>
|
||||
<list type=""bullet"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"1. foo
|
||||
|
||||
```
|
||||
bar
|
||||
```
|
||||
|
||||
baz
|
||||
|
||||
> bam", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
<code_block>bar
|
||||
</code_block>
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>bam</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- Foo
|
||||
|
||||
bar
|
||||
|
||||
|
||||
baz", @"<document>
|
||||
<list type=""bullet"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
</paragraph>
|
||||
<code_block>bar
|
||||
|
||||
|
||||
baz
|
||||
</code_block>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"123456789. ok", @"<document>
|
||||
<list type=""ordered"" start=""123456789"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>ok</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"1234567890. not ok", @"<document>
|
||||
<paragraph>
|
||||
<text>1234567890. not ok</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"0. ok", @"<document>
|
||||
<list type=""ordered"" start=""0"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>ok</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"003. ok", @"<document>
|
||||
<list type=""ordered"" start=""3"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>ok</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"-1. not ok", @"<document>
|
||||
<paragraph>
|
||||
<text>-1. not ok</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"- foo
|
||||
|
||||
bar", @"<document>
|
||||
<list type=""bullet"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
<code_block>bar
|
||||
</code_block>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@" 10. foo
|
||||
|
||||
bar", @"<document>
|
||||
<list type=""ordered"" start=""10"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
<code_block>bar
|
||||
</code_block>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"1. indented code
|
||||
|
||||
paragraph
|
||||
|
||||
more code", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""false"">
|
||||
<item>
|
||||
<code_block>indented code
|
||||
</code_block>
|
||||
<paragraph>
|
||||
<text>paragraph</text>
|
||||
</paragraph>
|
||||
<code_block>more code
|
||||
</code_block>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- indented code
|
||||
|
||||
paragraph
|
||||
|
||||
more code", @"<document>
|
||||
<list type=""bullet"" tight=""false"">
|
||||
<item>
|
||||
<code_block>indented code
|
||||
</code_block>
|
||||
<paragraph>
|
||||
<text>paragraph</text>
|
||||
</paragraph>
|
||||
<code_block>more code
|
||||
</code_block>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"1. indented code
|
||||
|
||||
paragraph
|
||||
|
||||
more code", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""false"">
|
||||
<item>
|
||||
<code_block> indented code
|
||||
</code_block>
|
||||
<paragraph>
|
||||
<text>paragraph</text>
|
||||
</paragraph>
|
||||
<code_block>more code
|
||||
</code_block>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- foo
|
||||
|
||||
bar", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"- foo
|
||||
|
||||
bar", @"<document>
|
||||
<list type=""bullet"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"-
|
||||
foo
|
||||
-
|
||||
```
|
||||
bar
|
||||
```
|
||||
-
|
||||
baz", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<code_block>bar
|
||||
</code_block>
|
||||
</item>
|
||||
<item>
|
||||
<code_block>baz
|
||||
</code_block>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"1.
|
||||
foo", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"-
|
||||
foo", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"-
|
||||
|
||||
foo", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item />
|
||||
</list>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"- foo
|
||||
-
|
||||
- bar", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item />
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- foo
|
||||
-
|
||||
- bar", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item />
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"1. foo
|
||||
2.
|
||||
3. bar", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item />
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"*", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item />
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"foo
|
||||
*
|
||||
|
||||
foo
|
||||
1.", @"<document>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<softbreak />
|
||||
<text>*</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<softbreak />
|
||||
<text>1.</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@" 1. A paragraph
|
||||
with two lines.
|
||||
|
||||
indented code
|
||||
|
||||
> A block quote.", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>A paragraph</text>
|
||||
<softbreak />
|
||||
<text>with two lines.</text>
|
||||
</paragraph>
|
||||
<code_block>indented code
|
||||
</code_block>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>A block quote.</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@" 1. A paragraph
|
||||
with two lines.
|
||||
|
||||
indented code
|
||||
|
||||
> A block quote.", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>A paragraph</text>
|
||||
<softbreak />
|
||||
<text>with two lines.</text>
|
||||
</paragraph>
|
||||
<code_block>indented code
|
||||
</code_block>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>A block quote.</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@" 1. A paragraph
|
||||
with two lines.
|
||||
|
||||
indented code
|
||||
|
||||
> A block quote.", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>A paragraph</text>
|
||||
<softbreak />
|
||||
<text>with two lines.</text>
|
||||
</paragraph>
|
||||
<code_block>indented code
|
||||
</code_block>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>A block quote.</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@" 1. A paragraph
|
||||
with two lines.
|
||||
|
||||
indented code
|
||||
|
||||
> A block quote.", @"<document>
|
||||
<code_block>1. A paragraph
|
||||
with two lines.
|
||||
|
||||
indented code
|
||||
|
||||
> A block quote.
|
||||
</code_block>
|
||||
</document>")]
|
||||
public void Parse_ListItem(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@" 1. A paragraph
|
||||
with two lines.
|
||||
|
||||
indented code
|
||||
|
||||
> A block quote.", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>A paragraph</text>
|
||||
<softbreak />
|
||||
<text>with two lines.</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>indented code</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
<code_block> > A block quote.
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@" 1. A paragraph
|
||||
with two lines.", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>A paragraph</text>
|
||||
<softbreak />
|
||||
<text>with two lines.</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"> 1. > Blockquote
|
||||
continued here.", @"<document>
|
||||
<block_quote>
|
||||
<list type=""ordered"" start=""1"" tight=""true"">
|
||||
<item>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>Blockquote</text>
|
||||
<softbreak />
|
||||
<text>continued here.</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</item>
|
||||
</list>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@"> 1. > Blockquote
|
||||
> continued here.", @"<document>
|
||||
<block_quote>
|
||||
<list type=""ordered"" start=""1"" tight=""true"">
|
||||
<item>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>Blockquote</text>
|
||||
<softbreak />
|
||||
<text>continued here.</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</item>
|
||||
</list>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
public void Parse_ListItem_WithLazyContinuation(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"- foo
|
||||
- bar
|
||||
- baz
|
||||
- boo", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>boo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- foo
|
||||
- bar
|
||||
- baz
|
||||
- boo", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>boo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"1. foo
|
||||
1. bar
|
||||
1. baz
|
||||
1. boo", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>boo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"
|
||||
1. foo
|
||||
1. bar
|
||||
1. baz
|
||||
1. boo", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
<list type=""ordered"" start=""1"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
<list type=""ordered"" start=""1"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
<list type=""ordered"" start=""1"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>boo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"10) foo
|
||||
- bar", @"<document>
|
||||
<list type=""ordered"" start=""10"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"
|
||||
10) foo
|
||||
- bar", @"<document>
|
||||
<list type=""ordered"" start=""10"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- - foo", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"1. - foo", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""true"">
|
||||
<item>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"1. 1. foo", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""true"">
|
||||
<item>
|
||||
<list type=""ordered"" start=""1"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- 1. foo", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<list type=""ordered"" start=""1"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"1. - 2. foo", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""true"">
|
||||
<item>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<list type=""ordered"" start=""2"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- # Foo
|
||||
- Bar
|
||||
---
|
||||
baz", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<heading level=""1"">
|
||||
<text>Foo</text>
|
||||
</heading>
|
||||
</item>
|
||||
<item>
|
||||
<heading level=""2"">
|
||||
<text>Bar</text>
|
||||
</heading>
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
public void Parse_ListItem_WithNesting(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
}
|
||||
693
Radzen.Blazor.Tests/Markdown/ListTests.cs
Normal file
693
Radzen.Blazor.Tests/Markdown/ListTests.cs
Normal file
@@ -0,0 +1,693 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class ListTests
|
||||
{
|
||||
private static string ToXml(string markdown)
|
||||
{
|
||||
var document = MarkdownParser.Parse(markdown);
|
||||
|
||||
return XmlVisitor.ToXml(document);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"- foo
|
||||
- bar
|
||||
+ baz", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"1. foo
|
||||
2. bar
|
||||
3) baz", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
<list type=""ordered"" start=""3"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"Foo
|
||||
- bar
|
||||
- baz", @"<document>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
</paragraph>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
public void Parse_List(string markdown, string expected)
|
||||
{
|
||||
var actual = ToXml(markdown);
|
||||
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"The number of windows in my house is
|
||||
14. The number of doors is 6.", @"<document>
|
||||
<paragraph>
|
||||
<text>The number of windows in my house is</text>
|
||||
<softbreak />
|
||||
<text>14. The number of doors is 6.</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"The number of windows in my house is
|
||||
1. The number of doors is 6.", @"<document>
|
||||
<paragraph>
|
||||
<text>The number of windows in my house is</text>
|
||||
</paragraph>
|
||||
<list type=""ordered"" start=""1"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>The number of doors is 6.</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
public void Parse_OnlyNumberedListsThatStartWithOneCanInterruptParagraphs(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"- foo
|
||||
|
||||
- bar
|
||||
|
||||
|
||||
- baz", @"<document>
|
||||
<list type=""bullet"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- foo
|
||||
- bar
|
||||
- baz
|
||||
|
||||
|
||||
bim
|
||||
", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
<list type=""bullet"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>bim</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- a
|
||||
- b
|
||||
|
||||
- c", @"<document>
|
||||
<list type=""bullet"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>b</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>c</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"* a
|
||||
*
|
||||
|
||||
* c", @"<document>
|
||||
<list type=""bullet"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item />
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>c</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- a
|
||||
- b
|
||||
|
||||
c
|
||||
- d", @"<document>
|
||||
<list type=""bullet"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>b</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>c</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>d</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- a
|
||||
- b
|
||||
|
||||
[ref]: /url
|
||||
- d", @"<document>
|
||||
<list type=""bullet"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>b</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>d</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- a
|
||||
- ```
|
||||
b
|
||||
|
||||
|
||||
```
|
||||
- c", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<code_block>b
|
||||
|
||||
|
||||
</code_block>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>c</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- a
|
||||
- b
|
||||
|
||||
c
|
||||
- d", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
</paragraph>
|
||||
<list type=""bullet"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>b</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>c</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>d</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"* a
|
||||
> b
|
||||
>
|
||||
* c", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
</paragraph>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>b</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>c</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- a
|
||||
> b
|
||||
```
|
||||
c
|
||||
```
|
||||
- d
|
||||
", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
</paragraph>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>b</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
<code_block>c
|
||||
</code_block>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>d</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- a", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- a
|
||||
- b", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
</paragraph>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>b</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"1. ```
|
||||
foo
|
||||
```
|
||||
|
||||
bar", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""false"">
|
||||
<item>
|
||||
<code_block>foo
|
||||
</code_block>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"* foo
|
||||
* bar
|
||||
|
||||
baz", @"<document>
|
||||
<list type=""bullet"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- a
|
||||
- b
|
||||
- c
|
||||
|
||||
- d
|
||||
- e
|
||||
- f", @"<document>
|
||||
<list type=""bullet"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
</paragraph>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>b</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>c</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>d</text>
|
||||
</paragraph>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>e</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>f</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
public void Parse_TightAndLooseLists(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"- foo
|
||||
- bar
|
||||
|
||||
<!-- -->
|
||||
|
||||
- baz
|
||||
- bim", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
<html_block><!-- --></html_block>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bim</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- foo
|
||||
|
||||
notcode
|
||||
|
||||
- foo
|
||||
|
||||
<!-- -->
|
||||
|
||||
code", @"<document>
|
||||
<list type=""bullet"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>notcode</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
<html_block><!-- --></html_block>
|
||||
<code_block>code
|
||||
</code_block>
|
||||
</document>")]
|
||||
public void Parse_List_Separators(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"- a
|
||||
- b
|
||||
- c
|
||||
- d
|
||||
- e
|
||||
- f
|
||||
- g", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>b</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>c</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>d</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>e</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>f</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>g</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"1. a
|
||||
|
||||
2. b
|
||||
|
||||
3. c
|
||||
", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>b</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>c</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- a
|
||||
- b
|
||||
- c
|
||||
- d
|
||||
- e", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>b</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>c</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>d</text>
|
||||
<softbreak />
|
||||
<text>- e</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"1. a
|
||||
|
||||
2. b
|
||||
|
||||
3. c", @"<document>
|
||||
<list type=""ordered"" start=""1"" tight=""false"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>b</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
<code_block>3. c
|
||||
</code_block>
|
||||
</document>")]
|
||||
public void Parse_List_Identation(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_BasicNestedLists()
|
||||
{
|
||||
Assert.Equal(@"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>b</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>c</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>", ToXml(@"
|
||||
- a
|
||||
- b
|
||||
- c"));
|
||||
}
|
||||
}
|
||||
109
Radzen.Blazor.Tests/Markdown/ParagraphTests.cs
Normal file
109
Radzen.Blazor.Tests/Markdown/ParagraphTests.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class ParagraphTests
|
||||
{
|
||||
private static string ToXml(string markdown)
|
||||
{
|
||||
var document = MarkdownParser.Parse(markdown);
|
||||
|
||||
return XmlVisitor.ToXml(document);
|
||||
}
|
||||
[Fact]
|
||||
public void Parse_BasicParagraph()
|
||||
{
|
||||
Assert.Equal(@"<document>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</document>", ToXml(@"foo"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"aaa
|
||||
|
||||
bbb", @"<document>
|
||||
<paragraph>
|
||||
<text>aaa</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>bbb</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"aaa
|
||||
bbb
|
||||
|
||||
ccc
|
||||
ddd", @"<document>
|
||||
<paragraph>
|
||||
<text>aaa</text>
|
||||
<softbreak />
|
||||
<text>bbb</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>ccc</text>
|
||||
<softbreak />
|
||||
<text>ddd</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"aaa
|
||||
|
||||
|
||||
bbb
|
||||
", @"<document>
|
||||
<paragraph>
|
||||
<text>aaa</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>bbb</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@" aaa
|
||||
bbb", @"<document>
|
||||
<paragraph>
|
||||
<text>aaa</text>
|
||||
<softbreak />
|
||||
<text>bbb</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"aaa
|
||||
bbb
|
||||
ccc", @"<document>
|
||||
<paragraph>
|
||||
<text>aaa</text>
|
||||
<softbreak />
|
||||
<text>bbb</text>
|
||||
<softbreak />
|
||||
<text>ccc</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@" aaa
|
||||
bbb", @"<document>
|
||||
<paragraph>
|
||||
<text>aaa</text>
|
||||
<softbreak />
|
||||
<text>bbb</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@" aaa
|
||||
bbb", @"<document>
|
||||
<code_block>aaa
|
||||
</code_block>
|
||||
<paragraph>
|
||||
<text>bbb</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"aaa
|
||||
bbb ", @"<document>
|
||||
<paragraph>
|
||||
<text>aaa</text>
|
||||
<linebreak />
|
||||
<text>bbb</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_Paragraph(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
}
|
||||
41
Radzen.Blazor.Tests/Markdown/SoftLineBreakTests.cs
Normal file
41
Radzen.Blazor.Tests/Markdown/SoftLineBreakTests.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class SoftLineBreakTests
|
||||
{
|
||||
private static string ToXml(string markdown)
|
||||
{
|
||||
var document = MarkdownParser.Parse(markdown);
|
||||
|
||||
return XmlVisitor.ToXml(document);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"foo
|
||||
baz", @"<document>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<softbreak />
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_SoftLineBreak(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"foo
|
||||
baz", @"<document>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<softbreak />
|
||||
<text>baz</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_SoftLine_RemovesSpacesAtEndAndStartOfLine(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
}
|
||||
135
Radzen.Blazor.Tests/Markdown/StrongTests.cs
Normal file
135
Radzen.Blazor.Tests/Markdown/StrongTests.cs
Normal file
@@ -0,0 +1,135 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class StrongTests
|
||||
{
|
||||
private static string ToXml(string markdown)
|
||||
{
|
||||
var document = MarkdownParser.Parse(markdown);
|
||||
return XmlVisitor.ToXml(document);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("**foo bar**",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<strong>
|
||||
<text>foo bar</text>
|
||||
</strong>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("** foo bar**",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>**</text>
|
||||
<text> foo bar</text>
|
||||
<text>**</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("a**\"foo\"**",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
<text>**</text>
|
||||
<text>""foo""</text>
|
||||
<text>**</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("foo**bar**",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<strong>
|
||||
<text>bar</text>
|
||||
</strong>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("__foo bar__",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<strong>
|
||||
<text>foo bar</text>
|
||||
</strong>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("__ foo bar__",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>__</text>
|
||||
<text> foo bar</text>
|
||||
<text>__</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("__\nfoo bar__",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>__</text>
|
||||
<softbreak />
|
||||
<text>foo bar</text>
|
||||
<text>__</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("a__\"foo\"__",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>a</text>
|
||||
<text>__</text>
|
||||
<text>""foo""</text>
|
||||
<text>__</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("foo__bar__",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
<text>__</text>
|
||||
<text>bar</text>
|
||||
<text>__</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("5__6__78",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>5</text>
|
||||
<text>__</text>
|
||||
<text>6</text>
|
||||
<text>__</text>
|
||||
<text>78</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("пристаням__стремятся__",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>пристаням</text>
|
||||
<text>__</text>
|
||||
<text>стремятся</text>
|
||||
<text>__</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("__foo, __bar__, baz__",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<strong>
|
||||
<text>foo, </text>
|
||||
<strong>
|
||||
<text>bar</text>
|
||||
</strong>
|
||||
<text>, baz</text>
|
||||
</strong>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData("foo-__(bar)__",
|
||||
@"<document>
|
||||
<paragraph>
|
||||
<text>foo-</text>
|
||||
<strong>
|
||||
<text>(bar)</text>
|
||||
</strong>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_StrongEmphasisRules_AdheresToCommonMarkSpec(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
}
|
||||
414
Radzen.Blazor.Tests/Markdown/TableTests.cs
Normal file
414
Radzen.Blazor.Tests/Markdown/TableTests.cs
Normal file
@@ -0,0 +1,414 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class TableTests
|
||||
{
|
||||
private static string ToXml(string markdown)
|
||||
{
|
||||
var document = MarkdownParser.Parse(markdown);
|
||||
|
||||
return XmlVisitor.ToXml(document);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Parse_BasicTable()
|
||||
{
|
||||
Assert.Equal(
|
||||
@"<document>
|
||||
<table>
|
||||
<header>
|
||||
<cell>
|
||||
<text>foo</text>
|
||||
</cell>
|
||||
<cell>
|
||||
<text>bar</text>
|
||||
</cell>
|
||||
</header>
|
||||
<row>
|
||||
<cell>
|
||||
<text>baz</text>
|
||||
</cell>
|
||||
<cell>
|
||||
<text>bim</text>
|
||||
</cell>
|
||||
</row>
|
||||
</table>
|
||||
</document>",
|
||||
ToXml(@"
|
||||
foo|bar
|
||||
--|--
|
||||
baz|bim"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"| foo | bar |
|
||||
| --- | --- |
|
||||
| baz | bim |", @"<document>
|
||||
<table>
|
||||
<header>
|
||||
<cell>
|
||||
<text>foo</text>
|
||||
</cell>
|
||||
<cell>
|
||||
<text>bar</text>
|
||||
</cell>
|
||||
</header>
|
||||
<row>
|
||||
<cell>
|
||||
<text>baz</text>
|
||||
</cell>
|
||||
<cell>
|
||||
<text>bim</text>
|
||||
</cell>
|
||||
</row>
|
||||
</table>
|
||||
</document>")]
|
||||
[InlineData(@"| f\|oo |
|
||||
| ------ |
|
||||
| b `\|` az |
|
||||
| b **\|** im |", @"<document>
|
||||
<table>
|
||||
<header>
|
||||
<cell>
|
||||
<text>f|oo</text>
|
||||
</cell>
|
||||
</header>
|
||||
<row>
|
||||
<cell>
|
||||
<text>b </text>
|
||||
<code>|</code>
|
||||
<text> az</text>
|
||||
</cell>
|
||||
</row>
|
||||
<row>
|
||||
<cell>
|
||||
<text>b </text>
|
||||
<strong>
|
||||
<text>|</text>
|
||||
</strong>
|
||||
<text> im</text>
|
||||
</cell>
|
||||
</row>
|
||||
</table>
|
||||
</document>")]
|
||||
[InlineData(@"| abc | defghi |
|
||||
:-: | -----------:
|
||||
bar | baz", @"<document>
|
||||
<table>
|
||||
<header>
|
||||
<cell align=""center"">
|
||||
<text>abc</text>
|
||||
</cell>
|
||||
<cell align=""right"">
|
||||
<text>defghi</text>
|
||||
</cell>
|
||||
</header>
|
||||
<row>
|
||||
<cell align=""center"">
|
||||
<text>bar</text>
|
||||
</cell>
|
||||
<cell align=""right"">
|
||||
<text>baz</text>
|
||||
</cell>
|
||||
</row>
|
||||
</table>
|
||||
</document>")]
|
||||
|
||||
|
||||
[InlineData(@"| abc | def |
|
||||
| --- | --- |
|
||||
bar", @"<document>
|
||||
<table>
|
||||
<header>
|
||||
<cell>
|
||||
<text>abc</text>
|
||||
</cell>
|
||||
<cell>
|
||||
<text>def</text>
|
||||
</cell>
|
||||
</header>
|
||||
<row>
|
||||
<cell>
|
||||
<text>bar</text>
|
||||
</cell>
|
||||
<cell />
|
||||
</row>
|
||||
</table>
|
||||
</document>")]
|
||||
[InlineData(@"| abc | def |
|
||||
| --- | --- |
|
||||
c:\\foo", @"<document>
|
||||
<table>
|
||||
<header>
|
||||
<cell>
|
||||
<text>abc</text>
|
||||
</cell>
|
||||
<cell>
|
||||
<text>def</text>
|
||||
</cell>
|
||||
</header>
|
||||
<row>
|
||||
<cell>
|
||||
<text>c:</text>
|
||||
<text>\</text>
|
||||
<text>foo</text>
|
||||
</cell>
|
||||
<cell />
|
||||
</row>
|
||||
</table>
|
||||
</document>")]
|
||||
public void Parse_Table(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"| abc | def |
|
||||
| --- | --- |
|
||||
| bar | baz |
|
||||
|
||||
boo", @"<document>
|
||||
<table>
|
||||
<header>
|
||||
<cell>
|
||||
<text>abc</text>
|
||||
</cell>
|
||||
<cell>
|
||||
<text>def</text>
|
||||
</cell>
|
||||
</header>
|
||||
<row>
|
||||
<cell>
|
||||
<text>bar</text>
|
||||
</cell>
|
||||
<cell>
|
||||
<text>baz</text>
|
||||
</cell>
|
||||
</row>
|
||||
</table>
|
||||
<paragraph>
|
||||
<text>boo</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"| foo |
|
||||
| --- |
|
||||
# bar", @"<document>
|
||||
<table>
|
||||
<header>
|
||||
<cell>
|
||||
<text>foo</text>
|
||||
</cell>
|
||||
</header>
|
||||
</table>
|
||||
<heading level=""1"">
|
||||
<text>bar</text>
|
||||
</heading>
|
||||
</document>")]
|
||||
[InlineData(@"| foo |
|
||||
| --- |
|
||||
- bar", @"<document>
|
||||
<table>
|
||||
<header>
|
||||
<cell>
|
||||
<text>foo</text>
|
||||
</cell>
|
||||
</header>
|
||||
</table>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"| foo |
|
||||
| --- |
|
||||
1. bar", @"<document>
|
||||
<table>
|
||||
<header>
|
||||
<cell>
|
||||
<text>foo</text>
|
||||
</cell>
|
||||
</header>
|
||||
</table>
|
||||
<list type=""ordered"" start=""1"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"| abc | def |
|
||||
| --- | --- |
|
||||
| bar | baz |
|
||||
> bar", @"<document>
|
||||
<table>
|
||||
<header>
|
||||
<cell>
|
||||
<text>abc</text>
|
||||
</cell>
|
||||
<cell>
|
||||
<text>def</text>
|
||||
</cell>
|
||||
</header>
|
||||
<row>
|
||||
<cell>
|
||||
<text>bar</text>
|
||||
</cell>
|
||||
<cell>
|
||||
<text>baz</text>
|
||||
</cell>
|
||||
</row>
|
||||
</table>
|
||||
<block_quote>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</block_quote>
|
||||
</document>")]
|
||||
[InlineData(@"| abc | def |
|
||||
| --- | --- |
|
||||
| bar | baz |
|
||||
bar", @"<document>
|
||||
<table>
|
||||
<header>
|
||||
<cell>
|
||||
<text>abc</text>
|
||||
</cell>
|
||||
<cell>
|
||||
<text>def</text>
|
||||
</cell>
|
||||
</header>
|
||||
<row>
|
||||
<cell>
|
||||
<text>bar</text>
|
||||
</cell>
|
||||
<cell>
|
||||
<text>baz</text>
|
||||
</cell>
|
||||
</row>
|
||||
</table>
|
||||
<code_block>bar
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"| abc | def |
|
||||
| --- | --- |
|
||||
| bar | baz |
|
||||
```
|
||||
bar
|
||||
```", @"<document>
|
||||
<table>
|
||||
<header>
|
||||
<cell>
|
||||
<text>abc</text>
|
||||
</cell>
|
||||
<cell>
|
||||
<text>def</text>
|
||||
</cell>
|
||||
</header>
|
||||
<row>
|
||||
<cell>
|
||||
<text>bar</text>
|
||||
</cell>
|
||||
<cell>
|
||||
<text>baz</text>
|
||||
</cell>
|
||||
</row>
|
||||
</table>
|
||||
<code_block>bar
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"| abc | def |
|
||||
| --- | --- |
|
||||
| bar | baz |
|
||||
<div>
|
||||
</div>", @"<document>
|
||||
<table>
|
||||
<header>
|
||||
<cell>
|
||||
<text>abc</text>
|
||||
</cell>
|
||||
<cell>
|
||||
<text>def</text>
|
||||
</cell>
|
||||
</header>
|
||||
<row>
|
||||
<cell>
|
||||
<text>bar</text>
|
||||
</cell>
|
||||
<cell>
|
||||
<text>baz</text>
|
||||
</cell>
|
||||
</row>
|
||||
</table>
|
||||
<html_block><div>
|
||||
</div></html_block>
|
||||
</document>")]
|
||||
[InlineData(@"| abc | def |
|
||||
| --- | --- |
|
||||
| bar | baz |
|
||||
---", @"<document>
|
||||
<table>
|
||||
<header>
|
||||
<cell>
|
||||
<text>abc</text>
|
||||
</cell>
|
||||
<cell>
|
||||
<text>def</text>
|
||||
</cell>
|
||||
</header>
|
||||
<row>
|
||||
<cell>
|
||||
<text>bar</text>
|
||||
</cell>
|
||||
<cell>
|
||||
<text>baz</text>
|
||||
</cell>
|
||||
</row>
|
||||
</table>
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
public void Parse_Table_AnyBlockOrEmptyLineBreaksTable(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"| abc | def |
|
||||
| --- |", @"<document>
|
||||
<paragraph>
|
||||
<text>| abc | def |</text>
|
||||
<softbreak />
|
||||
<text>| --- |</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
public void Parse_Table_ChecksHeaderAndDelimiter(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"| abc |
|
||||
| --- |
|
||||
| bar | baz |", @"<document>
|
||||
<table>
|
||||
<header>
|
||||
<cell>
|
||||
<text>abc</text>
|
||||
</cell>
|
||||
</header>
|
||||
<row>
|
||||
<cell>
|
||||
<text>bar</text>
|
||||
</cell>
|
||||
</row>
|
||||
</table>
|
||||
</document>")]
|
||||
public void Parse_Table_IgnoresExtraCells(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
}
|
||||
162
Radzen.Blazor.Tests/Markdown/ThematicBreakTests.cs
Normal file
162
Radzen.Blazor.Tests/Markdown/ThematicBreakTests.cs
Normal file
@@ -0,0 +1,162 @@
|
||||
using Xunit;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class ThematicBreakTests
|
||||
{
|
||||
private static string ToXml(string markdown)
|
||||
{
|
||||
var document = MarkdownParser.Parse(markdown);
|
||||
|
||||
return XmlVisitor.ToXml(document);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"***
|
||||
---
|
||||
___", @"<document>
|
||||
<thematic_break />
|
||||
<thematic_break />
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
[InlineData(@"--
|
||||
**
|
||||
__", @"<document>
|
||||
<paragraph>
|
||||
<text>--</text>
|
||||
<softbreak />
|
||||
<text>**</text>
|
||||
<softbreak />
|
||||
<text>__</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@" ***
|
||||
***
|
||||
***", @"<document>
|
||||
<thematic_break />
|
||||
<thematic_break />
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
[InlineData(@" ***", @"<document>
|
||||
<code_block>***
|
||||
</code_block>
|
||||
</document>")]
|
||||
[InlineData(@"Foo
|
||||
***", @"<document>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
<softbreak />
|
||||
<text>***</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"_____________________________________", @"<document>
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
[InlineData(@"- - -", @"<document>
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
[InlineData(@" ** * ** * ** * **", @"<document>
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
[InlineData(@"- - - -", @"<document>
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
[InlineData(@"- - - - ", @"<document>
|
||||
<thematic_break />
|
||||
</document>")]
|
||||
[InlineData(@"_ _ _ _ a
|
||||
|
||||
a------
|
||||
|
||||
---a---", @"<document>
|
||||
<paragraph>
|
||||
<text>_</text>
|
||||
<text> </text>
|
||||
<text>_</text>
|
||||
<text> </text>
|
||||
<text>_</text>
|
||||
<text> </text>
|
||||
<text>_</text>
|
||||
<text> a</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>a------</text>
|
||||
</paragraph>
|
||||
<paragraph>
|
||||
<text>---a---</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@" *-*", @"<document>
|
||||
<paragraph>
|
||||
<emph>
|
||||
<text>-</text>
|
||||
</emph>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"- foo
|
||||
***
|
||||
- bar", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
<thematic_break />
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"Foo
|
||||
***
|
||||
bar", @"<document>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
</paragraph>
|
||||
<thematic_break />
|
||||
<paragraph>
|
||||
<text>bar</text>
|
||||
</paragraph>
|
||||
</document>")]
|
||||
[InlineData(@"* Foo
|
||||
* * *
|
||||
* Bar", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
<thematic_break />
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>Bar</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
[InlineData(@"- Foo
|
||||
- * * *", @"<document>
|
||||
<list type=""bullet"" tight=""true"">
|
||||
<item>
|
||||
<paragraph>
|
||||
<text>Foo</text>
|
||||
</paragraph>
|
||||
</item>
|
||||
<item>
|
||||
<thematic_break />
|
||||
</item>
|
||||
</list>
|
||||
</document>")]
|
||||
public void Parse_ThematicBreak(string markdown, string expected)
|
||||
{
|
||||
Assert.Equal(expected, ToXml(markdown));
|
||||
}
|
||||
}
|
||||
210
Radzen.Blazor.Tests/Markdown/XmlVisitor.cs
Normal file
210
Radzen.Blazor.Tests/Markdown/XmlVisitor.cs
Normal file
@@ -0,0 +1,210 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
|
||||
namespace Radzen.Blazor.Markdown.Tests;
|
||||
|
||||
public class XmlVisitor : NodeVisitorBase, IDisposable
|
||||
{
|
||||
private readonly XmlWriter writer;
|
||||
|
||||
private XmlVisitor(StringBuilder xml)
|
||||
{
|
||||
writer = XmlWriter.Create(xml, new XmlWriterSettings { OmitXmlDeclaration = true, Indent = true, IndentChars = " ", });
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
writer.Dispose();
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
writer.Close();
|
||||
}
|
||||
|
||||
public static string ToXml(Document document)
|
||||
{
|
||||
var xml = new StringBuilder();
|
||||
|
||||
using var visitor = new XmlVisitor(xml);
|
||||
|
||||
document.Accept(visitor);
|
||||
|
||||
visitor.Close();
|
||||
|
||||
return xml.ToString()!;
|
||||
}
|
||||
|
||||
public override void VisitBlockQuote(BlockQuote blockQuote)
|
||||
{
|
||||
writer.WriteStartElement("block_quote");
|
||||
base.VisitBlockQuote(blockQuote);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
public override void VisitDocument(Document document)
|
||||
{
|
||||
writer.WriteStartDocument();
|
||||
writer.WriteStartElement("document");
|
||||
base.VisitDocument(document);
|
||||
writer.WriteEndElement();
|
||||
writer.WriteEndDocument();
|
||||
}
|
||||
|
||||
public override void VisitHeading(Heading heading)
|
||||
{
|
||||
writer.WriteStartElement($"heading");
|
||||
writer.WriteAttributeString("level", heading.Level.ToString());
|
||||
base.VisitHeading(heading);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
public override void VisitListItem(ListItem listItem)
|
||||
{
|
||||
writer.WriteStartElement("item");
|
||||
base.VisitListItem(listItem);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
public override void VisitParagraph(Paragraph paragraph)
|
||||
{
|
||||
writer.WriteStartElement("paragraph");
|
||||
base.VisitParagraph(paragraph);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
public override void VisitUnorderedList(UnorderedList unorderedList)
|
||||
{
|
||||
writer.WriteStartElement("list");
|
||||
writer.WriteAttributeString("type", "bullet");
|
||||
writer.WriteAttributeString("tight", unorderedList.Tight.ToString().ToLowerInvariant());
|
||||
base.VisitUnorderedList(unorderedList);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
public override void VisitText(Text text)
|
||||
{
|
||||
writer.WriteElementString("text", text.Value);
|
||||
}
|
||||
|
||||
public override void VisitOrderedList(OrderedList orderedList)
|
||||
{
|
||||
writer.WriteStartElement("list");
|
||||
writer.WriteAttributeString("type", "ordered");
|
||||
writer.WriteAttributeString("start", orderedList.Start.ToString());
|
||||
writer.WriteAttributeString("tight", orderedList.Tight.ToString().ToLowerInvariant());
|
||||
base.VisitOrderedList(orderedList);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
public override void VisitLink(Link link)
|
||||
{
|
||||
writer.WriteStartElement("link");
|
||||
writer.WriteAttributeString("destination", link.Destination);
|
||||
writer.WriteAttributeString("title", link.Title);
|
||||
base.VisitLink(link);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
public override void VisitImage(Image image)
|
||||
{
|
||||
writer.WriteStartElement("image");
|
||||
writer.WriteAttributeString("destination", image.Destination);
|
||||
writer.WriteAttributeString("title", image.Title);
|
||||
base.VisitImage(image);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
public override void VisitEmphasis(Emphasis emphasis)
|
||||
{
|
||||
writer.WriteStartElement("emph");
|
||||
base.VisitEmphasis(emphasis);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
public override void VisitStrong(Strong strong)
|
||||
{
|
||||
writer.WriteStartElement("strong");
|
||||
base.VisitStrong(strong);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
public override void VisitCode(Code code)
|
||||
{
|
||||
writer.WriteElementString("code", code.Value);
|
||||
}
|
||||
|
||||
public override void VisitHtmlInline(HtmlInline html)
|
||||
{
|
||||
writer.WriteElementString("html_inline", html.Value);
|
||||
}
|
||||
|
||||
public override void VisitLineBreak(LineBreak lineBreak)
|
||||
{
|
||||
writer.WriteElementString("linebreak", string.Empty);
|
||||
}
|
||||
|
||||
public override void VisitSoftLineBreak(SoftLineBreak softLineBreak)
|
||||
{
|
||||
writer.WriteElementString("softbreak", string.Empty);
|
||||
}
|
||||
|
||||
public override void VisitThematicBreak(ThematicBreak thematicBreak)
|
||||
{
|
||||
writer.WriteElementString("thematic_break", string.Empty);
|
||||
}
|
||||
|
||||
public override void VisitIndentedCodeBlock(IndentedCodeBlock codeBlock)
|
||||
{
|
||||
writer.WriteElementString("code_block", codeBlock.Value);
|
||||
}
|
||||
|
||||
public override void VisitFencedCodeBlock(FencedCodeBlock fencedCodeBlock)
|
||||
{
|
||||
writer.WriteStartElement("code_block");
|
||||
if (!string.IsNullOrEmpty(fencedCodeBlock.Info))
|
||||
{
|
||||
writer.WriteAttributeString("info", fencedCodeBlock.Info);
|
||||
}
|
||||
writer.WriteString(fencedCodeBlock.Value);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
public override void VisitHtmlBlock(HtmlBlock htmlBlock)
|
||||
{
|
||||
writer.WriteElementString("html_block", htmlBlock.Value);
|
||||
}
|
||||
|
||||
public override void VisitTable(Table table)
|
||||
{
|
||||
writer.WriteStartElement("table");
|
||||
base.VisitTable(table);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
public override void VisitTableHeaderRow(TableHeaderRow header)
|
||||
{
|
||||
writer.WriteStartElement("header");
|
||||
base.VisitTableHeaderRow(header);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
public override void VisitTableRow(TableRow row)
|
||||
{
|
||||
writer.WriteStartElement("row");
|
||||
base.VisitTableRow(row);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
public override void VisitTableCell(TableCell cell)
|
||||
{
|
||||
writer.WriteStartElement("cell");
|
||||
if (cell.Alignment != TableCellAlignment.None)
|
||||
{
|
||||
writer.WriteAttributeString("align", cell.Alignment.ToString().ToLowerInvariant());
|
||||
}
|
||||
base.VisitTableCell(cell);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
}
|
||||
@@ -198,14 +198,10 @@ namespace Radzen.Blazor.Tests
|
||||
});
|
||||
|
||||
Assert.Contains("SummaryContent", component.Markup);
|
||||
Assert.Equal(
|
||||
"display: block",
|
||||
component.Find(".rz-panel-content-summary").ParentElement.Attributes.First(attr => attr.Name == "style").Value
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Panel_DontRenders_SummaryWhenOpen()
|
||||
public void Panel_DoesNotRender_SummaryWhenOpen()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
var component = ctx.RenderComponent<RadzenPanel>();
|
||||
@@ -225,8 +221,8 @@ namespace Radzen.Blazor.Tests
|
||||
|
||||
Assert.Contains("SummaryContent", component.Markup);
|
||||
Assert.Equal(
|
||||
"display: none",
|
||||
component.Find(".rz-panel-content-summary").ParentElement.Attributes.First(attr => attr.Name == "style").Value
|
||||
"true",
|
||||
component.Find(".rz-panel-content-summary").ParentElement.ParentElement.Attributes.First(attr => attr.Name == "aria-hidden").Value
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
1187
Radzen.Blazor.Tests/TimeSpanPickerTests.cs
Normal file
1187
Radzen.Blazor.Tests/TimeSpanPickerTests.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -371,6 +371,40 @@ namespace Radzen
|
||||
December = 11,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the time unit of <see cref="TimeSpan"/>.
|
||||
/// </summary>
|
||||
public enum TimeSpanUnit
|
||||
{
|
||||
/// <summary>
|
||||
/// Day.
|
||||
/// </summary>
|
||||
Day = 0,
|
||||
/// <summary>
|
||||
/// Hour.
|
||||
/// </summary>
|
||||
Hour = 1,
|
||||
/// <summary>
|
||||
/// Minute.
|
||||
/// </summary>
|
||||
Minute = 2,
|
||||
/// <summary>
|
||||
/// Second.
|
||||
/// </summary>
|
||||
Second = 3,
|
||||
/// <summary>
|
||||
/// Millisecond.
|
||||
/// </summary>
|
||||
Millisecond = 4
|
||||
#if NET7_0_OR_GREATER
|
||||
,
|
||||
/// <summary>
|
||||
/// Microsecond.
|
||||
/// </summary>
|
||||
Microsecond = 5
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Html editor mode (Rendered or Raw). Also used for toolbar buttons to enable/disable according to mode.
|
||||
/// </summary>
|
||||
|
||||
@@ -7,6 +7,25 @@ using Microsoft.JSInterop;
|
||||
|
||||
namespace Radzen
|
||||
{
|
||||
/// <summary>
|
||||
/// Specifies the SameSite attribute for the cookie.
|
||||
/// </summary>
|
||||
public enum CookieSameSiteMode
|
||||
{
|
||||
/// <summary>
|
||||
/// No SameSite attribute.
|
||||
/// </summary>
|
||||
None,
|
||||
/// <summary>
|
||||
/// Lax SameSite attribute.
|
||||
/// </summary>
|
||||
Lax,
|
||||
/// <summary>
|
||||
/// Strict SameSite attribute.
|
||||
/// </summary>
|
||||
Strict
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for the <see cref="CookieThemeService" />.
|
||||
/// </summary>
|
||||
@@ -21,6 +40,16 @@ namespace Radzen
|
||||
/// Gets or sets the cookie duration.
|
||||
/// </summary>
|
||||
public TimeSpan Duration { get; set; } = TimeSpan.FromDays(365);
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to use secure cookies.
|
||||
/// </summary>
|
||||
public bool IsSecure { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the SameSite attribute for the cookie.
|
||||
/// </summary>
|
||||
public CookieSameSiteMode? SameSite { get; set; } = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -75,8 +104,19 @@ namespace Radzen
|
||||
private void OnThemeChanged()
|
||||
{
|
||||
var expiration = DateTime.Now.Add(options.Duration);
|
||||
var cookie = $"{options.Name}={themeService.Theme}; expires={expiration:R}; path=/";
|
||||
|
||||
_ = jsRuntime.InvokeVoidAsync("eval", $"document.cookie = \"{options.Name}={themeService.Theme}; expires={expiration:R}; path=/\"");
|
||||
if (options.SameSite.HasValue)
|
||||
{
|
||||
cookie += $"; SameSite={options.SameSite}";
|
||||
}
|
||||
|
||||
if (options.IsSecure)
|
||||
{
|
||||
cookie += "; Secure";
|
||||
}
|
||||
|
||||
_ = jsRuntime.InvokeVoidAsync("eval", $"document.cookie = \"{cookie}\"");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -629,7 +629,7 @@ namespace Radzen
|
||||
/// <param name="shouldSelectOnChange">Should select item on item change with keyboard.</param>
|
||||
protected virtual async System.Threading.Tasks.Task HandleKeyPress(Microsoft.AspNetCore.Components.Web.KeyboardEventArgs args, bool isFilter = false, bool? shouldSelectOnChange = null)
|
||||
{
|
||||
if (Disabled)
|
||||
if (Disabled || Data == null)
|
||||
return;
|
||||
|
||||
List<object> items = Enumerable.Empty<object>().ToList();
|
||||
@@ -679,7 +679,7 @@ namespace Radzen
|
||||
//
|
||||
}
|
||||
}
|
||||
else if (key == "Enter" || key == "NumpadEnter")
|
||||
else if (key == "Enter" || key == "NumpadEnter" || key == "Space")
|
||||
{
|
||||
preventKeydown = true;
|
||||
|
||||
@@ -699,11 +699,14 @@ namespace Radzen
|
||||
|
||||
if (!popupOpened)
|
||||
{
|
||||
await OpenPopup(key, isFilter);
|
||||
if(key != "Space")
|
||||
{
|
||||
await OpenPopup(key, isFilter);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Multiple)
|
||||
if (!Multiple && !isFilter)
|
||||
{
|
||||
await ClosePopup(key);
|
||||
}
|
||||
@@ -717,6 +720,8 @@ namespace Radzen
|
||||
}
|
||||
else if (key == "Escape" || key == "Tab")
|
||||
{
|
||||
preventKeydown = false;
|
||||
|
||||
await ClosePopup(key);
|
||||
}
|
||||
else if (key == "Delete" && AllowClear)
|
||||
@@ -740,7 +745,7 @@ namespace Radzen
|
||||
|
||||
Debounce(DebounceFilter, FilterDelay);
|
||||
}
|
||||
else
|
||||
else if(!args.CtrlKey && !args.AltKey)
|
||||
{
|
||||
var filteredItems = (!string.IsNullOrEmpty(TextProperty) ?
|
||||
Query.Where(TextProperty, args.Key, StringFilterOperator.StartsWith, FilterCaseSensitivity.CaseInsensitive) :
|
||||
@@ -757,7 +762,7 @@ namespace Radzen
|
||||
itemIndex = itemIndex + 1 >= filteredItems.Count() ? 0 : itemIndex + 1;
|
||||
var itemToSelect = filteredItems.ElementAtOrDefault(itemIndex);
|
||||
|
||||
if (itemToSelect != null)
|
||||
if (itemToSelect is not null)
|
||||
{
|
||||
if (!Multiple)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Radzen;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace System.Linq.Dynamic.Core
|
||||
{
|
||||
@@ -23,10 +24,14 @@ namespace System.Linq.Dynamic.Core
|
||||
{
|
||||
if (parameters != null && !string.IsNullOrEmpty(predicate))
|
||||
{
|
||||
for (var i = 0; i < parameters.Length; i++)
|
||||
predicate = Regex.Replace(predicate, @"@(\d+)", match =>
|
||||
{
|
||||
object param = parameters[i];
|
||||
string value = param switch
|
||||
int index = int.Parse(match.Groups[1].Value);
|
||||
if (index >= parameters.Length)
|
||||
throw new InvalidOperationException($"No parameter provided for {match.Value}");
|
||||
|
||||
object param = parameters[index];
|
||||
return param switch
|
||||
{
|
||||
string s when s == string.Empty => @"""""",
|
||||
null => "null",
|
||||
@@ -35,13 +40,11 @@ namespace System.Linq.Dynamic.Core
|
||||
Guid g => $"Guid.Parse(\"{g}\")",
|
||||
DateTime dt => $"DateTime.Parse(\"{dt:yyyy-MM-ddTHH:mm:ss.fffZ}\")",
|
||||
DateTimeOffset dto => $"DateTime.Parse(\"{dto.UtcDateTime:yyyy-MM-ddTHH:mm:ss.fffZ}\")",
|
||||
DateOnly d => $"DateOnly.Parse(\"{d:yyy-MM-dd}\")",
|
||||
DateOnly d => $"DateOnly.Parse(\"{d:yyyy-MM-dd}\")",
|
||||
TimeOnly t => $"TimeOnly.Parse(\"{t:HH:mm:ss}\")",
|
||||
_ => param.ToString()
|
||||
};
|
||||
|
||||
predicate = predicate.Replace($"@{i}", $"{value}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
predicate = (predicate == "true" ? "" : predicate)
|
||||
|
||||
56
Radzen.Blazor/DynamicTypeFactory.cs
Normal file
56
Radzen.Blazor/DynamicTypeFactory.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Reflection.Emit;
|
||||
|
||||
static class DynamicTypeFactory
|
||||
{
|
||||
public static Type CreateType(string typeName, string[] propertyNames, Type[] propertyTypes)
|
||||
{
|
||||
if (propertyNames.Length != propertyTypes.Length)
|
||||
{
|
||||
throw new ArgumentException("Property names and types count mismatch.");
|
||||
}
|
||||
|
||||
var assemblyName = new AssemblyName("DynamicTypesAssembly");
|
||||
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
|
||||
var moduleBuilder = assemblyBuilder.DefineDynamicModule("DynamicTypesModule");
|
||||
|
||||
var typeBuilder = moduleBuilder.DefineType(typeName, TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.Sealed);
|
||||
|
||||
for (int i = 0; i < propertyNames.Length; i++)
|
||||
{
|
||||
var fieldBuilder = typeBuilder.DefineField("_" + propertyNames[i], propertyTypes[i], FieldAttributes.Private);
|
||||
var propertyBuilder = typeBuilder.DefineProperty(propertyNames[i], PropertyAttributes.None, propertyTypes[i], null);
|
||||
|
||||
var getterMethod = typeBuilder.DefineMethod(
|
||||
"get_" + propertyNames[i],
|
||||
MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig,
|
||||
propertyTypes[i],
|
||||
Type.EmptyTypes);
|
||||
|
||||
var getterIl = getterMethod.GetILGenerator();
|
||||
getterIl.Emit(OpCodes.Ldarg_0);
|
||||
getterIl.Emit(OpCodes.Ldfld, fieldBuilder);
|
||||
getterIl.Emit(OpCodes.Ret);
|
||||
|
||||
propertyBuilder.SetGetMethod(getterMethod);
|
||||
|
||||
var setterMethod = typeBuilder.DefineMethod(
|
||||
"set_" + propertyNames[i],
|
||||
MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig,
|
||||
null,
|
||||
[propertyTypes[i]]);
|
||||
|
||||
var setterIl = setterMethod.GetILGenerator();
|
||||
setterIl.Emit(OpCodes.Ldarg_0);
|
||||
setterIl.Emit(OpCodes.Ldarg_1);
|
||||
setterIl.Emit(OpCodes.Stfld, fieldBuilder);
|
||||
setterIl.Emit(OpCodes.Ret);
|
||||
|
||||
propertyBuilder.SetSetMethod(setterMethod);
|
||||
}
|
||||
|
||||
var dynamicType = typeBuilder.CreateType();
|
||||
return dynamicType;
|
||||
}
|
||||
}
|
||||
873
Radzen.Blazor/ExpressionLexer.cs
Normal file
873
Radzen.Blazor/ExpressionLexer.cs
Normal file
@@ -0,0 +1,873 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text;
|
||||
|
||||
namespace Radzen;
|
||||
|
||||
class Token
|
||||
{
|
||||
public TokenType Type { get; set; }
|
||||
public string Value { get; set; } = string.Empty;
|
||||
public ValueKind ValueKind { get; set; } = ValueKind.None;
|
||||
public int IntValue { get; internal set; }
|
||||
public uint UintValue { get; internal set; }
|
||||
public long LongValue { get; internal set; }
|
||||
public ulong UlongValue { get; internal set; }
|
||||
public decimal DecimalValue { get; internal set; }
|
||||
public float FloatValue { get; internal set; }
|
||||
public double DoubleValue { get; internal set; }
|
||||
|
||||
public Token(TokenType type)
|
||||
{
|
||||
Type = type;
|
||||
}
|
||||
|
||||
public Token(TokenType type, string value)
|
||||
{
|
||||
Type = type;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public ConstantExpression ToConstantExpression()
|
||||
{
|
||||
return ValueKind switch
|
||||
{
|
||||
ValueKind.Null => Expression.Constant(null),
|
||||
ValueKind.String => Expression.Constant(Value),
|
||||
ValueKind.Character => Expression.Constant(Value[0]),
|
||||
ValueKind.Int => Expression.Constant(IntValue),
|
||||
ValueKind.UInt => Expression.Constant(UintValue),
|
||||
ValueKind.Long => Expression.Constant(LongValue),
|
||||
ValueKind.ULong => Expression.Constant(UlongValue),
|
||||
ValueKind.Float => Expression.Constant(FloatValue),
|
||||
ValueKind.Double => Expression.Constant(DoubleValue),
|
||||
ValueKind.Decimal => Expression.Constant(DecimalValue),
|
||||
ValueKind.True => Expression.Constant(true),
|
||||
ValueKind.False => Expression.Constant(false),
|
||||
_ => throw new InvalidOperationException($"Unsupported value kind: {ValueKind}")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
enum ValueKind
|
||||
{
|
||||
None,
|
||||
String,
|
||||
Int,
|
||||
Float,
|
||||
Double,
|
||||
Decimal,
|
||||
Character,
|
||||
Null,
|
||||
True,
|
||||
False,
|
||||
Long,
|
||||
UInt,
|
||||
ULong,
|
||||
}
|
||||
|
||||
|
||||
enum TokenType
|
||||
{
|
||||
None,
|
||||
Identifier,
|
||||
EqualsEquals,
|
||||
NotEquals,
|
||||
EqualsGreaterThan,
|
||||
StringLiteral,
|
||||
NumericLiteral,
|
||||
Dot,
|
||||
OpenParen,
|
||||
CloseParen,
|
||||
Comma,
|
||||
AmpersandAmpersand,
|
||||
Ampersand,
|
||||
BarBar,
|
||||
Bar,
|
||||
GreaterThan,
|
||||
LessThan,
|
||||
LessThanOrEqual,
|
||||
GreaterThanOrEqual,
|
||||
Plus,
|
||||
Minus,
|
||||
Star,
|
||||
Slash,
|
||||
CharacterLiteral,
|
||||
QuestionMark,
|
||||
QuestionMarkQuestionMark,
|
||||
Colon,
|
||||
QuestionDot,
|
||||
New,
|
||||
NullLiteral,
|
||||
TrueLiteral,
|
||||
FalseLiteral,
|
||||
OpenBracket,
|
||||
CloseBracket,
|
||||
OpenBrace,
|
||||
CloseBrace,
|
||||
ExclamationMark,
|
||||
Equals,
|
||||
Caret,
|
||||
GreaterThanGreaterThan,
|
||||
LessThanLessThan,
|
||||
}
|
||||
|
||||
static class TokenTypeExtensions
|
||||
{
|
||||
public static ExpressionType ToExpressionType(this TokenType tokenType)
|
||||
{
|
||||
return tokenType switch
|
||||
{
|
||||
TokenType.EqualsEquals => ExpressionType.Equal,
|
||||
TokenType.NotEquals => ExpressionType.NotEqual,
|
||||
TokenType.EqualsGreaterThan => ExpressionType.GreaterThanOrEqual,
|
||||
TokenType.AmpersandAmpersand => ExpressionType.AndAlso,
|
||||
TokenType.Ampersand => ExpressionType.And,
|
||||
TokenType.BarBar => ExpressionType.OrElse,
|
||||
TokenType.Bar => ExpressionType.Or,
|
||||
TokenType.GreaterThan => ExpressionType.GreaterThan,
|
||||
TokenType.LessThan => ExpressionType.LessThan,
|
||||
TokenType.LessThanOrEqual => ExpressionType.LessThanOrEqual,
|
||||
TokenType.GreaterThanOrEqual => ExpressionType.GreaterThanOrEqual,
|
||||
TokenType.Plus => ExpressionType.Add,
|
||||
TokenType.Minus => ExpressionType.Subtract,
|
||||
TokenType.Star => ExpressionType.Multiply,
|
||||
TokenType.Slash => ExpressionType.Divide,
|
||||
TokenType.Caret => ExpressionType.ExclusiveOr,
|
||||
TokenType.GreaterThanGreaterThan => ExpressionType.RightShift,
|
||||
TokenType.LessThanLessThan => ExpressionType.LeftShift,
|
||||
_ => throw new InvalidOperationException($"Unsupported token type: {tokenType}")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ExpressionLexer(string expression)
|
||||
{
|
||||
private int position;
|
||||
|
||||
private char Peek(int offset = 0)
|
||||
{
|
||||
if (position + offset >= expression.Length)
|
||||
{
|
||||
return '\0';
|
||||
}
|
||||
|
||||
return expression[position + offset];
|
||||
}
|
||||
|
||||
private void Advance(int count)
|
||||
{
|
||||
position += count;
|
||||
}
|
||||
|
||||
bool TryAdvance(char expected)
|
||||
{
|
||||
if (Peek(1) == expected)
|
||||
{
|
||||
Advance(1);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ScanTrivia()
|
||||
{
|
||||
while (char.IsWhiteSpace(Peek()))
|
||||
{
|
||||
Advance(1);
|
||||
}
|
||||
}
|
||||
|
||||
public static List<Token> Scan(string expression)
|
||||
{
|
||||
var lexer = new ExpressionLexer(expression);
|
||||
|
||||
return [.. lexer.Scan()];
|
||||
}
|
||||
|
||||
public IEnumerable<Token> Scan()
|
||||
{
|
||||
while (position < expression.Length)
|
||||
{
|
||||
ScanTrivia();
|
||||
|
||||
var token = ScanToken();
|
||||
|
||||
if (token.Type == TokenType.None)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return token;
|
||||
}
|
||||
|
||||
yield return new Token(TokenType.None, string.Empty);
|
||||
}
|
||||
|
||||
private Token ScanToken()
|
||||
{
|
||||
var ch = Peek();
|
||||
|
||||
switch (ch)
|
||||
{
|
||||
case '"':
|
||||
return ScanStringLiteral();
|
||||
case '\'':
|
||||
return ScanCharacterLiteral();
|
||||
case '=':
|
||||
if (TryAdvance('='))
|
||||
{
|
||||
Advance(1);
|
||||
return new Token(TokenType.EqualsEquals);
|
||||
}
|
||||
|
||||
if (TryAdvance('>'))
|
||||
{
|
||||
Advance(1);
|
||||
return new Token(TokenType.EqualsGreaterThan);
|
||||
}
|
||||
|
||||
Advance(1);
|
||||
return new Token(TokenType.Equals);
|
||||
case '!':
|
||||
if (TryAdvance('='))
|
||||
{
|
||||
Advance(1);
|
||||
return new Token(TokenType.NotEquals);
|
||||
}
|
||||
Advance(1);
|
||||
return new Token(TokenType.ExclamationMark);
|
||||
case '>':
|
||||
if (TryAdvance('='))
|
||||
{
|
||||
Advance(1);
|
||||
return new Token(TokenType.GreaterThanOrEqual);
|
||||
}
|
||||
if (TryAdvance('>'))
|
||||
{
|
||||
Advance(1);
|
||||
return new Token(TokenType.GreaterThanGreaterThan);
|
||||
}
|
||||
Advance(1);
|
||||
return new Token(TokenType.GreaterThan);
|
||||
case '<':
|
||||
if (TryAdvance('<'))
|
||||
{
|
||||
Advance(1);
|
||||
return new Token(TokenType.LessThanLessThan);
|
||||
}
|
||||
if (TryAdvance('='))
|
||||
{
|
||||
Advance(1);
|
||||
return new Token(TokenType.LessThanOrEqual);
|
||||
}
|
||||
Advance(1);
|
||||
return new Token(TokenType.LessThan);
|
||||
case '+':
|
||||
Advance(1);
|
||||
return new Token(TokenType.Plus);
|
||||
case '-':
|
||||
Advance(1);
|
||||
return new Token(TokenType.Minus);
|
||||
case '*':
|
||||
Advance(1);
|
||||
return new Token(TokenType.Star);
|
||||
case '/':
|
||||
Advance(1);
|
||||
return new Token(TokenType.Slash);
|
||||
case '.':
|
||||
Advance(1);
|
||||
return new Token(TokenType.Dot);
|
||||
case '(':
|
||||
Advance(1);
|
||||
return new Token(TokenType.OpenParen);
|
||||
case ')':
|
||||
Advance(1);
|
||||
return new Token(TokenType.CloseParen);
|
||||
case '[':
|
||||
Advance(1);
|
||||
return new Token(TokenType.OpenBracket);
|
||||
case ']':
|
||||
Advance(1);
|
||||
return new Token(TokenType.CloseBracket);
|
||||
case '{':
|
||||
Advance(1);
|
||||
return new Token(TokenType.OpenBrace);
|
||||
case '}':
|
||||
Advance(1);
|
||||
return new Token(TokenType.CloseBrace);
|
||||
case ',':
|
||||
Advance(1);
|
||||
return new Token(TokenType.Comma);
|
||||
case '&':
|
||||
if (TryAdvance('&'))
|
||||
{
|
||||
Advance(1);
|
||||
return new Token(TokenType.AmpersandAmpersand);
|
||||
}
|
||||
Advance(1);
|
||||
return new Token(TokenType.Ampersand);
|
||||
case '|':
|
||||
if (TryAdvance('|'))
|
||||
{
|
||||
Advance(1);
|
||||
return new Token(TokenType.BarBar);
|
||||
}
|
||||
Advance(1);
|
||||
return new Token(TokenType.Bar);
|
||||
case '?':
|
||||
if (TryAdvance('.'))
|
||||
{
|
||||
Advance(1);
|
||||
return new Token(TokenType.QuestionDot);
|
||||
}
|
||||
|
||||
if (TryAdvance('?'))
|
||||
{
|
||||
Advance(1);
|
||||
return new Token(TokenType.QuestionMarkQuestionMark);
|
||||
}
|
||||
|
||||
Advance(1);
|
||||
return new Token(TokenType.QuestionMark);
|
||||
case ':':
|
||||
Advance(1);
|
||||
return new Token(TokenType.Colon);
|
||||
case '^':
|
||||
Advance(1);
|
||||
return new Token(TokenType.Caret);
|
||||
case >= '0' and <= '9':
|
||||
return ScanNumericLiteral();
|
||||
case '_':
|
||||
case (>= 'a' and <= 'z') or (>= 'A' and <= 'Z'):
|
||||
var token = ScanIdentifier();
|
||||
|
||||
return token.Value switch
|
||||
{
|
||||
"null" => new Token(TokenType.NullLiteral) { ValueKind = ValueKind.Null },
|
||||
"true" => new Token(TokenType.TrueLiteral) { ValueKind = ValueKind.True },
|
||||
"false" => new Token(TokenType.FalseLiteral) { ValueKind = ValueKind.False },
|
||||
"new" => new Token(TokenType.New),
|
||||
_ => token
|
||||
};
|
||||
}
|
||||
|
||||
return new Token(TokenType.None, string.Empty);
|
||||
}
|
||||
|
||||
private char ScanEscapeSequence()
|
||||
{
|
||||
var ch = Peek();
|
||||
|
||||
Advance(1);
|
||||
|
||||
switch (ch)
|
||||
{
|
||||
case '\'':
|
||||
case '"':
|
||||
case '\\':
|
||||
break;
|
||||
case '0':
|
||||
ch = '\u0000';
|
||||
break;
|
||||
case 'a':
|
||||
ch = '\u0007';
|
||||
break;
|
||||
case 'b':
|
||||
ch = '\u0008';
|
||||
break;
|
||||
case 'f':
|
||||
ch = '\u000c';
|
||||
break;
|
||||
case 'n':
|
||||
ch = '\u000a';
|
||||
break;
|
||||
case 'r':
|
||||
ch = '\u000d';
|
||||
break;
|
||||
case 't':
|
||||
ch = '\u0009';
|
||||
break;
|
||||
case 'v':
|
||||
ch = '\u000b';
|
||||
break;
|
||||
case 'u':
|
||||
case 'U':
|
||||
case 'x':
|
||||
ch = ScanUnicodeEscapeSequence(ch);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"Invalid escape sequence '\\{ch}' at position {position}.");
|
||||
}
|
||||
|
||||
return ch;
|
||||
}
|
||||
|
||||
private char ScanUnicodeEscapeSequence(char ch)
|
||||
{
|
||||
var value = 0;
|
||||
|
||||
var count = ch == 'U' ? 8 : 4;
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var digit = Peek();
|
||||
|
||||
int digitValue;
|
||||
|
||||
if (digit >= '0' && digit <= '9')
|
||||
{
|
||||
digitValue = digit - '0';
|
||||
}
|
||||
else if (digit >= 'a' && digit <= 'f')
|
||||
{
|
||||
digitValue = digit - 'a' + 10;
|
||||
}
|
||||
else if (digit >= 'A' && digit <= 'F')
|
||||
{
|
||||
digitValue = digit - 'A' + 10;
|
||||
}
|
||||
else if (ch != 'x')
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid unicode escape sequence at position {position}.");
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
value = (value << 4) + digitValue;
|
||||
|
||||
Advance(1);
|
||||
}
|
||||
|
||||
return (char)value;
|
||||
}
|
||||
|
||||
private Token ScanCharacterLiteral()
|
||||
{
|
||||
Advance(1);
|
||||
|
||||
var buffer = new StringBuilder();
|
||||
|
||||
while (true)
|
||||
{
|
||||
var ch = Peek();
|
||||
|
||||
switch (ch)
|
||||
{
|
||||
case '\0':
|
||||
throw new InvalidOperationException($"Unexpected end of character literal at position {position}.");
|
||||
case '\\':
|
||||
Advance(1);
|
||||
buffer.Append(ScanEscapeSequence());
|
||||
break;
|
||||
case '\'':
|
||||
Advance(1);
|
||||
|
||||
return new Token(TokenType.CharacterLiteral, buffer.ToString())
|
||||
{
|
||||
ValueKind = ValueKind.Character
|
||||
};
|
||||
default:
|
||||
if (buffer.Length > 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Too many characters in character literal at position {position}.");
|
||||
}
|
||||
|
||||
buffer.Append(ch);
|
||||
Advance(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Token ScanStringLiteral()
|
||||
{
|
||||
Advance(1);
|
||||
|
||||
var buffer = new StringBuilder();
|
||||
|
||||
while (true)
|
||||
{
|
||||
var ch = Peek();
|
||||
|
||||
switch (ch)
|
||||
{
|
||||
case '\0':
|
||||
throw new InvalidOperationException($"Unexpected end of string literal at position {position}.");
|
||||
case '\\':
|
||||
Advance(1);
|
||||
buffer.Append(ScanEscapeSequence());
|
||||
break;
|
||||
case '"':
|
||||
Advance(1);
|
||||
return new Token(TokenType.StringLiteral, buffer.ToString())
|
||||
{
|
||||
ValueKind = ValueKind.String
|
||||
};
|
||||
default:
|
||||
buffer.Append(ch);
|
||||
Advance(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Token ScanNumericLiteral()
|
||||
{
|
||||
var buffer = new StringBuilder();
|
||||
var hasDecimal = false;
|
||||
var hasFSuffix = false;
|
||||
var hasDSuffix = false;
|
||||
var hasMSuffix = false;
|
||||
var hasLSuffix = false;
|
||||
var hasExponent = false;
|
||||
var hasHex = false;
|
||||
var hasUSuffix = false;
|
||||
|
||||
while (true)
|
||||
{
|
||||
var ch = Peek();
|
||||
|
||||
if (ch == '0')
|
||||
{
|
||||
var next = Peek(1);
|
||||
|
||||
if (next == 'x' || next == 'X')
|
||||
{
|
||||
hasHex = true;
|
||||
Advance(2);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (ch >= '0' && ch <= '9')
|
||||
{
|
||||
buffer.Append(ch);
|
||||
Advance(1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch == '.')
|
||||
{
|
||||
if (hasDecimal)
|
||||
{
|
||||
throw new InvalidOperationException($"Unexpected character '{ch}' at position {position}.");
|
||||
}
|
||||
|
||||
hasDecimal = true;
|
||||
|
||||
buffer.Append(ch);
|
||||
|
||||
Advance(1);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch == 'l' || ch == 'L')
|
||||
{
|
||||
if (hasLSuffix)
|
||||
{
|
||||
throw new InvalidOperationException($"Unexpected character '{ch}' at position {position}.");
|
||||
}
|
||||
|
||||
hasLSuffix = true;
|
||||
|
||||
Advance(1);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch == 'u' || ch == 'U')
|
||||
{
|
||||
if (hasUSuffix)
|
||||
{
|
||||
throw new InvalidOperationException($"Unexpected character '{ch}' at position {position}.");
|
||||
}
|
||||
|
||||
hasUSuffix = true;
|
||||
|
||||
Advance(1);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasHex && ((ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F')))
|
||||
{
|
||||
buffer.Append(ch);
|
||||
Advance(1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch == 'e' || ch == 'E')
|
||||
{
|
||||
if (hasExponent)
|
||||
{
|
||||
throw new InvalidOperationException($"Unexpected character '{ch}' at position {position}.");
|
||||
}
|
||||
|
||||
hasExponent = true;
|
||||
buffer.Append(ch);
|
||||
Advance(1);
|
||||
|
||||
// Check for optional + or - after e/E
|
||||
ch = Peek();
|
||||
if (ch == '+' || ch == '-')
|
||||
{
|
||||
buffer.Append(ch);
|
||||
Advance(1);
|
||||
}
|
||||
|
||||
// Must have at least one digit after e/E
|
||||
ch = Peek();
|
||||
|
||||
if (ch < '0' || ch > '9')
|
||||
{
|
||||
throw new InvalidOperationException($"Expected digit after exponent at position {position}.");
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasDecimal || hasExponent)
|
||||
{
|
||||
switch (ch)
|
||||
{
|
||||
case 'F':
|
||||
case 'f':
|
||||
hasFSuffix = true;
|
||||
Advance(1);
|
||||
break;
|
||||
case 'D':
|
||||
case 'd':
|
||||
hasDSuffix = true;
|
||||
Advance(1);
|
||||
break;
|
||||
case 'M':
|
||||
case 'm':
|
||||
hasMSuffix = true;
|
||||
Advance(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
var value = new Token(TokenType.NumericLiteral);
|
||||
|
||||
var valueKind = ValueKind.None;
|
||||
|
||||
if (hasDecimal || hasExponent)
|
||||
{
|
||||
valueKind = ValueKind.Double;
|
||||
}
|
||||
|
||||
if (hasFSuffix)
|
||||
{
|
||||
valueKind = ValueKind.Float;
|
||||
}
|
||||
|
||||
if (hasDSuffix)
|
||||
{
|
||||
valueKind = ValueKind.Double;
|
||||
}
|
||||
|
||||
if (hasMSuffix)
|
||||
{
|
||||
valueKind = ValueKind.Decimal;
|
||||
}
|
||||
|
||||
switch (valueKind)
|
||||
{
|
||||
case ValueKind.Float:
|
||||
value.ValueKind = ValueKind.Float;
|
||||
value.FloatValue = GetValueFloat(buffer.ToString());
|
||||
break;
|
||||
case ValueKind.Double:
|
||||
value.ValueKind = ValueKind.Double;
|
||||
value.DoubleValue = GetValueDouble(buffer.ToString());
|
||||
break;
|
||||
case ValueKind.Decimal:
|
||||
value.ValueKind = ValueKind.Decimal;
|
||||
value.DecimalValue = GetValueDecimal(buffer.ToString());
|
||||
break;
|
||||
default:
|
||||
var val = GetValueUInt64(buffer.ToString(), hasHex);
|
||||
|
||||
if (!hasUSuffix && !hasLSuffix)
|
||||
{
|
||||
if (val <= Int32.MaxValue)
|
||||
{
|
||||
value.ValueKind = ValueKind.Int;
|
||||
value.IntValue = (int)val;
|
||||
}
|
||||
else if (val <= UInt32.MaxValue)
|
||||
{
|
||||
value.ValueKind = ValueKind.UInt;
|
||||
value.UintValue = (uint)val;
|
||||
}
|
||||
else if (val <= Int64.MaxValue)
|
||||
{
|
||||
value.ValueKind = ValueKind.Long;
|
||||
value.LongValue = (long)val;
|
||||
}
|
||||
else
|
||||
{
|
||||
value.ValueKind = ValueKind.ULong;
|
||||
value.UlongValue = val;
|
||||
}
|
||||
}
|
||||
else if (hasUSuffix && !hasLSuffix)
|
||||
{
|
||||
if (val <= UInt32.MaxValue)
|
||||
{
|
||||
value.ValueKind = ValueKind.UInt;
|
||||
value.UintValue = (uint)val;
|
||||
}
|
||||
else
|
||||
{
|
||||
value.ValueKind = ValueKind.ULong;
|
||||
value.UlongValue = val;
|
||||
}
|
||||
}
|
||||
else if (!hasUSuffix & hasLSuffix)
|
||||
{
|
||||
if (val <= Int64.MaxValue)
|
||||
{
|
||||
value.ValueKind = ValueKind.Long;
|
||||
value.LongValue = (long)val;
|
||||
}
|
||||
else
|
||||
{
|
||||
value.ValueKind = ValueKind.ULong;
|
||||
value.UlongValue = val;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
value.ValueKind = ValueKind.ULong;
|
||||
value.UlongValue = val;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private static decimal GetValueDecimal(string text)
|
||||
{
|
||||
if (!decimal.TryParse(text, NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent, CultureInfo.InvariantCulture, out var result))
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid numeric literal: {text}");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static float GetValueFloat(string text)
|
||||
{
|
||||
if (!float.TryParse(text, NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent, CultureInfo.InvariantCulture, out var result))
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid numeric literal: {text}");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static double GetValueDouble(string text)
|
||||
{
|
||||
if (!double.TryParse(text, NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent, CultureInfo.InvariantCulture, out var result))
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid numeric literal: {text}");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static ulong GetValueUInt64(string text, bool isHex)
|
||||
{
|
||||
if (!UInt64.TryParse(text, isHex ? NumberStyles.AllowHexSpecifier : NumberStyles.None, CultureInfo.InvariantCulture, out var result))
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid numeric literal: {text}");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private Token ScanIdentifier()
|
||||
{
|
||||
var startOffset = position;
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (position == expression.Length)
|
||||
{
|
||||
var length = position - startOffset;
|
||||
|
||||
return new Token(TokenType.Identifier, expression.Substring(startOffset, length));
|
||||
}
|
||||
|
||||
switch (Peek())
|
||||
{
|
||||
case '\0':
|
||||
case ' ':
|
||||
case '\r':
|
||||
case '\n':
|
||||
case '\t':
|
||||
case '!':
|
||||
case '%':
|
||||
case '(':
|
||||
case ')':
|
||||
case '*':
|
||||
case '+':
|
||||
case ',':
|
||||
case '-':
|
||||
case '.':
|
||||
case '/':
|
||||
case ':':
|
||||
case ';':
|
||||
case '<':
|
||||
case '=':
|
||||
case '>':
|
||||
case '?':
|
||||
case '[':
|
||||
case ']':
|
||||
case '^':
|
||||
case '{':
|
||||
case '|':
|
||||
case '}':
|
||||
case '~':
|
||||
case '"':
|
||||
case '\'':
|
||||
// All of the following characters are not valid in an
|
||||
// identifier. If we see any of them, then we know we're
|
||||
// done.
|
||||
return new Token(TokenType.Identifier, expression[startOffset..position]);
|
||||
case >= '0' and <= '9':
|
||||
if (position == startOffset)
|
||||
{
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
goto case '_';
|
||||
}
|
||||
case (>= 'a' and <= 'z') or (>= 'A' and <= 'Z'):
|
||||
case '_':
|
||||
// All of these characters are valid inside an identifier.
|
||||
// consume it and keep processing.
|
||||
Advance(1);
|
||||
continue;
|
||||
|
||||
default:
|
||||
throw new InvalidOperationException($"Unexpected character '{Peek()}' at position {position}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
462
Radzen.Blazor/ExpressionSerializer.cs
Normal file
462
Radzen.Blazor/ExpressionSerializer.cs
Normal file
@@ -0,0 +1,462 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
|
||||
/// <summary>
|
||||
/// Serializes LINQ Expression Trees into C# string representations.
|
||||
/// </summary>
|
||||
public class ExpressionSerializer : ExpressionVisitor
|
||||
{
|
||||
private readonly StringBuilder _sb = new StringBuilder();
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a given LINQ Expression into a C# string.
|
||||
/// </summary>
|
||||
/// <param name="expression">The expression to serialize.</param>
|
||||
/// <returns>A string representation of the expression.</returns>
|
||||
public string Serialize(Expression expression)
|
||||
{
|
||||
_sb.Clear();
|
||||
Visit(expression);
|
||||
return _sb.ToString();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Expression VisitLambda<T>(Expression<T> node)
|
||||
{
|
||||
if (node.Parameters.Count > 1)
|
||||
{
|
||||
_sb.Append("(");
|
||||
for (int i = 0; i < node.Parameters.Count; i++)
|
||||
{
|
||||
if (i > 0) _sb.Append(", ");
|
||||
_sb.Append(node.Parameters[i].Name);
|
||||
}
|
||||
_sb.Append(") => ");
|
||||
}
|
||||
else
|
||||
{
|
||||
_sb.Append(node.Parameters[0].Name);
|
||||
_sb.Append(" => ");
|
||||
}
|
||||
Visit(node.Body);
|
||||
return node;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Expression VisitParameter(ParameterExpression node)
|
||||
{
|
||||
_sb.Append(node.Name);
|
||||
return node;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Expression VisitMember(MemberExpression node)
|
||||
{
|
||||
if (node.Expression != null)
|
||||
{
|
||||
Visit(node.Expression);
|
||||
_sb.Append($".{node.Member.Name}");
|
||||
}
|
||||
else
|
||||
{
|
||||
_sb.Append(node.Member.Name);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Expression VisitMethodCall(MethodCallExpression node)
|
||||
{
|
||||
if (node.Method.IsStatic && node.Arguments.Count > 0 &&
|
||||
(node.Method.DeclaringType == typeof(Enumerable) ||
|
||||
node.Method.DeclaringType == typeof(Queryable)))
|
||||
{
|
||||
Visit(node.Arguments[0]);
|
||||
_sb.Append($".{node.Method.Name}(");
|
||||
|
||||
for (int i = 1; i < node.Arguments.Count; i++)
|
||||
{
|
||||
if (i > 1) _sb.Append(", ");
|
||||
|
||||
if (node.Arguments[i] is NewArrayExpression arrayExpr)
|
||||
{
|
||||
VisitNewArray(arrayExpr);
|
||||
}
|
||||
else
|
||||
{
|
||||
Visit(node.Arguments[i]);
|
||||
}
|
||||
}
|
||||
|
||||
_sb.Append(")");
|
||||
}
|
||||
else if (node.Method.IsStatic)
|
||||
{
|
||||
_sb.Append($"{node.Method.DeclaringType.Name}.{node.Method.Name}(");
|
||||
|
||||
for (int i = 0; i < node.Arguments.Count; i++)
|
||||
{
|
||||
if (i > 0) _sb.Append(", ");
|
||||
Visit(node.Arguments[i]);
|
||||
}
|
||||
|
||||
_sb.Append(")");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (node.Object != null)
|
||||
{
|
||||
Visit(node.Object);
|
||||
_sb.Append($".{node.Method.Name}(");
|
||||
}
|
||||
else
|
||||
{
|
||||
_sb.Append($"{node.Method.Name}(");
|
||||
}
|
||||
|
||||
for (int i = 0; i < node.Arguments.Count; i++)
|
||||
{
|
||||
if (i > 0) _sb.Append(", ");
|
||||
Visit(node.Arguments[i]);
|
||||
}
|
||||
|
||||
_sb.Append(")");
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Expression VisitUnary(UnaryExpression node)
|
||||
{
|
||||
if (node.NodeType == ExpressionType.Not)
|
||||
{
|
||||
_sb.Append("(!");
|
||||
Visit(node.Operand);
|
||||
_sb.Append(")");
|
||||
}
|
||||
else if (node.NodeType == ExpressionType.Convert)
|
||||
{
|
||||
if (node.Operand is IndexExpression indexExpr)
|
||||
{
|
||||
_sb.Append($"({node.Type.DisplayName(true).Replace("+",".")})");
|
||||
|
||||
Visit(indexExpr.Object);
|
||||
|
||||
_sb.Append("[");
|
||||
Visit(indexExpr.Arguments[0]);
|
||||
_sb.Append("]");
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
Visit(node.Operand);
|
||||
}
|
||||
else
|
||||
{
|
||||
_sb.Append(node.NodeType switch
|
||||
{
|
||||
ExpressionType.Negate => "-",
|
||||
ExpressionType.UnaryPlus => "+",
|
||||
_ => throw new NotSupportedException($"Unsupported unary operator: {node.NodeType}")
|
||||
});
|
||||
Visit(node.Operand);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Expression VisitConstant(ConstantExpression node)
|
||||
{
|
||||
_sb.Append(FormatValue(node.Value));
|
||||
return node;
|
||||
}
|
||||
|
||||
private string FormatValue(object value)
|
||||
{
|
||||
if (value == null)
|
||||
return "null";
|
||||
|
||||
return value switch
|
||||
{
|
||||
string str => $"\"{str}\"",
|
||||
char c => $"'{c}'",
|
||||
bool b => b.ToString().ToLowerInvariant(),
|
||||
DateTime dt => $"DateTime.Parse(\"{dt.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture)}\", CultureInfo.InvariantCulture)",
|
||||
DateOnly dateOnly => $"DateOnly.Parse(\"{dateOnly.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)}\", CultureInfo.InvariantCulture)",
|
||||
TimeOnly timeOnly => $"TimeOnly.Parse(\"{timeOnly.ToString("HH:mm:ss", CultureInfo.InvariantCulture)}\", CultureInfo.InvariantCulture)",
|
||||
Guid guid => $"Guid.Parse(\"{guid.ToString("D", CultureInfo.InvariantCulture)}\")",
|
||||
IEnumerable enumerable when value is not string => FormatEnumerable(enumerable),
|
||||
_ => value.GetType().IsEnum
|
||||
? $"({value.GetType().FullName.Replace("+", ".")})" + Convert.ChangeType(value, Enum.GetUnderlyingType(value.GetType()), CultureInfo.InvariantCulture).ToString()
|
||||
: Convert.ToString(value, CultureInfo.InvariantCulture)
|
||||
};
|
||||
}
|
||||
|
||||
private string FormatEnumerable(IEnumerable enumerable)
|
||||
{
|
||||
var arrayType = enumerable.AsQueryable().ElementType;
|
||||
|
||||
var items = enumerable.Cast<object>().Select(FormatValue);
|
||||
return $"new {(Nullable.GetUnderlyingType(arrayType) != null ? arrayType.DisplayName(true).Replace("+", ".") : "")}[] {{ {string.Join(", ", items)} }}";
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Expression VisitNewArray(NewArrayExpression node)
|
||||
{
|
||||
bool needsParentheses = node.NodeType == ExpressionType.NewArrayInit &&
|
||||
(node.Expressions.Count > 1 || node.Expressions[0].NodeType != ExpressionType.Constant);
|
||||
|
||||
if (needsParentheses) _sb.Append("(");
|
||||
|
||||
_sb.Append("new [] { ");
|
||||
bool first = true;
|
||||
foreach (var expr in node.Expressions)
|
||||
{
|
||||
if (!first) _sb.Append(", ");
|
||||
first = false;
|
||||
Visit(expr);
|
||||
}
|
||||
_sb.Append(" }");
|
||||
|
||||
if (needsParentheses) _sb.Append(")");
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Expression VisitBinary(BinaryExpression node)
|
||||
{
|
||||
_sb.Append("(");
|
||||
Visit(node.Left);
|
||||
_sb.Append($" {GetOperator(node.NodeType)} ");
|
||||
Visit(node.Right);
|
||||
_sb.Append(")");
|
||||
return node;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Expression VisitConditional(ConditionalExpression node)
|
||||
{
|
||||
_sb.Append("(");
|
||||
Visit(node.Test);
|
||||
_sb.Append(" ? ");
|
||||
Visit(node.IfTrue);
|
||||
_sb.Append(" : ");
|
||||
Visit(node.IfFalse);
|
||||
_sb.Append(")");
|
||||
return node;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps an ExpressionType to its corresponding C# operator.
|
||||
/// </summary>
|
||||
/// <param name="type">The ExpressionType to map.</param>
|
||||
/// <returns>A string representation of the corresponding C# operator.</returns>
|
||||
private static string GetOperator(ExpressionType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
ExpressionType.Add => "+",
|
||||
ExpressionType.Subtract => "-",
|
||||
ExpressionType.Multiply => "*",
|
||||
ExpressionType.Divide => "/",
|
||||
ExpressionType.AndAlso => "&&",
|
||||
ExpressionType.OrElse => "||",
|
||||
ExpressionType.Equal => "==",
|
||||
ExpressionType.NotEqual => "!=",
|
||||
ExpressionType.LessThan => "<",
|
||||
ExpressionType.LessThanOrEqual => "<=",
|
||||
ExpressionType.GreaterThan => ">",
|
||||
ExpressionType.GreaterThanOrEqual => ">=",
|
||||
ExpressionType.Coalesce => "??",
|
||||
_ => throw new NotSupportedException($"Unsupported operator: {type}")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides an extension method for displaying type names.
|
||||
/// </summary>
|
||||
public static class SharedTypeExtensions
|
||||
{
|
||||
private static readonly Dictionary<Type, string> BuiltInTypeNames = new()
|
||||
{
|
||||
{ typeof(bool), "bool" },
|
||||
{ typeof(byte), "byte" },
|
||||
{ typeof(char), "char" },
|
||||
{ typeof(decimal), "decimal" },
|
||||
{ typeof(double), "double" },
|
||||
{ typeof(float), "float" },
|
||||
{ typeof(int), "int" },
|
||||
{ typeof(long), "long" },
|
||||
{ typeof(object), "object" },
|
||||
{ typeof(sbyte), "sbyte" },
|
||||
{ typeof(short), "short" },
|
||||
{ typeof(string), "string" },
|
||||
{ typeof(uint), "uint" },
|
||||
{ typeof(ulong), "ulong" },
|
||||
{ typeof(ushort), "ushort" },
|
||||
{ typeof(void), "void" }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Unwraps nullable type.
|
||||
/// </summary>
|
||||
public static Type UnwrapNullableType(this Type type)
|
||||
=> Nullable.GetUnderlyingType(type) ?? type;
|
||||
|
||||
/// <summary>
|
||||
/// Returns a display name for the given type.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to display.</param>
|
||||
/// <param name="fullName">Indicates whether to use the full name.</param>
|
||||
/// <param name="compilable">Indicates whether to use a compilable format.</param>
|
||||
/// <returns>A string representing the type name.</returns>
|
||||
public static string DisplayName(this Type type, bool fullName = true, bool compilable = false)
|
||||
{
|
||||
var stringBuilder = new StringBuilder();
|
||||
ProcessType(stringBuilder, type, fullName, compilable);
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
|
||||
private static void ProcessType(StringBuilder builder, Type type, bool fullName, bool compilable)
|
||||
{
|
||||
if (type.IsGenericType)
|
||||
{
|
||||
var genericArguments = type.GetGenericArguments();
|
||||
ProcessGenericType(builder, type, genericArguments, genericArguments.Length, fullName, compilable);
|
||||
}
|
||||
else if (type.IsArray)
|
||||
{
|
||||
ProcessArrayType(builder, type, fullName, compilable);
|
||||
}
|
||||
else if (BuiltInTypeNames.TryGetValue(type, out var builtInName))
|
||||
{
|
||||
builder.Append(builtInName);
|
||||
}
|
||||
else if (!type.IsGenericParameter)
|
||||
{
|
||||
if (compilable)
|
||||
{
|
||||
if (type.IsNested)
|
||||
{
|
||||
ProcessType(builder, type.DeclaringType!, fullName, compilable);
|
||||
builder.Append('.');
|
||||
}
|
||||
else if (fullName)
|
||||
{
|
||||
builder.Append(type.Namespace).Append('.');
|
||||
}
|
||||
|
||||
builder.Append(type.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append(fullName ? type.FullName : type.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ProcessArrayType(StringBuilder builder, Type type, bool fullName, bool compilable)
|
||||
{
|
||||
var innerType = type;
|
||||
while (innerType.IsArray)
|
||||
{
|
||||
innerType = innerType.GetElementType()!;
|
||||
}
|
||||
|
||||
ProcessType(builder, innerType, fullName, compilable);
|
||||
|
||||
while (type.IsArray)
|
||||
{
|
||||
builder.Append('[');
|
||||
builder.Append(',', type.GetArrayRank() - 1);
|
||||
builder.Append(']');
|
||||
type = type.GetElementType()!;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ProcessGenericType(
|
||||
StringBuilder builder,
|
||||
Type type,
|
||||
Type[] genericArguments,
|
||||
int length,
|
||||
bool fullName,
|
||||
bool compilable)
|
||||
{
|
||||
if (type.IsConstructedGenericType
|
||||
&& type.GetGenericTypeDefinition() == typeof(Nullable<>))
|
||||
{
|
||||
ProcessType(builder, type.UnwrapNullableType(), fullName, compilable);
|
||||
builder.Append('?');
|
||||
return;
|
||||
}
|
||||
|
||||
var offset = type.IsNested ? type.DeclaringType!.GetGenericArguments().Length : 0;
|
||||
|
||||
if (compilable)
|
||||
{
|
||||
if (type.IsNested)
|
||||
{
|
||||
ProcessType(builder, type.DeclaringType!, fullName, compilable);
|
||||
builder.Append('.');
|
||||
}
|
||||
else if (fullName)
|
||||
{
|
||||
builder.Append(type.Namespace);
|
||||
builder.Append('.');
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (fullName)
|
||||
{
|
||||
if (type.IsNested)
|
||||
{
|
||||
ProcessGenericType(builder, type.DeclaringType!, genericArguments, offset, fullName, compilable);
|
||||
builder.Append('+');
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append(type.Namespace);
|
||||
builder.Append('.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var genericPartIndex = type.Name.IndexOf('`');
|
||||
if (genericPartIndex <= 0)
|
||||
{
|
||||
builder.Append(type.Name);
|
||||
return;
|
||||
}
|
||||
|
||||
builder.Append(type.Name, 0, genericPartIndex);
|
||||
builder.Append('<');
|
||||
|
||||
for (var i = offset; i < length; i++)
|
||||
{
|
||||
ProcessType(builder, genericArguments[i], fullName, compilable);
|
||||
if (i + 1 == length)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
builder.Append(',');
|
||||
if (!genericArguments[i + 1].IsGenericParameter)
|
||||
{
|
||||
builder.Append(' ');
|
||||
}
|
||||
}
|
||||
|
||||
builder.Append('>');
|
||||
}
|
||||
}
|
||||
@@ -187,7 +187,7 @@ namespace Radzen.Blazor
|
||||
|
||||
if (Visible)
|
||||
{
|
||||
JSRuntime.InvokeVoidAsync("Radzen.destroyGauge", Element);
|
||||
JSRuntime.InvokeVoid("Radzen.destroyGauge", Element);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
24
Radzen.Blazor/JSRuntimeExtensions.cs
Normal file
24
Radzen.Blazor/JSRuntimeExtensions.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace Radzen;
|
||||
|
||||
static class JSRuntimeExtensions
|
||||
{
|
||||
public static void InvokeVoid(this IJSRuntime jsRuntime, string identifier, params object[] args)
|
||||
{
|
||||
_ = jsRuntime.InvokeVoidAsync(identifier, args).FireAndForget();
|
||||
}
|
||||
|
||||
private static async ValueTask FireAndForget(this ValueTask task)
|
||||
{
|
||||
try
|
||||
{
|
||||
await task;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
47
Radzen.Blazor/Markdown/AtxHeading.cs
Normal file
47
Radzen.Blazor/Markdown/AtxHeading.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a markdown ATX heading: <c># Heading</c>.
|
||||
/// </summary>
|
||||
public class AtxHeading : Heading
|
||||
{
|
||||
private static readonly Regex MarkerRegex = new(@"^#{1,6}(?:[ \t]+|$)");
|
||||
|
||||
private static readonly Regex StartRegex = new(@"^[ \t]*#+[ \t]*$");
|
||||
|
||||
private static readonly Regex EndRegex = new(@"[ \t]+#+[ \t]*$");
|
||||
|
||||
internal static BlockStart Start(BlockParser parser, Block block)
|
||||
{
|
||||
if (parser.Indented)
|
||||
{
|
||||
return BlockStart.Skip;
|
||||
}
|
||||
|
||||
var line = parser.CurrentLine[parser.NextNonSpace..];
|
||||
|
||||
var match = MarkerRegex.Match(line);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
parser.AdvanceNextNonSpace();
|
||||
parser.AdvanceOffset(match.Length, false);
|
||||
parser.CloseUnmatchedBlocks();
|
||||
var container = parser.AddChild<AtxHeading>(parser.NextNonSpace);
|
||||
container.Level = match.Value.Trim().Length;
|
||||
|
||||
// remove trailing ###s:
|
||||
line = parser.CurrentLine[parser.Offset..];
|
||||
|
||||
container.Value = EndRegex.Replace(StartRegex.Replace(line, ""), "");
|
||||
|
||||
parser.AdvanceOffset(parser.CurrentLine.Length - parser.Offset, false);
|
||||
|
||||
return BlockStart.Leaf;
|
||||
}
|
||||
|
||||
return BlockStart.Skip;
|
||||
}
|
||||
}
|
||||
380
Radzen.Blazor/Markdown/BlazorMarkdownRenderer.cs
Normal file
380
Radzen.Blazor/Markdown/BlazorMarkdownRenderer.cs
Normal file
@@ -0,0 +1,380 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
#nullable enable
|
||||
|
||||
class BlazorMarkdownRendererOptions
|
||||
{
|
||||
public int AutoLinkHeadingDepth { get; set; }
|
||||
public bool AllowHtml { get; set; }
|
||||
|
||||
public IEnumerable<string>? AllowedHtmlTags { get; set; }
|
||||
|
||||
public IEnumerable<string>? AllowedHtmlAttributes { get; set; }
|
||||
}
|
||||
|
||||
class BlazorMarkdownRenderer(BlazorMarkdownRendererOptions options, RenderTreeBuilder builder, Action<RenderTreeBuilder, int> outlet) : NodeVisitorBase
|
||||
{
|
||||
public const string Outlet = "<!--rz-outlet-{0}-->";
|
||||
private static readonly Regex OutletRegex = new (@"<!--rz-outlet-(\d+)-->");
|
||||
private static readonly Regex HtmlTagRegex = new(@"<(\w+)((?:\s+[^>]*)?)\/?>");
|
||||
private static readonly Regex HtmlClosingTagRegex = new(@"</(\w+)>");
|
||||
private static readonly Regex AttributeRegex = new(@"(\w+)(?:\s*=\s*(?:([""'])(.*?)\2|([^\s>]+)))?");
|
||||
private readonly HtmlSanitizer sanitizer = new (options.AllowedHtmlTags, options.AllowedHtmlAttributes);
|
||||
|
||||
public override void VisitHeading(Heading heading)
|
||||
{
|
||||
builder.OpenComponent<RadzenText>(0);
|
||||
builder.AddAttribute(1, nameof(RadzenText.ChildContent), RenderChildren(heading.Children));
|
||||
|
||||
switch (heading.Level)
|
||||
{
|
||||
case 1:
|
||||
builder.AddAttribute(2, nameof(RadzenText.TextStyle), TextStyle.H1);
|
||||
break;
|
||||
case 2:
|
||||
builder.AddAttribute(3, nameof(RadzenText.TextStyle), TextStyle.H2);
|
||||
break;
|
||||
case 3:
|
||||
builder.AddAttribute(4, nameof(RadzenText.TextStyle), TextStyle.H3);
|
||||
break;
|
||||
case 4:
|
||||
builder.AddAttribute(5, nameof(RadzenText.TextStyle), TextStyle.H4);
|
||||
break;
|
||||
case 5:
|
||||
builder.AddAttribute(6, nameof(RadzenText.TextStyle), TextStyle.H5);
|
||||
break;
|
||||
case 6:
|
||||
builder.AddAttribute(7, nameof(RadzenText.TextStyle), TextStyle.H6);
|
||||
break;
|
||||
}
|
||||
|
||||
if (heading.Level <= options.AutoLinkHeadingDepth)
|
||||
{
|
||||
var anchor = Regex.Replace(heading.Value, @"[^\w\s-]", string.Empty).Replace(' ', '-').ToLowerInvariant().Trim();
|
||||
builder.AddAttribute(8, nameof(RadzenText.Anchor), anchor);
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.AddAttribute(9, nameof(RadzenText.Anchor), (string?)null);
|
||||
}
|
||||
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
public override void VisitTable(Table table)
|
||||
{
|
||||
builder.OpenComponent<RadzenTable>(0);
|
||||
builder.AddAttribute(1, nameof(RadzenTable.ChildContent), RenderChildren(table.Rows));
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
public override void VisitTableRow(TableRow row)
|
||||
{
|
||||
builder.OpenComponent<RadzenTableRow>(0);
|
||||
builder.AddAttribute(1, nameof(RadzenTableRow.ChildContent), RenderChildren(row.Cells));
|
||||
builder.CloseComponent();
|
||||
}
|
||||
public override void VisitTableCell(TableCell cell)
|
||||
{
|
||||
builder.OpenComponent<RadzenTableCell>(0);
|
||||
builder.AddAttribute(1, nameof(RadzenTableCell.ChildContent), RenderChildren(cell.Children));
|
||||
RenderCellAlignment(builder, cell.Alignment);
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
private static void RenderCellAlignment(RenderTreeBuilder builder, TableCellAlignment alignment)
|
||||
{
|
||||
switch (alignment)
|
||||
{
|
||||
case TableCellAlignment.Center:
|
||||
builder.AddAttribute(2, nameof(RadzenTableCell.Style), "text-align: center");
|
||||
break;
|
||||
case TableCellAlignment.Right:
|
||||
builder.AddAttribute(3, nameof(RadzenTableCell.Style), "text-align: right");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public override void VisitTableHeaderRow(TableHeaderRow header)
|
||||
{
|
||||
builder.OpenComponent<RadzenTableHeader>(0);
|
||||
builder.AddAttribute(1, nameof(RadzenTableHeader.ChildContent), new RenderFragment(headerBuilder =>
|
||||
{
|
||||
headerBuilder.OpenComponent<RadzenTableHeaderRow>(0);
|
||||
headerBuilder.AddAttribute(1, nameof(RadzenTableHeaderRow.ChildContent), new RenderFragment(headerRowBuilder =>
|
||||
{
|
||||
foreach (var cell in header.Cells)
|
||||
{
|
||||
headerRowBuilder.OpenComponent<RadzenTableHeaderCell>(0);
|
||||
headerRowBuilder.AddAttribute(1, nameof(RadzenTableHeaderCell.ChildContent), RenderChildren(cell.Children));
|
||||
RenderCellAlignment(headerRowBuilder, cell.Alignment);
|
||||
headerRowBuilder.CloseComponent();
|
||||
}
|
||||
}));
|
||||
headerBuilder.CloseComponent();
|
||||
}));
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
public override void VisitIndentedCodeBlock(IndentedCodeBlock code)
|
||||
{
|
||||
builder.OpenElement(0, "pre");
|
||||
builder.OpenElement(1, "code");
|
||||
builder.AddContent(2, code.Value);
|
||||
builder.CloseElement();
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
public override void VisitParagraph(Paragraph paragraph)
|
||||
{
|
||||
if (paragraph.Parent is ListItem item && item.Parent is List list && list.Tight)
|
||||
{
|
||||
VisitChildren(paragraph.Children);
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.OpenComponent<RadzenText>(0);
|
||||
builder.AddAttribute(1, nameof(RadzenText.ChildContent), RenderChildren(paragraph.Children));
|
||||
builder.CloseComponent();
|
||||
}
|
||||
}
|
||||
|
||||
private RenderFragment RenderChildren(IEnumerable<INode> children)
|
||||
{
|
||||
return innerBuilder =>
|
||||
{
|
||||
var inner = new BlazorMarkdownRenderer(options, innerBuilder, outlet);
|
||||
inner.VisitChildren(children);
|
||||
};
|
||||
}
|
||||
|
||||
public override void VisitBlockQuote(BlockQuote blockQuote)
|
||||
{
|
||||
builder.OpenElement(0, "blockquote");
|
||||
VisitChildren(blockQuote.Children);
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
public override void VisitCode(Code code)
|
||||
{
|
||||
builder.OpenElement(0, "code");
|
||||
builder.AddContent(1, code.Value);
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
public override void VisitStrong(Strong strong)
|
||||
{
|
||||
builder.OpenElement(0, "strong");
|
||||
VisitChildren(strong.Children);
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
public override void VisitEmphasis(Emphasis emphasis)
|
||||
{
|
||||
builder.OpenElement(0, "em");
|
||||
VisitChildren(emphasis.Children);
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
public override void VisitLink(Link link)
|
||||
{
|
||||
builder.OpenComponent<RadzenLink>(0);
|
||||
|
||||
if (!HtmlSanitizer.IsDangerousUrl(link.Destination))
|
||||
{
|
||||
builder.AddAttribute(1, nameof(RadzenLink.Path), link.Destination);
|
||||
}
|
||||
|
||||
builder.AddAttribute(2, nameof(RadzenLink.ChildContent), RenderChildren(link.Children));
|
||||
|
||||
if (!string.IsNullOrEmpty(link.Title))
|
||||
{
|
||||
builder.AddAttribute(3, "title", link.Title);
|
||||
}
|
||||
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
public override void VisitImage(Image image)
|
||||
{
|
||||
builder.OpenComponent<RadzenImage>(0);
|
||||
|
||||
if (!HtmlSanitizer.IsDangerousUrl(image.Destination))
|
||||
{
|
||||
builder.AddAttribute(1, nameof(RadzenImage.Path), image.Destination);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(image.Title))
|
||||
{
|
||||
builder.AddAttribute(2, nameof(RadzenImage.AlternateText), image.Title);
|
||||
}
|
||||
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
public override void VisitOrderedList(OrderedList orderedList)
|
||||
{
|
||||
builder.OpenElement(0, "ol");
|
||||
VisitChildren(orderedList.Children);
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
public override void VisitUnorderedList(UnorderedList unorderedList)
|
||||
{
|
||||
builder.OpenElement(0, "ul");
|
||||
VisitChildren(unorderedList.Children);
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
public override void VisitListItem(ListItem listItem)
|
||||
{
|
||||
builder.OpenElement(0, "li");
|
||||
VisitChildren(listItem.Children);
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
public override void VisitFencedCodeBlock(FencedCodeBlock fencedCodeBlock)
|
||||
{
|
||||
builder.OpenElement(0, "pre");
|
||||
builder.OpenElement(1, "code");
|
||||
builder.AddContent(2, fencedCodeBlock.Value);
|
||||
builder.CloseElement();
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
public override void VisitThematicBreak(ThematicBreak thematicBreak)
|
||||
{
|
||||
builder.OpenElement(0, "hr");
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
public override void VisitHtmlBlock(HtmlBlock htmlBlock)
|
||||
{
|
||||
var match = OutletRegex.Match(htmlBlock.Value);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
var markerId = Convert.ToInt32(match.Groups[1].Value);
|
||||
outlet(builder, markerId);
|
||||
}
|
||||
else if (options.AllowHtml)
|
||||
{
|
||||
var html = sanitizer.Sanitize(htmlBlock.Value);
|
||||
builder.AddMarkupContent(0, html);
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.AddContent(0, htmlBlock.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public override void VisitLineBreak(LineBreak lineBreak)
|
||||
{
|
||||
builder.OpenElement(0, "br");
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
public override void VisitText(Text text)
|
||||
{
|
||||
builder.AddContent(0, text.Value);
|
||||
}
|
||||
|
||||
private static bool IsVoidElement(string tagName)
|
||||
{
|
||||
return tagName.ToLowerInvariant() switch
|
||||
{
|
||||
"area" => true,
|
||||
"base" => true,
|
||||
"br" => true,
|
||||
"col" => true,
|
||||
"embed" => true,
|
||||
"hr" => true,
|
||||
"img" => true,
|
||||
"input" => true,
|
||||
"link" => true,
|
||||
"meta" => true,
|
||||
"param" => true,
|
||||
"source" => true,
|
||||
"track" => true,
|
||||
"wbr" => true,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
public override void VisitSoftLineBreak(SoftLineBreak softBreak)
|
||||
{
|
||||
builder.AddContent(0, "\n");
|
||||
}
|
||||
|
||||
public override void VisitHtmlInline(HtmlInline htmlInline)
|
||||
{
|
||||
var match = OutletRegex.Match(htmlInline.Value);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
var markerId = Convert.ToInt32(match.Groups[1].Value);
|
||||
outlet(builder, markerId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!options.AllowHtml)
|
||||
{
|
||||
builder.AddContent(0, htmlInline.Value);
|
||||
return;
|
||||
}
|
||||
|
||||
var html = sanitizer.Sanitize(htmlInline.Value);
|
||||
|
||||
var closingMatch = HtmlClosingTagRegex.Match(html);
|
||||
|
||||
if (closingMatch.Success)
|
||||
{
|
||||
builder.CloseElement();
|
||||
return;
|
||||
}
|
||||
|
||||
var openingMatch = HtmlTagRegex.Match(html);
|
||||
|
||||
if (openingMatch.Success)
|
||||
{
|
||||
var tagName = openingMatch.Groups[1].Value;
|
||||
|
||||
builder.OpenElement(0, tagName);
|
||||
|
||||
var attributes = openingMatch.Groups[2].Value;
|
||||
|
||||
if (!string.IsNullOrEmpty(attributes))
|
||||
{
|
||||
var matches = AttributeRegex.Matches(attributes);
|
||||
|
||||
foreach (Match attribute in matches)
|
||||
{
|
||||
var name = attribute.Groups[1].Value;
|
||||
var value = name;
|
||||
|
||||
if (attribute.Groups[2].Success) // Quoted value (either single or double)
|
||||
{
|
||||
value = attribute.Groups[3].Value;
|
||||
}
|
||||
else if (attribute.Groups[4].Success) // Unquoted value
|
||||
{
|
||||
value = attribute.Groups[4].Value;
|
||||
}
|
||||
|
||||
builder.AddAttribute(1, name, value);
|
||||
}
|
||||
}
|
||||
|
||||
if (html.EndsWith("/>") || IsVoidElement(tagName))
|
||||
{
|
||||
builder.CloseElement();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
73
Radzen.Blazor/Markdown/Block.cs
Normal file
73
Radzen.Blazor/Markdown/Block.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
#nullable enable
|
||||
/// <summary>
|
||||
/// Base class for a markdown block nodes.
|
||||
/// </summary>
|
||||
public abstract class Block : INode
|
||||
{
|
||||
/// <summary>
|
||||
/// Accepts a visitor.
|
||||
/// </summary>
|
||||
/// <param name="visitor"></param>
|
||||
public abstract void Accept(INodeVisitor visitor);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the last child of the block.
|
||||
/// </summary>
|
||||
public virtual Block? LastChild => null;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the first child of the block.
|
||||
/// </summary>
|
||||
public virtual Block? FirstChild => null;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the next sibling of the block.
|
||||
/// </summary>
|
||||
public virtual Block? Next => Parent.NextSibling(this);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the parent node of the block.
|
||||
/// </summary>
|
||||
public BlockContainer Parent { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Removes the block from its parent.
|
||||
/// </summary>
|
||||
public void Remove()
|
||||
{
|
||||
Parent.Remove(this);
|
||||
}
|
||||
|
||||
internal virtual BlockMatch Matches(BlockParser parser) => 0;
|
||||
|
||||
internal bool Open { get; set; } = true;
|
||||
|
||||
internal Range Range;
|
||||
|
||||
internal virtual void Close(BlockParser parser)
|
||||
{
|
||||
Open = false;
|
||||
}
|
||||
}
|
||||
|
||||
enum BlockMatch
|
||||
{
|
||||
Match,
|
||||
Skip,
|
||||
Break
|
||||
}
|
||||
|
||||
struct Position
|
||||
{
|
||||
public int Line { get; set; }
|
||||
public int Column { get; set; }
|
||||
}
|
||||
|
||||
struct Range
|
||||
{
|
||||
public Position Start;
|
||||
public Position End;
|
||||
}
|
||||
87
Radzen.Blazor/Markdown/BlockContainer.cs
Normal file
87
Radzen.Blazor/Markdown/BlockContainer.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
#nullable enable
|
||||
/// <summary>
|
||||
/// Base class for markdown block nodes that can contain other blocks.
|
||||
/// </summary>
|
||||
public abstract class BlockContainer : Block
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the children of the block.
|
||||
/// </summary>
|
||||
public IReadOnlyList<Block> Children => children;
|
||||
|
||||
private readonly List<Block> children = [];
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the block can contain the specified node.
|
||||
/// </summary>
|
||||
public virtual bool CanContain(Block node) => false;
|
||||
|
||||
/// <summary>
|
||||
/// Appends a block to the children of the block.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the block.</typeparam>
|
||||
/// <param name="block">The block to add.</param>
|
||||
/// <returns>The added block.</returns>
|
||||
public virtual T Add<T>(T block) where T : Block
|
||||
{
|
||||
children.Add(block);
|
||||
|
||||
block.Parent = this;
|
||||
|
||||
return block;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces a block with another block.
|
||||
/// </summary>
|
||||
/// <param name="source">The block to replace.</param>
|
||||
/// <param name="target">The block to replace with.</param>
|
||||
public void Replace(Block source, Block target)
|
||||
{
|
||||
var index = children.IndexOf(source);
|
||||
|
||||
if (index >= 0)
|
||||
{
|
||||
children[index] = target;
|
||||
target.Parent = this;
|
||||
target.Range = source.Range;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a block from the children of the block.
|
||||
/// </summary>
|
||||
/// <param name="block">The block to remove.</param>
|
||||
public void Remove(Block block)
|
||||
{
|
||||
children.Remove(block);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the next sibling of the block.
|
||||
/// </summary>
|
||||
/// <param name="block">The block to get the next sibling of.</param>
|
||||
/// <returns>The next sibling of the block.</returns>
|
||||
public Block? NextSibling(Block block)
|
||||
{
|
||||
var index = children.IndexOf(block);
|
||||
|
||||
if (index >= 0 && index < children.Count - 1)
|
||||
{
|
||||
return children[index + 1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Block? LastChild => children.Count > 0 ? children[^1] : null;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override Block? FirstChild => children.Count > 0 ? children[0] : null;
|
||||
}
|
||||
491
Radzen.Blazor/Markdown/BlockParser.cs
Normal file
491
Radzen.Blazor/Markdown/BlockParser.cs
Normal file
@@ -0,0 +1,491 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
#nullable enable
|
||||
|
||||
class BlockParser
|
||||
{
|
||||
private static readonly string tagName = @"[A-Za-z][A-Za-z0-9-]*";
|
||||
private static readonly string attributeName = @"[a-zA-Z_:][a-zA-Z0-9:._-]*";
|
||||
private static readonly string unquotedValue = @"[^""'=<>`\x00-\x20]+";
|
||||
private static readonly string singleQuotedValue = @"'[^']*'";
|
||||
private static readonly string doubleQuotedValue = @"""[^""]*""";
|
||||
private static readonly string attributeValue = @$"(?:{unquotedValue}|{singleQuotedValue}|{doubleQuotedValue})";
|
||||
private static readonly string attributeValueSpec = @$"(?:\s*=\s*{attributeValue})";
|
||||
private static readonly string attribute = @$"(?:\s+{attributeName}{attributeValueSpec}?)";
|
||||
|
||||
private static readonly string OpenTag = @$"<{tagName}{attribute}*\s*/?>";
|
||||
private static readonly string CloseTag = @$"</{tagName}\s*[>]";
|
||||
private static readonly string htmlComment = @"<!-->|<!--->|<!--[\s\S]*?-->";
|
||||
private static readonly string processingInstruction = @"<\?[ \s\S]*?\?>";
|
||||
private static readonly string declaration = @$"<![A-Za-z]+[^>]*>";
|
||||
private static readonly string cdata = @"<!\[CDATA\[[\s\S]*?\]\]>";
|
||||
|
||||
public static readonly Regex HtmlRegex = new(@$"^(?:{OpenTag}|{CloseTag}|{htmlComment}|{processingInstruction}|{declaration}|{cdata})");
|
||||
|
||||
private BlockParser()
|
||||
{
|
||||
Tip = document;
|
||||
|
||||
OldTip = document;
|
||||
|
||||
lastMatchedContainer = document;
|
||||
}
|
||||
|
||||
public static Document Parse(string markdown)
|
||||
{
|
||||
var parser = new BlockParser();
|
||||
|
||||
var document = parser.ParseBlocks(markdown);
|
||||
|
||||
parser.ParseInlines(document);
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
public static readonly Regex NewLineRegex = new(@"\r\n|\r|\n");
|
||||
|
||||
private readonly Document document = new();
|
||||
|
||||
private void ParseInlines(Document document)
|
||||
{
|
||||
var visitor = new InlineVisitor(linkReferences);
|
||||
document.Accept(visitor);
|
||||
}
|
||||
|
||||
public char Peek()
|
||||
{
|
||||
return CurrentLine.Peek(Offset);
|
||||
}
|
||||
|
||||
public char PeekNonSpace(int offset = 0)
|
||||
{
|
||||
return CurrentLine.Peek(NextNonSpace + offset);
|
||||
}
|
||||
|
||||
public void AdvanceOffset(int count, bool columns)
|
||||
{
|
||||
var currentLine = CurrentLine;
|
||||
char c;
|
||||
|
||||
while (count > 0 && Offset < currentLine.Length != default)
|
||||
{
|
||||
c = currentLine[Offset];
|
||||
|
||||
if (c == '\t')
|
||||
{
|
||||
var charsToTab = 4 - (Column % 4);
|
||||
|
||||
if (columns)
|
||||
{
|
||||
PartiallyConsumedTab = charsToTab > count;
|
||||
var charsToAdvance = charsToTab > count ? count : charsToTab;
|
||||
Column += charsToAdvance;
|
||||
Offset += PartiallyConsumedTab ? 0 : 1;
|
||||
count -= charsToAdvance;
|
||||
}
|
||||
else
|
||||
{
|
||||
PartiallyConsumedTab = false;
|
||||
Column += charsToTab;
|
||||
Offset += 1;
|
||||
count -= 1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
PartiallyConsumedTab = false;
|
||||
Offset += 1;
|
||||
Column += 1;
|
||||
count -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Document ParseBlocks(string markdown)
|
||||
{
|
||||
LineNumber = 0;
|
||||
|
||||
lastMatchedContainer = document;
|
||||
|
||||
var lines = NewLineRegex.Split(markdown);
|
||||
|
||||
var length = lines.Length;
|
||||
|
||||
if (markdown.EndsWith(InlineParser.LineFeed))
|
||||
{
|
||||
length--;
|
||||
}
|
||||
|
||||
for (var index = 0; index < length; index++)
|
||||
{
|
||||
IncorporateLine(lines[index]);
|
||||
}
|
||||
|
||||
while (Tip != null)
|
||||
{
|
||||
Close(Tip, length);
|
||||
}
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
private void IncorporateLine(string line)
|
||||
{
|
||||
Offset = 0;
|
||||
Column = 0;
|
||||
Blank = false;
|
||||
PartiallyConsumedTab = false;
|
||||
LineNumber++;
|
||||
|
||||
Block container = document;
|
||||
OldTip = Tip;
|
||||
Block? tail;
|
||||
var allMatched = true;
|
||||
CurrentLine = line;
|
||||
|
||||
while ((tail = container.LastChild) != null && tail.Open)
|
||||
{
|
||||
container = tail;
|
||||
|
||||
FindNextNonSpace();
|
||||
|
||||
switch (container.Matches(this))
|
||||
{
|
||||
case BlockMatch.Match: // we've matched, keep going
|
||||
break;
|
||||
case BlockMatch.Skip: // we've failed to match a block
|
||||
allMatched = false;
|
||||
break;
|
||||
case BlockMatch.Break: // we've hit end of line for fenced code close and can return
|
||||
return;
|
||||
default:
|
||||
throw new InvalidOperationException("Invalid continue result");
|
||||
}
|
||||
|
||||
if (!allMatched)
|
||||
{
|
||||
container = container.Parent;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
AllClosed = container == OldTip;
|
||||
lastMatchedContainer = container;
|
||||
|
||||
var matchedLeaf = container is not (Paragraph or Table) && container is Leaf;
|
||||
|
||||
while (!matchedLeaf)
|
||||
{
|
||||
FindNextNonSpace();
|
||||
|
||||
int blockIndex;
|
||||
|
||||
for (blockIndex = 0; blockIndex < blockStarts.Length; blockIndex++)
|
||||
{
|
||||
var blockStart = blockStarts[blockIndex];
|
||||
|
||||
var result = blockStart(this, container);
|
||||
|
||||
if (result == BlockStart.Container)
|
||||
{
|
||||
container = Tip;
|
||||
break;
|
||||
}
|
||||
else if (result == BlockStart.Leaf)
|
||||
{
|
||||
container = Tip;
|
||||
matchedLeaf = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (blockIndex == blockStarts.Length)
|
||||
{
|
||||
AdvanceNextNonSpace();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// What remains at the offset is a text line. Add the text to the
|
||||
// appropriate container.
|
||||
|
||||
if (!AllClosed && !Blank && this.Tip is Paragraph or Table)
|
||||
{
|
||||
// lazy paragraph continuation
|
||||
if (Tip is Paragraph paragraph)
|
||||
{
|
||||
paragraph.AddLine(this);
|
||||
}
|
||||
else if (Tip is Table table)
|
||||
{
|
||||
table.AddLine(this);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// not a lazy continuation
|
||||
|
||||
// finalize any blocks not matched
|
||||
CloseUnmatchedBlocks();
|
||||
|
||||
if (container is Leaf leaf)
|
||||
{
|
||||
leaf.AddLine(this);
|
||||
|
||||
if (container is HtmlBlock block && block.Type >= 1 && block.Type <= 5 && HtmlBlockCloseRegex[block.Type].IsMatch(line[Offset..]))
|
||||
{
|
||||
LastLineLength = line.Length;
|
||||
Close(container, LineNumber);
|
||||
}
|
||||
}
|
||||
else if (Offset < line.Length && !Blank)
|
||||
{
|
||||
var paragraph = AddChild<Paragraph>(Offset);
|
||||
AdvanceNextNonSpace();
|
||||
paragraph.AddLine(this);
|
||||
}
|
||||
}
|
||||
LastLineLength = line.Length;
|
||||
}
|
||||
|
||||
public int LastLineLength { get; set; }
|
||||
|
||||
public void Close(Block block, int lineNumber)
|
||||
{
|
||||
var above = block.Parent;
|
||||
block.Range.End.Line = lineNumber;
|
||||
block.Range.End.Column = LastLineLength;
|
||||
block.Close(this);
|
||||
Tip = above;
|
||||
}
|
||||
|
||||
public T AddChild<T>(int offset) where T : Block, new()
|
||||
{
|
||||
var node = new T();
|
||||
|
||||
AddChild(node, offset);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
public void AddChild(Block node, int offset)
|
||||
{
|
||||
while (Tip is not BlockContainer container || !container.CanContain(node))
|
||||
{
|
||||
Close(Tip, LineNumber - 1);
|
||||
}
|
||||
|
||||
if (Tip is BlockContainer parent)
|
||||
{
|
||||
parent.Add(node);
|
||||
}
|
||||
|
||||
var columnNumber = offset + 1; // offset 0 = column 1
|
||||
|
||||
node.Range.Start.Line = LineNumber;
|
||||
node.Range.Start.Column = columnNumber;
|
||||
|
||||
Tip = node;
|
||||
}
|
||||
|
||||
public void CloseUnmatchedBlocks()
|
||||
{
|
||||
if (!AllClosed)
|
||||
{
|
||||
while (OldTip != lastMatchedContainer)
|
||||
{
|
||||
var parent = OldTip.Parent;
|
||||
Close(OldTip, LineNumber - 1);
|
||||
OldTip = parent;
|
||||
}
|
||||
|
||||
AllClosed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void AdvanceNextNonSpace()
|
||||
{
|
||||
Offset = NextNonSpace;
|
||||
Column = NextNonSpaceColumn;
|
||||
PartiallyConsumedTab = false;
|
||||
}
|
||||
|
||||
private static readonly Func<BlockParser, Block, BlockStart>[] blockStarts =
|
||||
[
|
||||
BlockQuote.Start,
|
||||
AtxHeading.Start,
|
||||
FencedCodeBlock.Start,
|
||||
HtmlBlock.Start,
|
||||
SetExtHeading.Start,
|
||||
ThematicBreak.Start,
|
||||
ListItem.Start,
|
||||
IndentedCodeBlock.Start,
|
||||
Table.Start,
|
||||
];
|
||||
|
||||
public bool AllClosed { get; private set; }
|
||||
|
||||
private Block lastMatchedContainer;
|
||||
|
||||
public void FindNextNonSpace()
|
||||
{
|
||||
var currentLine = CurrentLine;
|
||||
var i = Offset;
|
||||
var cols = Column;
|
||||
char c = default;
|
||||
|
||||
while (i < currentLine.Length)
|
||||
{
|
||||
c = currentLine[i];
|
||||
|
||||
if (c == ' ')
|
||||
{
|
||||
i++;
|
||||
cols++;
|
||||
}
|
||||
else if (c == '\t')
|
||||
{
|
||||
i++;
|
||||
cols += 4 - (cols % 4);
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Blank = c == '\n' || c == '\r' || i == currentLine.Length;
|
||||
NextNonSpace = i;
|
||||
NextNonSpaceColumn = cols;
|
||||
Indent = NextNonSpaceColumn - Column;
|
||||
Indented = Indent >= CodeIndent;
|
||||
}
|
||||
|
||||
public const int CodeIndent = 4;
|
||||
|
||||
public int Indent { get; private set; }
|
||||
public bool Indented { get; private set; }
|
||||
public int NextNonSpaceColumn { get; private set; }
|
||||
|
||||
public int NextNonSpace { get; private set; }
|
||||
|
||||
public bool Blank { get; private set; }
|
||||
public bool PartiallyConsumedTab { get; private set; }
|
||||
public Block Tip { get; set; }
|
||||
public Block OldTip { get; private set; }
|
||||
public string CurrentLine { get; private set; } = string.Empty;
|
||||
public int Offset { get; set; }
|
||||
public int Column { get; set; }
|
||||
public int LineNumber { get; private set; }
|
||||
|
||||
private static readonly Regex LinkReferenceRegex = new(@"^[ \t]{0,3}\[");
|
||||
|
||||
private readonly Dictionary<string, LinkReference> linkReferences = [];
|
||||
|
||||
// https://spec.commonmark.org/0.31.2/#html-blocks
|
||||
internal static readonly Regex[] HtmlBlockOpenRegex = [
|
||||
new (@"."), // dummy for 1 based indexing
|
||||
new (@"^<(?:script|pre|textarea|style)(?:\s|>|$)", RegexOptions.IgnoreCase),
|
||||
new (@"^<!--"),
|
||||
new (@"^<[?]"),
|
||||
new (@"^<![A-Za-z]"),
|
||||
new (@"^<!\[CDATA\["),
|
||||
new (@"^<[/]?(?:address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[123456]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|nav|noframes|ol|optgroup|option|p|param|section|search|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul)(?:\s|[/]?[>]|$)", RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||
new (@$"^(?:{OpenTag}|{CloseTag})\s*$", RegexOptions.IgnoreCase)
|
||||
];
|
||||
|
||||
private static readonly Regex[] HtmlBlockCloseRegex = [
|
||||
new (@"."), // dummy for 1 based indexing
|
||||
new (@"</(?:script|pre|textarea|style)>", RegexOptions.IgnoreCase),
|
||||
new (@"-->"),
|
||||
new (@"\?>"),
|
||||
new (@">"),
|
||||
new (@"\]\]>")
|
||||
];
|
||||
|
||||
|
||||
public bool TryParseLinkReference(string markdown, out int newIndex)
|
||||
{
|
||||
newIndex = 0;
|
||||
|
||||
if (!LinkReferenceRegex.IsMatch(markdown))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var position = 0;
|
||||
|
||||
while (position < markdown.Length - 1 && (markdown[position] is not InlineParser.CloseBracket || (position > 0 && markdown[position - 1] is InlineParser.Backslash)))
|
||||
{
|
||||
position++;
|
||||
}
|
||||
|
||||
if (position >= markdown.Length - 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
position++;
|
||||
|
||||
if (position >= markdown.Length || markdown[position] is not InlineParser.Colon)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var colonIndex = position;
|
||||
var closeIndex = colonIndex - 1;
|
||||
var openIndex = 0;
|
||||
|
||||
while (openIndex < closeIndex && markdown[openIndex] is not InlineParser.OpenBracket)
|
||||
{
|
||||
openIndex++;
|
||||
}
|
||||
|
||||
if (openIndex == closeIndex)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var id = new StringBuilder();
|
||||
|
||||
for (var index = openIndex + 1; index < closeIndex; index++)
|
||||
{
|
||||
var next = index < closeIndex - 1 ? markdown[index + 1] : default;
|
||||
|
||||
if (markdown[index] is not InlineParser.Backslash || !next.IsPunctuation())
|
||||
{
|
||||
id.Append(markdown[index]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!InlineParser.TryParseDestinationAndTitle(markdown, colonIndex + 1, out var destination, out var title, out position))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var link = new LinkReference { Destination = destination, Title = title };
|
||||
|
||||
var key = id.ToString().ToLowerInvariant();
|
||||
|
||||
if (!linkReferences.ContainsKey(key))
|
||||
{
|
||||
linkReferences[key] = link;
|
||||
}
|
||||
|
||||
newIndex = position;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
enum BlockStart
|
||||
{
|
||||
Skip,
|
||||
Container,
|
||||
Leaf
|
||||
}
|
||||
64
Radzen.Blazor/Markdown/BlockQuote.cs
Normal file
64
Radzen.Blazor/Markdown/BlockQuote.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Represents a markdown block quote: <c>> Quote</c>.
|
||||
/// </summary>
|
||||
public class BlockQuote : BlockContainer
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void Accept(INodeVisitor visitor)
|
||||
{
|
||||
visitor.VisitBlockQuote(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanContain(Block node)
|
||||
{
|
||||
return node is not ListItem;
|
||||
}
|
||||
|
||||
internal override BlockMatch Matches(BlockParser parser)
|
||||
{
|
||||
if (!parser.Indented && parser.PeekNonSpace() == '>')
|
||||
{
|
||||
parser.AdvanceNextNonSpace();
|
||||
parser.AdvanceOffset(1, false);
|
||||
// optional following space
|
||||
|
||||
if (parser.Peek().IsSpaceOrTab())
|
||||
{
|
||||
parser.AdvanceOffset(1, true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return BlockMatch.Skip;
|
||||
}
|
||||
|
||||
return BlockMatch.Match;
|
||||
}
|
||||
|
||||
internal static BlockStart Start(BlockParser parser, Block block)
|
||||
{
|
||||
if (!parser.Indented && parser.PeekNonSpace() == '>')
|
||||
{
|
||||
parser.AdvanceNextNonSpace();
|
||||
parser.AdvanceOffset(1, false);
|
||||
// optional following space
|
||||
|
||||
if (parser.Peek().IsSpaceOrTab())
|
||||
{
|
||||
parser.AdvanceOffset(1, true);
|
||||
}
|
||||
|
||||
parser.CloseUnmatchedBlocks();
|
||||
|
||||
parser.AddChild<BlockQuote>(parser.NextNonSpace);
|
||||
|
||||
return BlockStart.Container;
|
||||
}
|
||||
|
||||
return BlockStart.Skip;
|
||||
}
|
||||
}
|
||||
10
Radzen.Blazor/Markdown/CharExtensions.cs
Normal file
10
Radzen.Blazor/Markdown/CharExtensions.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
static class CharExtensions
|
||||
{
|
||||
public static bool IsNullOrWhiteSpace(this char ch) => ch == '\0' || char.IsWhiteSpace(ch);
|
||||
|
||||
public static bool IsPunctuation(this char ch) => char.IsPunctuation(ch);
|
||||
|
||||
public static bool IsSpaceOrTab(this char ch) => ch == ' ' || ch == '\t';
|
||||
}
|
||||
19
Radzen.Blazor/Markdown/Code.cs
Normal file
19
Radzen.Blazor/Markdown/Code.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a markdown inline code block: <c>`code`</c>.
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
public class Code(string value) : Inline
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the code value.
|
||||
/// </summary>
|
||||
public string Value { get; set; } = value;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Accept(INodeVisitor visitor)
|
||||
{
|
||||
visitor.VisitCode(this);
|
||||
}
|
||||
}
|
||||
35
Radzen.Blazor/Markdown/Document.cs
Normal file
35
Radzen.Blazor/Markdown/Document.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a markdown document.
|
||||
/// </summary>
|
||||
public class Document : BlockContainer
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Document"/> class.
|
||||
/// </summary>
|
||||
public Document()
|
||||
{
|
||||
Range.Start.Line = 1;
|
||||
Range.Start.Column = 1;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Accept(INodeVisitor visitor)
|
||||
{
|
||||
visitor.VisitDocument(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanContain(Block node)
|
||||
{
|
||||
return node is not ListItem;
|
||||
}
|
||||
|
||||
internal override void Close(BlockParser parser)
|
||||
{
|
||||
base.Close(parser);
|
||||
|
||||
LinkReferenceParser.Parse(parser, this);
|
||||
}
|
||||
}
|
||||
13
Radzen.Blazor/Markdown/Emphasis.cs
Normal file
13
Radzen.Blazor/Markdown/Emphasis.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an emphasis element in a markdown document: <c>_emphasis_</c> or <c>*emphasis*</c>.
|
||||
/// </summary>
|
||||
public class Emphasis : InlineContainer
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void Accept(INodeVisitor visitor)
|
||||
{
|
||||
visitor.VisitEmphasis(this);
|
||||
}
|
||||
}
|
||||
98
Radzen.Blazor/Markdown/FencedCodeBlock.cs
Normal file
98
Radzen.Blazor/Markdown/FencedCodeBlock.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a fenced code block in a markdown document: <c>```</c> or <c>~~~</c>.
|
||||
/// </summary>
|
||||
public class FencedCodeBlock : Leaf
|
||||
{
|
||||
/// <summary>
|
||||
/// The delimiter used to start and end the code block.
|
||||
/// </summary>
|
||||
public string Delimiter { get; private set; }
|
||||
internal int Indent { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The info string of the code block. This is the first line of the code block and is used to specify the language of the code block.
|
||||
/// </summary>
|
||||
public string Info { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Accept(INodeVisitor visitor)
|
||||
{
|
||||
visitor.VisitFencedCodeBlock(this);
|
||||
}
|
||||
|
||||
internal override void Close(BlockParser parser)
|
||||
{
|
||||
base.Close(parser);
|
||||
|
||||
// first line becomes info string
|
||||
var newlinePos = Value.IndexOf('\n');
|
||||
var firstLine = Value[..newlinePos];
|
||||
Info = firstLine.Trim();
|
||||
Value = Value[(newlinePos + 1)..];
|
||||
}
|
||||
|
||||
internal override BlockMatch Matches(BlockParser parser)
|
||||
{
|
||||
var line = parser.CurrentLine[parser.NextNonSpace..];
|
||||
|
||||
var indent = parser.Indent;
|
||||
|
||||
var match = ClosingFenceRegex.Match(line);
|
||||
|
||||
if (indent <= 3 && parser.PeekNonSpace() == Delimiter[0] && match.Success && match.Length >= Delimiter.Length)
|
||||
{
|
||||
// closing fence - we're at end of line, so we can return
|
||||
parser.LastLineLength = parser.Offset + indent + match.Length;
|
||||
parser.Close(this, parser.LineNumber);
|
||||
return BlockMatch.Break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// skip optional spaces of fence offset
|
||||
var i = Indent;
|
||||
|
||||
while (i > 0 && parser.Peek().IsSpaceOrTab())
|
||||
{
|
||||
parser.AdvanceOffset(1, true);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
return BlockMatch.Match;
|
||||
}
|
||||
|
||||
|
||||
private static readonly Regex ClosingFenceRegex = new(@"^(?:`{3,}|~{3,})(?=[ \t]*$)");
|
||||
|
||||
private static readonly Regex OpeningFenceRegex = new(@"^`{3,}(?!.*`)|^~{3,}");
|
||||
|
||||
internal static BlockStart Start(BlockParser parser, Block node)
|
||||
{
|
||||
if (parser.Indented)
|
||||
{
|
||||
return BlockStart.Skip;
|
||||
}
|
||||
|
||||
var line = parser.CurrentLine[parser.NextNonSpace..];
|
||||
|
||||
var match = OpeningFenceRegex.Match(line);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
parser.CloseUnmatchedBlocks();
|
||||
|
||||
var container = parser.AddChild<FencedCodeBlock>(parser.NextNonSpace);
|
||||
container.Delimiter = match.Value;
|
||||
container.Indent = parser.Indent;
|
||||
parser.AdvanceNextNonSpace();
|
||||
parser.AdvanceOffset(match.Value.Length, false);
|
||||
return BlockStart.Leaf;
|
||||
}
|
||||
|
||||
return BlockStart.Skip;
|
||||
}
|
||||
}
|
||||
25
Radzen.Blazor/Markdown/Heading.cs
Normal file
25
Radzen.Blazor/Markdown/Heading.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// A base class for all heading elements.
|
||||
/// </summary>
|
||||
public abstract class Heading : Leaf
|
||||
{
|
||||
/// <summary>
|
||||
/// The level of the heading. The value is between 1 and 6.
|
||||
/// </summary>
|
||||
public int Level { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Accept(INodeVisitor visitor)
|
||||
{
|
||||
visitor.VisitHeading(this);
|
||||
}
|
||||
|
||||
internal override BlockMatch Matches(BlockParser parser)
|
||||
{
|
||||
// a heading can never container another line
|
||||
return BlockMatch.Skip;
|
||||
}
|
||||
}
|
||||
56
Radzen.Blazor/Markdown/HtmlBlock.cs
Normal file
56
Radzen.Blazor/Markdown/HtmlBlock.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an HTML block.
|
||||
/// </summary>
|
||||
public class HtmlBlock : Leaf
|
||||
{
|
||||
internal int Type { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Accept(INodeVisitor visitor)
|
||||
{
|
||||
visitor.VisitHtmlBlock(this);
|
||||
}
|
||||
|
||||
internal override BlockMatch Matches(BlockParser parser)
|
||||
{
|
||||
return parser.Blank && (Type == 6 || Type == 7) ? BlockMatch.Skip : BlockMatch.Match;
|
||||
}
|
||||
|
||||
private static readonly Regex TrailinNewLineRegex = new(@"\n$");
|
||||
|
||||
internal override void Close(BlockParser parser)
|
||||
{
|
||||
base.Close(parser);
|
||||
|
||||
Value = TrailinNewLineRegex.Replace(Value, "");
|
||||
}
|
||||
|
||||
internal static BlockStart Start(BlockParser parser, Block node)
|
||||
{
|
||||
if (!parser.Indented && parser.PeekNonSpace() == '<')
|
||||
{
|
||||
var line = parser.CurrentLine[parser.NextNonSpace..];
|
||||
|
||||
for (var blockType = 1; blockType <= 7; blockType++) {
|
||||
|
||||
if (BlockParser.HtmlBlockOpenRegex[blockType].IsMatch(line) &&
|
||||
(blockType < 7 || (node is not Paragraph &&
|
||||
!(!parser.AllClosed && !parser.Blank && parser.Tip is Paragraph) // maybe lazy
|
||||
))) {
|
||||
parser.CloseUnmatchedBlocks();
|
||||
// We don't adjust parser.offset;
|
||||
// spaces are part of the HTML block:
|
||||
var block = parser.AddChild<HtmlBlock>(parser.Offset);
|
||||
block.Type = blockType;
|
||||
return BlockStart.Leaf;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return BlockStart.Skip;
|
||||
}
|
||||
}
|
||||
19
Radzen.Blazor/Markdown/HtmlInline.cs
Normal file
19
Radzen.Blazor/Markdown/HtmlInline.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an inline HTML element.
|
||||
/// </summary>
|
||||
public class HtmlInline : Inline
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the HTML element value.
|
||||
/// </summary>
|
||||
public string Value { get; set; }
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Accept(INodeVisitor visitor)
|
||||
{
|
||||
visitor.VisitHtmlInline(this);
|
||||
}
|
||||
}
|
||||
136
Radzen.Blazor/Markdown/HtmlSanitizer.cs
Normal file
136
Radzen.Blazor/Markdown/HtmlSanitizer.cs
Normal file
@@ -0,0 +1,136 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
#nullable enable
|
||||
class HtmlSanitizer
|
||||
{
|
||||
private readonly ISet<string> allowedTags;
|
||||
private readonly ISet<string> allowedAttributes;
|
||||
|
||||
public HtmlSanitizer(IEnumerable<string>? allowedHtmlTags, IEnumerable<string>? allowedHtmlAttributes)
|
||||
{
|
||||
allowedTags = allowedHtmlTags != null ? new HashSet<string>(allowedHtmlTags) : AllowedTags;
|
||||
allowedAttributes = allowedHtmlAttributes != null ? new HashSet<string>(allowedHtmlAttributes) : AllowedAttributes;
|
||||
}
|
||||
|
||||
private static ISet<string> AllowedTags { get; } = new HashSet<string>()
|
||||
{
|
||||
// https://developer.mozilla.org/en/docs/Web/Guide/HTML/HTML5/HTML5_element_list
|
||||
"a", "abbr", "acronym", "address", "area", "b",
|
||||
"big", "blockquote", "br", "button", "caption", "center", "cite",
|
||||
"code", "col", "colgroup", "dd", "del", "dfn", "dir", "div", "dl", "dt",
|
||||
"em", "fieldset", "font", "form", "h1", "h2", "h3", "h4", "h5", "h6",
|
||||
"hr", "i", "img", "input", "ins", "kbd", "label", "legend", "li", "map",
|
||||
"menu", "ol", "optgroup", "option", "p", "pre", "q", "s", "samp",
|
||||
"select", "small", "span", "strike", "strong", "sub", "sup", "table",
|
||||
"tbody", "td", "textarea", "tfoot", "th", "thead", "tr", "tt", "u",
|
||||
"ul", "var", "section", "nav", "article", "aside", "header", "footer", "main",
|
||||
"figure", "figcaption", "data", "time", "mark", "ruby", "rt", "rp", "bdi", "wbr",
|
||||
"datalist", "keygen", "output", "progress", "meter", "details", "summary", "menuitem",
|
||||
"html", "head", "body"
|
||||
};
|
||||
|
||||
public static ISet<string> AllowedAttributes { get; } = new HashSet<string>()
|
||||
{
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
|
||||
"abbr", "accept", "accept-charset", "accesskey",
|
||||
"action", "align", "alt", "axis", "bgcolor", "border", "cellpadding",
|
||||
"cellspacing", "char", "charoff", "charset", "checked", "cite", "class",
|
||||
"clear", "cols", "colspan", "color", "compact", "coords", "datetime",
|
||||
"dir", "disabled", "enctype", "for", "frame", "headers", "height",
|
||||
"href", "hreflang", "hspace", "id", "ismap", "label", "lang",
|
||||
"longdesc", "maxlength", "media", "method", "multiple", "name",
|
||||
"nohref", "noshade", "nowrap", "prompt", "readonly", "rel", "rev",
|
||||
"rows", "rowspan", "rules", "scope", "selected", "shape", "size",
|
||||
"span", "src", "start", "style", "summary", "tabindex", "target", "title",
|
||||
"type", "usemap", "valign", "value", "vspace", "width",
|
||||
"high", "keytype", "list", "low", "max", "min", "novalidate", "open", "optimum",
|
||||
"pattern", "placeholder", "pubdate", "radiogroup", "required", "reversed", "spellcheck", "step",
|
||||
"wrap", "challenge", "contenteditable", "draggable", "dropzone", "autocomplete", "autosave",
|
||||
};
|
||||
|
||||
public static ISet<string> UriAttributes { get; } = new HashSet<string>()
|
||||
{
|
||||
"action", "background", "dynsrc", "href", "lowsrc", "src"
|
||||
};
|
||||
|
||||
public string Sanitize(string input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return Regex.Replace(input, @"</?([a-zA-Z0-9]+)(\s[^>]*)?>", SanitizeTag);
|
||||
}
|
||||
|
||||
private string SanitizeTag(Match match)
|
||||
{
|
||||
var tag = match.Groups[1].Value.ToLowerInvariant();
|
||||
|
||||
if (!allowedTags.Contains(tag))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var attributes = match.Groups[2].Value;
|
||||
|
||||
var safeAttributes = Regex.Replace(attributes, @"(\w+)\s*=\s*(""[^""]*""|'[^']*'|[^\s>]+)", SanitizeAttribute);
|
||||
|
||||
return $"<{(match.Value.StartsWith("</") ? "/" : "")}{tag}{safeAttributes}>";
|
||||
}
|
||||
|
||||
private string SanitizeAttribute(Match match)
|
||||
{
|
||||
var name = match.Groups[1].Value.ToLowerInvariant();
|
||||
var value = match.Groups[2].Value;
|
||||
|
||||
if (!allowedAttributes.Contains(name))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (name == "style")
|
||||
{
|
||||
var decoded = HtmlDecode(value).ToLowerInvariant();
|
||||
|
||||
if (Regex.IsMatch(decoded, @"expression|javascript:|vbscript:|url\s*\(\s*(['""])?\s*javascript", RegexOptions.IgnoreCase))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
if (UriAttributes.Contains(name) && IsDangerousUrl(value))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if ((value.StartsWith('\'') && value.EndsWith('\'')) || (value.StartsWith('"') && value.EndsWith('"')))
|
||||
{
|
||||
value = value[1..^1];
|
||||
}
|
||||
|
||||
return $" {name}=\"{value}\"";
|
||||
}
|
||||
|
||||
private static string HtmlDecode(string input)
|
||||
{
|
||||
return System.Web.HttpUtility.HtmlDecode(input);
|
||||
}
|
||||
|
||||
public static bool IsDangerousUrl(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var decoded = HtmlDecode(value).Trim().ToLowerInvariant();
|
||||
|
||||
return decoded.StartsWith("javascript:") ||
|
||||
decoded.StartsWith("vbscript:") ||
|
||||
decoded.StartsWith("data:text/html") ||
|
||||
decoded.Contains("expression(");
|
||||
}
|
||||
}
|
||||
25
Radzen.Blazor/Markdown/IBlockInlineContainer.cs
Normal file
25
Radzen.Blazor/Markdown/IBlockInlineContainer.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a block node that has inline children.
|
||||
/// </summary>
|
||||
public interface IBlockInlineContainer
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the inline children of the block.
|
||||
/// </summary>
|
||||
IReadOnlyList<Inline> Children { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds an inline child to the block.
|
||||
/// </summary>
|
||||
/// <param name="child"></param>
|
||||
void Add(Inline child);
|
||||
|
||||
/// <summary>
|
||||
/// Gets string value of the block.
|
||||
/// </summary>
|
||||
string Value { get; }
|
||||
}
|
||||
15
Radzen.Blazor/Markdown/INode.cs
Normal file
15
Radzen.Blazor/Markdown/INode.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a markdown node that can be visited by a <see cref="INodeVisitor"/>.
|
||||
/// </summary>
|
||||
public interface INode
|
||||
{
|
||||
/// <summary>
|
||||
/// Accepts a <see cref="INodeVisitor"/>.
|
||||
/// </summary>
|
||||
/// <param name="visitor"></param>
|
||||
public void Accept(INodeVisitor visitor);
|
||||
}
|
||||
128
Radzen.Blazor/Markdown/INodeVisitor.cs
Normal file
128
Radzen.Blazor/Markdown/INodeVisitor.cs
Normal file
@@ -0,0 +1,128 @@
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Represents a visitor for Markdown AST nodes.
|
||||
/// </summary>
|
||||
public interface INodeVisitor
|
||||
{
|
||||
/// <summary>
|
||||
/// Visits a heading node.
|
||||
/// </summary>
|
||||
void VisitHeading(Heading heading);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a paragraph node.
|
||||
/// </summary>
|
||||
void VisitParagraph(Paragraph paragraph);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a block quote node.
|
||||
/// </summary>
|
||||
void VisitBlockQuote(BlockQuote blockQuote);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a document node.
|
||||
/// </summary>
|
||||
void VisitDocument(Document document);
|
||||
|
||||
/// <summary>
|
||||
/// Visits an unordered list node.
|
||||
/// </summary>
|
||||
void VisitUnorderedList(UnorderedList unorderedList);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a list item node.
|
||||
/// </summary>
|
||||
void VisitListItem(ListItem listItem);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a text node.
|
||||
/// </summary>
|
||||
void VisitText(Text text);
|
||||
|
||||
/// <summary>
|
||||
/// Visits an ordered list node.
|
||||
/// </summary>
|
||||
void VisitOrderedList(OrderedList orderedList);
|
||||
|
||||
/// <summary>
|
||||
/// Visits an emphasis node.
|
||||
/// </summary>
|
||||
void VisitEmphasis(Emphasis emphasis);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a strong node.
|
||||
/// </summary>
|
||||
void VisitStrong(Strong strong);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a code node.
|
||||
/// </summary>
|
||||
void VisitCode(Code code);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a link node.
|
||||
/// </summary>
|
||||
void VisitLink(Link link);
|
||||
|
||||
/// <summary>
|
||||
/// Visits an image node.
|
||||
/// </summary>
|
||||
void VisitImage(Image image);
|
||||
|
||||
/// <summary>
|
||||
/// Visits an HTML inline node.
|
||||
/// </summary>
|
||||
void VisitHtmlInline(HtmlInline html);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a line break node.
|
||||
/// </summary>
|
||||
void VisitLineBreak(LineBreak lineBreak);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a soft line break node.
|
||||
/// </summary>
|
||||
void VisitSoftLineBreak(SoftLineBreak softLineBreak);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a thematic break node.
|
||||
/// </summary>
|
||||
void VisitThematicBreak(ThematicBreak thematicBreak);
|
||||
|
||||
/// <summary>
|
||||
/// Visits an indented code block node.
|
||||
/// </summary>
|
||||
void VisitIndentedCodeBlock(IndentedCodeBlock codeBlock);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a fenced code block node.
|
||||
/// </summary>
|
||||
void VisitFencedCodeBlock(FencedCodeBlock fencedCodeBlock);
|
||||
|
||||
/// <summary>
|
||||
/// Visits an HTML block node.
|
||||
/// </summary>
|
||||
void VisitHtmlBlock(HtmlBlock htmlBlock);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a table node.
|
||||
/// </summary>
|
||||
void VisitTable(Table table);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a table header row node.
|
||||
/// </summary>
|
||||
void VisitTableHeaderRow(TableHeaderRow header);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a table row node.
|
||||
/// </summary>
|
||||
void VisitTableRow(TableRow row);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a table cell node.
|
||||
/// </summary>
|
||||
void VisitTableCell(TableCell cell);
|
||||
}
|
||||
23
Radzen.Blazor/Markdown/Image.cs
Normal file
23
Radzen.Blazor/Markdown/Image.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an inline image element: <c></c>
|
||||
/// </summary>
|
||||
public class Image : InlineContainer
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the destination (URL) of the image.
|
||||
/// </summary>
|
||||
public string Destination { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the alternative text of the image.
|
||||
/// </summary>
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Accept(INodeVisitor visitor)
|
||||
{
|
||||
visitor.VisitImage(this);
|
||||
}
|
||||
}
|
||||
69
Radzen.Blazor/Markdown/IndentedCodeBlock.cs
Normal file
69
Radzen.Blazor/Markdown/IndentedCodeBlock.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a markdown indented code block.
|
||||
/// </summary>
|
||||
public class IndentedCodeBlock : Leaf
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void Accept(INodeVisitor visitor)
|
||||
{
|
||||
visitor.VisitIndentedCodeBlock(this);
|
||||
}
|
||||
|
||||
internal override BlockMatch Matches(BlockParser parser)
|
||||
{
|
||||
if (parser.Indent >= BlockParser.CodeIndent)
|
||||
{
|
||||
parser.AdvanceOffset(BlockParser.CodeIndent, true);
|
||||
}
|
||||
else if (parser.Blank)
|
||||
{
|
||||
parser.AdvanceNextNonSpace();
|
||||
}
|
||||
else
|
||||
{
|
||||
return BlockMatch.Skip;
|
||||
}
|
||||
|
||||
return BlockMatch.Match;
|
||||
}
|
||||
|
||||
private static readonly Regex TrailingWhiteSpaceRegex = new(@"^[ \t]*$");
|
||||
|
||||
internal override void Close(BlockParser parser)
|
||||
{
|
||||
base.Close(parser);
|
||||
|
||||
var lines = Value.Split('\n').ToList();;
|
||||
// Note that indented code block cannot be empty, so
|
||||
// lines.length cannot be zero.
|
||||
|
||||
while (TrailingWhiteSpaceRegex.IsMatch(lines[^1]))
|
||||
{
|
||||
lines.RemoveAt(lines.Count - 1);
|
||||
}
|
||||
|
||||
Value = string.Join('\n', lines) + '\n';
|
||||
|
||||
Range.End.Line = Range.Start.Line + lines.Count - 1;
|
||||
Range.End.Column = Range.Start.Column + lines[^1].Length - 1;
|
||||
}
|
||||
|
||||
internal static BlockStart Start(BlockParser parser, Block container)
|
||||
{
|
||||
if (parser.Indented && parser.Tip is not Paragraph && !parser.Blank)
|
||||
{
|
||||
// indented code
|
||||
parser.AdvanceOffset(BlockParser.CodeIndent, true);
|
||||
parser.CloseUnmatchedBlocks();
|
||||
parser.AddChild<IndentedCodeBlock>(parser.Offset);
|
||||
return BlockStart.Leaf;
|
||||
}
|
||||
|
||||
return BlockStart.Skip;
|
||||
}
|
||||
}
|
||||
12
Radzen.Blazor/Markdown/Inline.cs
Normal file
12
Radzen.Blazor/Markdown/Inline.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for markdown inline nodes.
|
||||
/// </summary>
|
||||
public abstract class Inline : INode
|
||||
{
|
||||
/// <summary>
|
||||
/// Accepts a visitor.
|
||||
/// </summary>
|
||||
public abstract void Accept(INodeVisitor visitor);
|
||||
}
|
||||
25
Radzen.Blazor/Markdown/InlineContainer.cs
Normal file
25
Radzen.Blazor/Markdown/InlineContainer.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for inline elements that contain other inline elements.
|
||||
/// </summary>
|
||||
public abstract class InlineContainer : Inline
|
||||
{
|
||||
private readonly List<Inline> children = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the children of the container.
|
||||
/// </summary>
|
||||
public IReadOnlyList<Inline> Children => children;
|
||||
|
||||
/// <summary>
|
||||
/// Appends a child to the container.
|
||||
/// </summary>
|
||||
/// <param name="node">The child to add.</param>
|
||||
public void Add(Inline node)
|
||||
{
|
||||
children.Add(node);
|
||||
}
|
||||
}
|
||||
959
Radzen.Blazor/Markdown/InlineParser.cs
Normal file
959
Radzen.Blazor/Markdown/InlineParser.cs
Normal file
@@ -0,0 +1,959 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Text;
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
class InlineParser
|
||||
{
|
||||
class Delimiter
|
||||
{
|
||||
public char Char { get; set; }
|
||||
public int Length { get; set; }
|
||||
public int Position { get; set; }
|
||||
public Text Node { get; set; }
|
||||
public bool CanOpen { get; set; }
|
||||
public bool CanClose { get; set; }
|
||||
public bool Active { get; set; } = true;
|
||||
}
|
||||
|
||||
private const char Asterisk = '*';
|
||||
private const char Underscore = '_';
|
||||
internal const char Backslash = '\\';
|
||||
private const char Null = '\0';
|
||||
private const char Backtick = '`';
|
||||
internal const char Space = ' ';
|
||||
internal const char LineFeed = '\n';
|
||||
private const char CarrigeReturn = '\r';
|
||||
internal const char OpenBracket = '[';
|
||||
internal const char CloseBracket = ']';
|
||||
private const char OpenParenthesis = '(';
|
||||
private const char CloseParenthesis = ')';
|
||||
internal const char Quote = '"';
|
||||
internal const char OpenAngleBracket = '<';
|
||||
internal const char CloseAngleBracket = '>';
|
||||
private const char SingleQuote = '\'';
|
||||
private const char Exclamation = '!';
|
||||
internal const char Colon = ':';
|
||||
|
||||
private readonly List<Inline> inlines = [];
|
||||
private readonly List<Delimiter> delimiters = [];
|
||||
private readonly StringBuilder buffer = new();
|
||||
|
||||
enum LinkState
|
||||
{
|
||||
Text,
|
||||
Destination,
|
||||
Title
|
||||
}
|
||||
|
||||
private void AddTextNode(bool trim = false)
|
||||
{
|
||||
if (buffer.Length > 0)
|
||||
{
|
||||
var output = new StringBuilder();
|
||||
|
||||
for (var index = 0; index < buffer.Length; index++)
|
||||
{
|
||||
var ch = buffer[index];
|
||||
|
||||
if (ch is Backslash && index < buffer.Length - 1 && !buffer[index + 1].IsPunctuation())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
output.Append(ch);
|
||||
}
|
||||
}
|
||||
var value = output.ToString();
|
||||
|
||||
if (trim)
|
||||
{
|
||||
value = value.TrimEnd();
|
||||
}
|
||||
|
||||
inlines.Add(new Text(value));
|
||||
|
||||
buffer.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryParseCode(string text, int index, out int newIndex)
|
||||
{
|
||||
if (text[index] is not Backtick)
|
||||
{
|
||||
newIndex = index;
|
||||
return false;
|
||||
}
|
||||
|
||||
AddTextNode();
|
||||
|
||||
// Count opening backticks
|
||||
var openingCount = 0;
|
||||
var position = index;
|
||||
while (position < text.Length && text[position] is Backtick)
|
||||
{
|
||||
openingCount++;
|
||||
position++;
|
||||
}
|
||||
|
||||
// Find matching closing backticks
|
||||
var searchStart = position;
|
||||
var bestMatch = -1;
|
||||
|
||||
while (position < text.Length)
|
||||
{
|
||||
// Count consecutive backticks
|
||||
var count = 0;
|
||||
var closingStart = position;
|
||||
while (position < text.Length && text[position] is Backtick)
|
||||
{
|
||||
count++;
|
||||
position++;
|
||||
}
|
||||
|
||||
if (count == openingCount)
|
||||
{
|
||||
bestMatch = closingStart;
|
||||
break;
|
||||
}
|
||||
|
||||
if (position < text.Length)
|
||||
{
|
||||
position++;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestMatch >= 0)
|
||||
{
|
||||
var content = text[searchStart..bestMatch];
|
||||
|
||||
content = BlockParser.NewLineRegex.Replace(content, $"{Space}");
|
||||
|
||||
if (content.StartsWith(Space) && content.EndsWith(Space) && !string.IsNullOrWhiteSpace(content))
|
||||
{
|
||||
content = content[1..^1];
|
||||
}
|
||||
|
||||
inlines.Add(new Code(content));
|
||||
|
||||
newIndex = bestMatch + openingCount;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
inlines.Add(new Text($"{new string(Backtick, openingCount)}"));
|
||||
|
||||
newIndex = index + openingCount;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryParseBackslash(string text, int index, char next, out int newIndex)
|
||||
{
|
||||
if (text[index] is not Backslash)
|
||||
{
|
||||
newIndex = index;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (next.IsPunctuation())
|
||||
{
|
||||
AddTextNode();
|
||||
inlines.Add(new Text(text[index + 1].ToString()));
|
||||
newIndex = index + 2;
|
||||
return true;
|
||||
}
|
||||
|
||||
buffer.Append(text[index]);
|
||||
newIndex = index + 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryParseDelimiter(string text, int index, char next, char prev, out int newIndex)
|
||||
{
|
||||
var ch = text[index];
|
||||
|
||||
if (ch is not (Asterisk or Underscore or OpenBracket) && (ch is not Exclamation || next is not OpenBracket))
|
||||
{
|
||||
newIndex = index;
|
||||
return false;
|
||||
}
|
||||
|
||||
AddTextNode();
|
||||
|
||||
var position = index;
|
||||
|
||||
while (position < text.Length && text[position] == ch)
|
||||
{
|
||||
buffer.Append(ch);
|
||||
|
||||
position++;
|
||||
}
|
||||
|
||||
if (ch is Exclamation)
|
||||
{
|
||||
buffer.Append(OpenBracket);
|
||||
position++;
|
||||
}
|
||||
|
||||
next = position < text.Length ? text[position] : Null;
|
||||
|
||||
if (buffer.Length > 0)
|
||||
{
|
||||
var node = new Text(buffer.ToString());
|
||||
var leftFlanking = LeftFlanking(prev, next);
|
||||
var rightFlanking = RightFlanking(prev, next);
|
||||
|
||||
var canOpen = false;
|
||||
var canClose = false;
|
||||
|
||||
if (ch is Asterisk)
|
||||
{
|
||||
canOpen = leftFlanking;
|
||||
canClose = rightFlanking;
|
||||
}
|
||||
|
||||
if (ch is Underscore)
|
||||
{
|
||||
canClose = rightFlanking && (!leftFlanking || next.IsPunctuation());
|
||||
canOpen = leftFlanking && (!rightFlanking || prev.IsPunctuation());
|
||||
}
|
||||
|
||||
var delimiter = new Delimiter
|
||||
{
|
||||
Node = node,
|
||||
Char = ch,
|
||||
Length = buffer.Length,
|
||||
Position = index,
|
||||
CanClose = canClose,
|
||||
CanOpen = canOpen
|
||||
};
|
||||
|
||||
delimiters.Add(delimiter);
|
||||
inlines.Add(node);
|
||||
buffer.Clear();
|
||||
}
|
||||
|
||||
newIndex = position;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool RightFlanking(char prev, char next)
|
||||
{
|
||||
/*
|
||||
that is (1) not preceded by Unicode whitespace, and either (2a) not preceded by a Unicode punctuation character,
|
||||
or (2b) preceded by a Unicode punctuation character and followed by Unicode whitespace or a Unicode punctuation character.
|
||||
*/
|
||||
|
||||
return !prev.IsNullOrWhiteSpace() && (!prev.IsPunctuation() || next.IsNullOrWhiteSpace() || next.IsPunctuation());
|
||||
}
|
||||
|
||||
private static bool LeftFlanking(char prev, char next)
|
||||
{
|
||||
/*
|
||||
that is (1) not followed by Unicode whitespace, and either (2a) not followed by a Unicode punctuation character,
|
||||
or (2b) followed by a Unicode punctuation character and preceded by Unicode whitespace or a Unicode punctuation character.
|
||||
*/
|
||||
|
||||
return !next.IsNullOrWhiteSpace() && (!next.IsPunctuation() || prev.IsNullOrWhiteSpace() || prev.IsPunctuation());
|
||||
}
|
||||
|
||||
public static List<Inline> Parse(string text, Dictionary<string, LinkReference> linkReferences)
|
||||
{
|
||||
var parser = new InlineParser();
|
||||
|
||||
return parser.ParseInlines(text.Trim(), linkReferences);
|
||||
}
|
||||
|
||||
private static readonly Regex EmailRegex = new(@"^([a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)");
|
||||
|
||||
private bool TryParseAutoLink(string text, int index, out int newIndex)
|
||||
{
|
||||
newIndex = index;
|
||||
|
||||
if (text[index] is not OpenAngleBracket)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var destination = new StringBuilder();
|
||||
|
||||
var position = index + 1;
|
||||
|
||||
while (position < text.Length && text[position] is not CloseAngleBracket)
|
||||
{
|
||||
destination.Append(text[position]);
|
||||
position++;
|
||||
}
|
||||
|
||||
if (position >= text.Length || text[position] is not CloseAngleBracket)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var url = destination.ToString();
|
||||
|
||||
if (url.Contains(Space))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var content = url;
|
||||
|
||||
if (EmailRegex.IsMatch(url))
|
||||
{
|
||||
url = $"mailto:{url}";
|
||||
}
|
||||
else if (!Uri.TryCreate(url, UriKind.Absolute, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var link = new Link { Destination = url };
|
||||
|
||||
link.Add(new Text(content));
|
||||
|
||||
inlines.Add(link);
|
||||
|
||||
newIndex = position + 1;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private List<Inline> ParseInlines(string text, Dictionary<string, LinkReference> references)
|
||||
{
|
||||
var index = 0;
|
||||
|
||||
while (index < text.Length)
|
||||
{
|
||||
if (TryParseHtml(text, index, out index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryParseAutoLink(text, index, out index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryParseCode(text, index, out index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryParseLineBreak(text, index, out index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryParseSoftLineBreak(text, index, out index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
char next = index < text.Length - 1 ? text[index + 1] : Null;
|
||||
|
||||
if (TryParseBackslash(text, index, next, out index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
char prev = index > 0 ? text[index - 1] : Null;
|
||||
|
||||
if (TryParseDelimiter(text, index, next, prev, out index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryParseLinkFromReference(text, index, references, out index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryParseLinkOrImage(text, index, out index))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
buffer.Append(text[index]);
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
AddTextNode();
|
||||
|
||||
ParseEmphasisAndStrong();
|
||||
|
||||
NormalizeText();
|
||||
|
||||
return inlines;
|
||||
}
|
||||
|
||||
private void NormalizeText()
|
||||
{
|
||||
if (inlines.Count > 0)
|
||||
{
|
||||
if (inlines[0] is Text first)
|
||||
{
|
||||
first.Value = first.Value.TrimStart();
|
||||
}
|
||||
|
||||
if (inlines[^1] is Text last)
|
||||
{
|
||||
last.Value = last.Value.TrimEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryParseSoftLineBreak(string text, int index, out int newIndex)
|
||||
{
|
||||
newIndex = index;
|
||||
|
||||
if (TryParseNewLine(text, index, out var position))
|
||||
{
|
||||
AddTextNode(trim: true);
|
||||
|
||||
inlines.Add(new SoftLineBreak());
|
||||
|
||||
newIndex = position;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryParseLineBreak(string text, int index, out int newIndex)
|
||||
{
|
||||
newIndex = index;
|
||||
|
||||
if (text[index] is not Space && text[index] is not Backslash)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var position = index + 1;
|
||||
|
||||
if (position < text.Length && text[position] is Space)
|
||||
{
|
||||
while (position < text.Length && text[position] is Space)
|
||||
{
|
||||
position++;
|
||||
}
|
||||
}
|
||||
|
||||
if (text[index] is Space && position == index + 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (position < text.Length && TryParseNewLine(text, position, out position))
|
||||
{
|
||||
AddTextNode(trim: true);
|
||||
|
||||
inlines.Add(new LineBreak());
|
||||
|
||||
newIndex = position;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryParseNewLine(string text, int position, out int newIndex)
|
||||
{
|
||||
newIndex = position;
|
||||
|
||||
if (position >= text.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (text[position] is LineFeed)
|
||||
{
|
||||
newIndex = position + 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (text[position] is CarrigeReturn && position < text.Length - 1 && text[position + 1] is LineFeed)
|
||||
{
|
||||
newIndex = position + 2;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryParseHtml(string text, int index, out int newIndex)
|
||||
{
|
||||
newIndex = index;
|
||||
|
||||
var match = BlockParser.HtmlRegex.Match(text[index..]);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
AddTextNode();
|
||||
|
||||
var value = text[index..(index + match.Length)];
|
||||
|
||||
inlines.Add(new HtmlInline { Value = value });
|
||||
|
||||
newIndex = index + match.Length;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static bool TryParseDestinationAndTitle(string text, int position, out string destination, out string title, out int newPosition)
|
||||
{
|
||||
newPosition = position;
|
||||
destination = string.Empty;
|
||||
title = string.Empty;
|
||||
|
||||
// Skip whitespace
|
||||
while (position < text.Length && text[position] is Space or LineFeed)
|
||||
{
|
||||
position++;
|
||||
}
|
||||
|
||||
if (position >= text.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse destination
|
||||
var destinationBuilder = new StringBuilder();
|
||||
|
||||
var angleBrackets = position < text.Length && text[position] is OpenAngleBracket;
|
||||
|
||||
if (angleBrackets)
|
||||
{
|
||||
position++;
|
||||
}
|
||||
|
||||
var parentheses = 0;
|
||||
|
||||
while (position < text.Length)
|
||||
{
|
||||
var ch = text[position];
|
||||
var prev = position > 0 ? text[position - 1] : Null;
|
||||
var next = position < text.Length - 1 ? text[position + 1] : Null;
|
||||
|
||||
if (angleBrackets && ch is CloseAngleBracket && prev is not Backslash)
|
||||
{
|
||||
position++;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!angleBrackets)
|
||||
{
|
||||
if (ch is OpenParenthesis && prev is not Backslash)
|
||||
{
|
||||
parentheses++;
|
||||
}
|
||||
else if (ch is CloseParenthesis && prev is not Backslash)
|
||||
{
|
||||
if (parentheses == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
parentheses--;
|
||||
}
|
||||
|
||||
if (ch is Space or LineFeed)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ch is Backslash && next.IsPunctuation())
|
||||
{
|
||||
position++;
|
||||
continue;
|
||||
}
|
||||
|
||||
destinationBuilder.Append(ch);
|
||||
position++;
|
||||
}
|
||||
|
||||
if (angleBrackets)
|
||||
{
|
||||
// Skip whitespace after angle brackets
|
||||
while (position < text.Length && text[position] is Space)
|
||||
{
|
||||
position++;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse title if present
|
||||
var titleBuilder = new StringBuilder();
|
||||
if (position < text.Length && text[position].IsNullOrWhiteSpace())
|
||||
{
|
||||
var lines = 0;
|
||||
|
||||
while (position < text.Length && text[position].IsNullOrWhiteSpace())
|
||||
{
|
||||
if (text[position] is LineFeed)
|
||||
{
|
||||
lines++;
|
||||
}
|
||||
|
||||
position++;
|
||||
}
|
||||
|
||||
var titleStart = position;
|
||||
|
||||
if (position < text.Length)
|
||||
{
|
||||
var titleDelimiter = text[position];
|
||||
|
||||
if (titleDelimiter is Quote or SingleQuote or OpenParenthesis)
|
||||
{
|
||||
position++; // Skip opening delimiter
|
||||
|
||||
char closingDelimiter = titleDelimiter is OpenParenthesis ? CloseParenthesis : titleDelimiter;
|
||||
|
||||
while (position < text.Length && (text[position] != closingDelimiter || text[position - 1] is Backslash))
|
||||
{
|
||||
if (text[position] is LineFeed && titleBuilder.Length > 0 && titleBuilder[^1] is LineFeed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
titleBuilder.Append(text[position]);
|
||||
position++;
|
||||
}
|
||||
|
||||
if (position < text.Length && text[position] == closingDelimiter)
|
||||
{
|
||||
position++; // Skip closing delimiter
|
||||
|
||||
// Skip whitespace after title
|
||||
while (position < text.Length && text[position] is Space)
|
||||
{
|
||||
position++;
|
||||
}
|
||||
|
||||
if (position < text.Length)
|
||||
{
|
||||
if (position < text.Length && text[position] is not (CloseParenthesis or LineFeed))
|
||||
{
|
||||
if (lines > 0)
|
||||
{
|
||||
// non-white space characters after title
|
||||
newPosition = titleStart;
|
||||
destination = destinationBuilder.ToString();
|
||||
title = string.Empty;
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (position < text.Length)
|
||||
{
|
||||
if (text[position] is LineFeed)
|
||||
{
|
||||
position++;
|
||||
}
|
||||
else if (text[position] is Quote or SingleQuote or OpenParenthesis)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
destination = destinationBuilder.ToString();
|
||||
title = titleBuilder.ToString();
|
||||
newPosition = position;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryParseLinkOrImage(string text, int index, out int newIndex)
|
||||
{
|
||||
newIndex = index;
|
||||
|
||||
if (!TryGetOpenerIndex(text, index, out var openerIndex, out var position))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryParseDestinationAndTitle(text, position, out var destination, out var title, out position))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (position >= text.Length || text[position] is not CloseParenthesis)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var opener = delimiters[openerIndex];
|
||||
|
||||
InlineContainer container = opener.Char == Exclamation ? new Image { Destination = destination, Title = title } : new Link { Destination = destination, Title = title };
|
||||
|
||||
ReplaceOpener(openerIndex, container);
|
||||
|
||||
newIndex = position + 1;
|
||||
|
||||
if (container is Link)
|
||||
{
|
||||
for (var delimiterIndex = 0; delimiterIndex < openerIndex; delimiterIndex++)
|
||||
{
|
||||
if (delimiters[delimiterIndex].Char == OpenBracket)
|
||||
{
|
||||
delimiters[delimiterIndex].Active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delimiters.Remove(opener);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private bool TryGetOpenerIndex(string text, int index, out int openerIndex, out int position)
|
||||
{
|
||||
position = index;
|
||||
openerIndex = -1;
|
||||
|
||||
if (text[index] is not CloseBracket)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var di = delimiters.Count - 1;
|
||||
|
||||
while (di >= 0)
|
||||
{
|
||||
var delimiter = delimiters[di];
|
||||
|
||||
if ((delimiter.Active && delimiter.Char is OpenBracket) || delimiter.Char is Exclamation)
|
||||
{
|
||||
openerIndex = di;
|
||||
break;
|
||||
}
|
||||
|
||||
di--;
|
||||
}
|
||||
|
||||
if (di < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
AddTextNode();
|
||||
|
||||
position = index + 1;
|
||||
|
||||
// Skip if not followed by opening parenthesis
|
||||
if (position >= text.Length || text[position] is not OpenParenthesis)
|
||||
{
|
||||
delimiters.RemoveAt(openerIndex);
|
||||
return false;
|
||||
}
|
||||
|
||||
position++; // Skip opening parenthesis
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ReplaceOpener(int openerIndex, InlineContainer parent)
|
||||
{
|
||||
var startIndex = inlines.FindIndex(delimiters[openerIndex].Node.Equals);
|
||||
|
||||
ParseEmphasisAndStrong(openerIndex);
|
||||
|
||||
var endIndex = inlines.Count - startIndex;
|
||||
|
||||
var children = inlines.GetRange(startIndex + 1, endIndex - 1);
|
||||
|
||||
inlines.RemoveRange(startIndex, endIndex);
|
||||
|
||||
foreach (var child in children)
|
||||
{
|
||||
parent.Add(child);
|
||||
}
|
||||
|
||||
inlines.Insert(startIndex, parent);
|
||||
}
|
||||
|
||||
|
||||
private bool TryParseLinkFromReference(string text, int index, Dictionary<string, LinkReference> references, out int newIndex)
|
||||
{
|
||||
newIndex = index;
|
||||
|
||||
if (references.Count == 0 || text[index] is not CloseBracket)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var openerIndex = FindOpenBracketIndex(OpenBracket);
|
||||
|
||||
if (openerIndex < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
AddTextNode();
|
||||
|
||||
var startIndex = inlines.FindIndex(delimiters[openerIndex].Node.Equals);
|
||||
|
||||
var endIndex = inlines.Count - startIndex;
|
||||
|
||||
var children = inlines.GetRange(startIndex + 1, endIndex - 1);
|
||||
|
||||
var id = new StringBuilder();
|
||||
|
||||
foreach (var child in children)
|
||||
{
|
||||
if (child is Text textNode)
|
||||
{
|
||||
id.Append(textNode.Value);
|
||||
}
|
||||
}
|
||||
|
||||
if (!references.TryGetValue(id.ToString().ToLowerInvariant(), out var reference))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var link = new Link { Destination = reference.Destination, Title = reference.Title };
|
||||
|
||||
foreach (var child in children)
|
||||
{
|
||||
link.Add(child);
|
||||
}
|
||||
|
||||
inlines.RemoveRange(startIndex, endIndex);
|
||||
inlines.Insert(startIndex, link);
|
||||
|
||||
link.Destination = reference.Destination;
|
||||
link.Title = reference.Title;
|
||||
newIndex = index + 1;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private int FindOpenBracketIndex(char ch)
|
||||
{
|
||||
for (var index = delimiters.Count - 1; index >= 0; index--)
|
||||
{
|
||||
if (delimiters[index].Char == ch)
|
||||
{
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private void ParseEmphasisAndStrong(int index = -1)
|
||||
{
|
||||
var closerIndex = 0;
|
||||
|
||||
while ((closerIndex = FindCloserIndex()) > 0)
|
||||
{
|
||||
var openerIndex = FindOpenerIndex(closerIndex, index);
|
||||
|
||||
if (openerIndex >= 0)
|
||||
{
|
||||
var closer = delimiters[closerIndex];
|
||||
var opener = delimiters[openerIndex];
|
||||
var startIndex = inlines.FindIndex(opener.Node.Equals);
|
||||
var endIndex = inlines.FindIndex(closer.Node.Equals);
|
||||
|
||||
if (startIndex >= 0 && endIndex >= 0)
|
||||
{
|
||||
var innerInlines = inlines.GetRange(startIndex + 1, endIndex - startIndex - 1);
|
||||
|
||||
var charsToConsume = closer.Length == opener.Length && closer.Length > 1 ? 2 : 1;
|
||||
|
||||
InlineContainer parent = charsToConsume == 1 ? new Emphasis() : new Strong();
|
||||
|
||||
foreach (var child in innerInlines)
|
||||
{
|
||||
parent.Add(child);
|
||||
}
|
||||
|
||||
opener.Length -= charsToConsume;
|
||||
|
||||
if (opener.Length > 0)
|
||||
{
|
||||
opener.Node.Value = opener.Node.Value[..^charsToConsume];
|
||||
startIndex += charsToConsume;
|
||||
}
|
||||
|
||||
closer.Length -= charsToConsume;
|
||||
|
||||
if (closer.Length > 0)
|
||||
{
|
||||
closer.Node.Value = closer.Node.Value[..^charsToConsume];
|
||||
endIndex -= charsToConsume;
|
||||
}
|
||||
|
||||
inlines.RemoveRange(startIndex, endIndex - startIndex + 1);
|
||||
|
||||
inlines.Insert(startIndex, parent);
|
||||
}
|
||||
|
||||
delimiters.RemoveAt(closerIndex);
|
||||
delimiters.RemoveAt(openerIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int FindCloserIndex()
|
||||
{
|
||||
for (var index = 1; index < delimiters.Count; index++)
|
||||
{
|
||||
var delimiter = delimiters[index];
|
||||
|
||||
if (delimiter.CanClose && (delimiter.Char is Asterisk or Underscore))
|
||||
{
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private int FindOpenerIndex(int startIndex, int endIndex)
|
||||
{
|
||||
var closer = delimiters[startIndex];
|
||||
|
||||
for (var index = startIndex - 1; index > endIndex; index--)
|
||||
{
|
||||
var delimiter = delimiters[index];
|
||||
|
||||
if (delimiter.CanOpen && delimiter.Char == closer.Char)
|
||||
{
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
22
Radzen.Blazor/Markdown/InlineVisitor.cs
Normal file
22
Radzen.Blazor/Markdown/InlineVisitor.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
class InlineVisitor(Dictionary<string, LinkReference> references) : NodeVisitorBase
|
||||
{
|
||||
public override void VisitHeading(Heading heading) => ParseChildren(heading, references);
|
||||
|
||||
private static void ParseChildren(IBlockInlineContainer node, Dictionary<string, LinkReference> references)
|
||||
{
|
||||
var inlines = InlineParser.Parse(node.Value, references);
|
||||
|
||||
foreach (var inline in inlines)
|
||||
{
|
||||
node.Add(inline);
|
||||
}
|
||||
}
|
||||
|
||||
public override void VisitParagraph(Paragraph paragraph) => ParseChildren(paragraph, references);
|
||||
|
||||
public override void VisitTableCell(TableCell cell) => ParseChildren(cell, references);
|
||||
}
|
||||
42
Radzen.Blazor/Markdown/Leaf.cs
Normal file
42
Radzen.Blazor/Markdown/Leaf.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for markdown leaf block nodes.
|
||||
/// </summary>
|
||||
public abstract class Leaf : Block, IBlockInlineContainer
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the value of the leaf node.
|
||||
/// </summary>
|
||||
public string Value { get; set; } = string.Empty;
|
||||
|
||||
private readonly List<Inline> children = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the children of the leaf node.
|
||||
/// </summary>
|
||||
public IReadOnlyList<Inline> Children => children;
|
||||
|
||||
/// <summary>
|
||||
/// Appends a child to the leaf node.
|
||||
/// </summary>
|
||||
public void Add(Inline node)
|
||||
{
|
||||
children.Add(node);
|
||||
}
|
||||
internal void AddLine(BlockParser blockParser)
|
||||
{
|
||||
if (blockParser.PartiallyConsumedTab)
|
||||
{
|
||||
blockParser.Offset += 1;
|
||||
|
||||
var charsToTab = 4 - (blockParser.Column % 4);
|
||||
|
||||
Value += new string(' ', charsToTab);
|
||||
}
|
||||
|
||||
Value += blockParser.CurrentLine[blockParser.Offset..] + "\n";
|
||||
}
|
||||
}
|
||||
13
Radzen.Blazor/Markdown/LineBreak.cs
Normal file
13
Radzen.Blazor/Markdown/LineBreak.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a line break node. Line breaks are usually empty lines and are used to separate paragraphs.
|
||||
/// </summary>
|
||||
public class LineBreak : Inline
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void Accept(INodeVisitor visitor)
|
||||
{
|
||||
visitor.VisitLineBreak(this);
|
||||
}
|
||||
}
|
||||
23
Radzen.Blazor/Markdown/Link.cs
Normal file
23
Radzen.Blazor/Markdown/Link.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a link element: <c>[Link text](/path/to/page "Optional title")</c>
|
||||
/// </summary>
|
||||
public class Link : InlineContainer
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the destination (URL) of the link.
|
||||
/// </summary>
|
||||
public string Destination { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the link title.
|
||||
/// </summary>
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Accept(INodeVisitor visitor)
|
||||
{
|
||||
visitor.VisitLink(this);
|
||||
}
|
||||
}
|
||||
7
Radzen.Blazor/Markdown/LinkReference.cs
Normal file
7
Radzen.Blazor/Markdown/LinkReference.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
class LinkReference
|
||||
{
|
||||
public string Destination { get; set; }
|
||||
public string Title { get; set; }
|
||||
}
|
||||
46
Radzen.Blazor/Markdown/LinkReferenceParser.cs
Normal file
46
Radzen.Blazor/Markdown/LinkReferenceParser.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
class LinkReferenceParser(BlockParser parser) : NodeVisitorBase
|
||||
{
|
||||
private readonly List<Block> emptyNodes = [];
|
||||
|
||||
public override void VisitParagraph(Paragraph paragraph)
|
||||
{
|
||||
var hasReferenceDefs = false;
|
||||
// Try parsing the beginning as link reference definitions;
|
||||
// Note that link reference definitions must be the beginning of a
|
||||
// paragraph node since link reference definitions cannot interrupt
|
||||
// paragraphs.
|
||||
while (paragraph.Value.Peek() == '[' && parser.TryParseLinkReference(paragraph.Value, out var position))
|
||||
{
|
||||
var removedText = paragraph.Value[..position];
|
||||
|
||||
paragraph.Value = paragraph.Value[position..];
|
||||
hasReferenceDefs = true;
|
||||
|
||||
var lines = removedText.Split('\n');
|
||||
|
||||
// -1 for final newline.
|
||||
paragraph.Range.Start.Line += lines.Length - 1;
|
||||
}
|
||||
|
||||
if (hasReferenceDefs && string.IsNullOrWhiteSpace(paragraph.Value))
|
||||
{
|
||||
emptyNodes.Add(paragraph);
|
||||
}
|
||||
}
|
||||
|
||||
public static void Parse(BlockParser parser, Document document)
|
||||
{
|
||||
var visitor = new LinkReferenceParser(parser);
|
||||
|
||||
document.Accept(visitor);
|
||||
|
||||
foreach (var node in visitor.emptyNodes)
|
||||
{
|
||||
node.Remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
74
Radzen.Blazor/Markdown/ListBase.cs
Normal file
74
Radzen.Blazor/Markdown/ListBase.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for list elements (ordered and unordered).
|
||||
/// </summary>
|
||||
public abstract class List : BlockContainer
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the list marker.
|
||||
/// </summary>
|
||||
public char Marker { get; set; }
|
||||
|
||||
internal int MarkerOffset { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the list is tight. Tight lists have no space between their items.
|
||||
/// </summary>
|
||||
public bool Tight { get; set; } = true;
|
||||
internal int Padding { get; set; }
|
||||
internal string Delimiter { get; set; }
|
||||
|
||||
internal override BlockMatch Matches(BlockParser parser)
|
||||
{
|
||||
return BlockMatch.Match;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanContain(Block node)
|
||||
{
|
||||
return node is ListItem;
|
||||
}
|
||||
|
||||
private static bool EndsWithBlankLine(Block block)
|
||||
{
|
||||
return block.Next != null && block.Range.End.Line != block.Next.Range.Start.Line - 1;
|
||||
}
|
||||
|
||||
internal override void Close(BlockParser parser)
|
||||
{
|
||||
base.Close(parser);
|
||||
|
||||
var item = FirstChild;
|
||||
|
||||
while (item != null)
|
||||
{
|
||||
// check for non-final list item ending with blank line:
|
||||
if (item.Next != null && EndsWithBlankLine(item))
|
||||
{
|
||||
Tight = false;
|
||||
break;
|
||||
}
|
||||
// recurse into children of list item, to see if there are
|
||||
// spaces between any of them:
|
||||
var subitem = item.FirstChild;
|
||||
|
||||
while (subitem != null)
|
||||
{
|
||||
if (subitem.Next != null && EndsWithBlankLine(subitem))
|
||||
{
|
||||
Tight = false;
|
||||
break;
|
||||
}
|
||||
subitem = subitem.Next;
|
||||
}
|
||||
|
||||
item = item.Next;
|
||||
}
|
||||
|
||||
if (LastChild != null)
|
||||
{
|
||||
Range.End = LastChild.Range.End;
|
||||
}
|
||||
}
|
||||
}
|
||||
199
Radzen.Blazor/Markdown/ListItem.cs
Normal file
199
Radzen.Blazor/Markdown/ListItem.cs
Normal file
@@ -0,0 +1,199 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
#nullable enable
|
||||
|
||||
/// <summary>
|
||||
/// Represents a list item node.
|
||||
/// </summary>
|
||||
public class ListItem : BlockContainer
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void Accept(INodeVisitor visitor)
|
||||
{
|
||||
visitor.VisitListItem(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanContain(Block node)
|
||||
{
|
||||
return node is not ListItem;
|
||||
}
|
||||
|
||||
internal override BlockMatch Matches(BlockParser parser)
|
||||
{
|
||||
if (parser.Blank)
|
||||
{
|
||||
if (Children.Count == 0)
|
||||
{
|
||||
// Blank line after empty list item
|
||||
return BlockMatch.Skip;
|
||||
}
|
||||
else
|
||||
{
|
||||
parser.AdvanceNextNonSpace();
|
||||
}
|
||||
}
|
||||
else if (parser.Indent >= data.MarkerOffset + data.Padding)
|
||||
{
|
||||
parser.AdvanceOffset(data.MarkerOffset + data.Padding, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
return BlockMatch.Skip;
|
||||
}
|
||||
|
||||
return BlockMatch.Match;
|
||||
}
|
||||
|
||||
internal override void Close(BlockParser parser)
|
||||
{
|
||||
base.Close(parser);
|
||||
|
||||
if (LastChild != null)
|
||||
{
|
||||
Range.End = LastChild.Range.End;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Empty list item
|
||||
Range.End.Line = Range.Start.Line;
|
||||
|
||||
if (Parent is List list)
|
||||
{
|
||||
Range.End.Column = list.MarkerOffset + list.Padding;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static BlockStart Start(BlockParser parser, Block container)
|
||||
{
|
||||
if ((!parser.Indented || container is List) && TryParseListMarker(parser, container, out var data))
|
||||
{
|
||||
parser.CloseUnmatchedBlocks();
|
||||
|
||||
var list = container as List ?? (container.Parent is ListItem item ? item.data : null);
|
||||
|
||||
// add the list if needed
|
||||
if (parser.Tip is not List || !ListsMatch(list, data))
|
||||
{
|
||||
parser.AddChild(data, parser.NextNonSpace);
|
||||
}
|
||||
|
||||
var node = parser.AddChild<ListItem>(parser.NextNonSpace);
|
||||
node.data = data;
|
||||
|
||||
return BlockStart.Container;
|
||||
}
|
||||
|
||||
return BlockStart.Skip;
|
||||
}
|
||||
|
||||
private List data = null!;
|
||||
|
||||
private static readonly Regex UnorderedMarkerRegex = new(@"^[*+-]");
|
||||
|
||||
private static readonly Regex OrderedMarkerRegex = new(@"^(\d{1,9})([.)])");
|
||||
|
||||
private static bool TryParseListMarker(BlockParser parser, Block container, out List data)
|
||||
{
|
||||
data = null!;
|
||||
|
||||
if (parser.Indent >= 4)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var rest = parser.CurrentLine[parser.NextNonSpace..];
|
||||
|
||||
var match = UnorderedMarkerRegex.Match(rest);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
data = new UnorderedList
|
||||
{
|
||||
Marker = match.Value[0],
|
||||
MarkerOffset = parser.Indent
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
match = OrderedMarkerRegex.Match(rest);
|
||||
|
||||
if (match.Success && (container is not Paragraph || match.Groups[1].Value == "1"))
|
||||
{
|
||||
var list = new OrderedList
|
||||
{
|
||||
MarkerOffset = parser.Indent,
|
||||
Start = int.Parse(match.Groups[1].Value),
|
||||
Delimiter = match.Groups[2].Value
|
||||
};
|
||||
data = list;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// make sure we have spaces after
|
||||
var ch = parser.PeekNonSpace(match.Length);
|
||||
|
||||
if (ch != default && !ch.IsSpaceOrTab())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// if it interrupts paragraph, make sure first line isn't blank
|
||||
if (container is Paragraph && string.IsNullOrWhiteSpace(parser.CurrentLine[(parser.NextNonSpace + match.Length)..]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// we've got a match! advance offset and calculate padding
|
||||
parser.AdvanceNextNonSpace(); // to start of marker
|
||||
parser.AdvanceOffset(match.Length, true); // to end of marker
|
||||
|
||||
var startColumn = parser.Column;
|
||||
var startOffset = parser.Offset;
|
||||
|
||||
do
|
||||
{
|
||||
parser.AdvanceOffset(1, true);
|
||||
ch = parser.Peek();
|
||||
} while (parser.Column - startColumn < 5 && ch.IsSpaceOrTab());
|
||||
|
||||
var blank = parser.Peek() == default;
|
||||
|
||||
var spacesAfterMarker = parser.Column - startColumn;
|
||||
|
||||
if (spacesAfterMarker >= 5 || spacesAfterMarker < 1 || blank)
|
||||
{
|
||||
data.Padding = match.Length + 1;
|
||||
parser.Column = startColumn;
|
||||
parser.Offset = startOffset;
|
||||
|
||||
if (parser.Peek().IsSpaceOrTab())
|
||||
{
|
||||
parser.AdvanceOffset(1, true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
data.Padding = match.Length + spacesAfterMarker;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool ListsMatch(List? x, List y)
|
||||
{
|
||||
if (x == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return x.GetType() == y.GetType() && x.Marker == y.Marker && x.Delimiter == y.Delimiter;
|
||||
}
|
||||
}
|
||||
17
Radzen.Blazor/Markdown/MarkdownParser.cs
Normal file
17
Radzen.Blazor/Markdown/MarkdownParser.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Parses a Markdown document.
|
||||
/// </summary>
|
||||
public static class MarkdownParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses a string containing Markdown into a document.
|
||||
/// </summary>
|
||||
/// <param name="markdown">The Markdown content to parse.</param>
|
||||
/// <returns>The parsed document.</returns>
|
||||
public static Document Parse(string markdown)
|
||||
{
|
||||
return BlockParser.Parse(markdown);
|
||||
}
|
||||
}
|
||||
159
Radzen.Blazor/Markdown/NodeVisitorBase.cs
Normal file
159
Radzen.Blazor/Markdown/NodeVisitorBase.cs
Normal file
@@ -0,0 +1,159 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for visitors that traverse a Markdown document.
|
||||
/// </summary>
|
||||
public abstract class NodeVisitorBase : INodeVisitor
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Visits a block quote by visiting its children.
|
||||
/// </summary>
|
||||
public virtual void VisitBlockQuote(BlockQuote blockQuote) => VisitChildren(blockQuote.Children);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a document by visiting its children.
|
||||
/// </summary>
|
||||
public virtual void VisitDocument(Document document) => VisitChildren(document.Children);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a heading by visiting its children.
|
||||
/// </summary>
|
||||
public virtual void VisitHeading(Heading heading) => VisitChildren(heading.Children);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a list item by visiting its children.
|
||||
/// </summary>
|
||||
public virtual void VisitListItem(ListItem listItem) => VisitChildren(listItem.Children);
|
||||
|
||||
/// <summary>
|
||||
/// Visits an ordered list by visiting its children.
|
||||
/// </summary>
|
||||
public virtual void VisitOrderedList(OrderedList orderedList) => VisitChildren(orderedList.Children);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a paragraph by visiting its children.
|
||||
/// </summary>
|
||||
public virtual void VisitParagraph(Paragraph paragraph) => VisitChildren(paragraph.Children);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a thematic break.
|
||||
/// </summary>
|
||||
public virtual void VisitThematicBreak(ThematicBreak thematicBreak)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Visits a text node.
|
||||
/// </summary>
|
||||
public virtual void VisitText(Text text)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Visits a code node.
|
||||
/// </summary>
|
||||
public virtual void VisitCode(Code code)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Visits an HTML block.
|
||||
/// </summary>
|
||||
public virtual void VisitHtmlInline(HtmlInline html)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Visits a line break.
|
||||
/// </summary>
|
||||
public virtual void VisitLineBreak(LineBreak lineBreak)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Visits a soft line break.
|
||||
/// </summary>
|
||||
public virtual void VisitSoftLineBreak(SoftLineBreak softLineBreak)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Visits an ordered list by visiting its children.
|
||||
/// </summary>
|
||||
public virtual void VisitUnorderedList(UnorderedList unorderedList) => VisitChildren(unorderedList.Children);
|
||||
|
||||
/// <summary>
|
||||
/// Visits an emphasis by visiting its children.
|
||||
/// </summary>
|
||||
public virtual void VisitEmphasis(Emphasis emphasis) => VisitChildren(emphasis.Children);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a strong by visiting its children.
|
||||
/// </summary>
|
||||
public virtual void VisitStrong(Strong strong) => VisitChildren(strong.Children);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a link by visiting its children.
|
||||
/// </summary>
|
||||
public virtual void VisitLink(Link link) => VisitChildren(link.Children);
|
||||
|
||||
/// <summary>
|
||||
/// Visits an image by visiting its children.
|
||||
/// </summary>
|
||||
public virtual void VisitImage(Image image) => VisitChildren(image.Children);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a code block.
|
||||
/// </summary>
|
||||
public virtual void VisitIndentedCodeBlock(IndentedCodeBlock codeBlock)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Visits a fenced code block.
|
||||
/// </summary>
|
||||
public virtual void VisitFencedCodeBlock(FencedCodeBlock fencedCodeBlock)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Visits an HTML block.
|
||||
/// </summary>
|
||||
public virtual void VisitHtmlBlock(HtmlBlock htmlBlock)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Visits a table.
|
||||
/// </summary>
|
||||
public virtual void VisitTable(Table table) => VisitChildren(table.Rows);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a table header row by visiting its children.
|
||||
/// </summary>
|
||||
public virtual void VisitTableHeaderRow(TableHeaderRow header) => VisitChildren(header.Cells);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a table row by visiting its children.
|
||||
/// </summary>
|
||||
public virtual void VisitTableRow(TableRow row) => VisitChildren(row.Cells);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a table cell by visiting its children.
|
||||
/// </summary>
|
||||
public virtual void VisitTableCell(TableCell cell) => VisitChildren(cell.Children);
|
||||
|
||||
/// <summary>
|
||||
/// Visits a collection of nodes.
|
||||
/// </summary>
|
||||
protected void VisitChildren(IEnumerable<INode> children)
|
||||
{
|
||||
foreach (var node in children)
|
||||
{
|
||||
node.Accept(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
18
Radzen.Blazor/Markdown/OrderedList.cs
Normal file
18
Radzen.Blazor/Markdown/OrderedList.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an ordered list: <c>1. item</c>.
|
||||
/// </summary>
|
||||
public class OrderedList : List
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void Accept(INodeVisitor visitor)
|
||||
{
|
||||
visitor.VisitOrderedList(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the start number of the ordered list.
|
||||
/// </summary>
|
||||
public int Start { get; set; }
|
||||
}
|
||||
19
Radzen.Blazor/Markdown/Paragraph.cs
Normal file
19
Radzen.Blazor/Markdown/Paragraph.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a paragraph node.
|
||||
/// </summary>
|
||||
public class Paragraph : Leaf
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void Accept(INodeVisitor visitor)
|
||||
{
|
||||
visitor.VisitParagraph(this);
|
||||
}
|
||||
|
||||
internal override BlockMatch Matches(BlockParser parser)
|
||||
{
|
||||
return parser.Blank ? BlockMatch.Skip : BlockMatch.Match;
|
||||
}
|
||||
}
|
||||
50
Radzen.Blazor/Markdown/SetExtHeading.cs
Normal file
50
Radzen.Blazor/Markdown/SetExtHeading.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a setext heading node. Setext headings are headings that are underlined with equal signs for level 1 headings and dashes for level 2 headings.
|
||||
/// </summary>
|
||||
public class SetExtHeading : Heading
|
||||
{
|
||||
private static readonly Regex HeadingRegex = new (@"^(?:=+|-+)[ \t]*$");
|
||||
|
||||
internal static BlockStart Start(BlockParser parser, Block block)
|
||||
{
|
||||
if (parser.Indented || block is not Paragraph paragraph)
|
||||
{
|
||||
return BlockStart.Skip;
|
||||
}
|
||||
|
||||
var line = parser.CurrentLine[parser.NextNonSpace..];
|
||||
|
||||
var match = HeadingRegex.Match(line);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
parser.CloseUnmatchedBlocks();
|
||||
|
||||
// resolve reference links
|
||||
while (paragraph.Value.Peek() == '[' && parser.TryParseLinkReference(paragraph.Value, out var position))
|
||||
{
|
||||
paragraph.Value = paragraph.Value[position..];
|
||||
}
|
||||
|
||||
if (paragraph.Value.Length > 0)
|
||||
{
|
||||
var heading = new SetExtHeading
|
||||
{
|
||||
Level = match.Value[0] == '=' ? 1 : 2,
|
||||
Value = paragraph.Value
|
||||
};
|
||||
paragraph.Parent.Replace(paragraph, heading);
|
||||
parser.Tip = heading;
|
||||
parser.AdvanceOffset(parser.CurrentLine.Length - parser.Offset, false);
|
||||
|
||||
return BlockStart.Leaf;
|
||||
}
|
||||
}
|
||||
|
||||
return BlockStart.Skip;
|
||||
}
|
||||
}
|
||||
13
Radzen.Blazor/Markdown/SoftLineBreak.cs
Normal file
13
Radzen.Blazor/Markdown/SoftLineBreak.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a soft line break node. Soft line breaks are usually used to separate lines in a paragraph.
|
||||
/// </summary>
|
||||
public class SoftLineBreak : Inline
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void Accept(INodeVisitor visitor)
|
||||
{
|
||||
visitor.VisitSoftLineBreak(this);
|
||||
}
|
||||
}
|
||||
9
Radzen.Blazor/Markdown/StringExtensions.cs
Normal file
9
Radzen.Blazor/Markdown/StringExtensions.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
static class StringExtensions
|
||||
{
|
||||
public static char Peek(this string line, int offset = 0)
|
||||
{
|
||||
return offset < line.Length ? line[offset] : default;
|
||||
}
|
||||
}
|
||||
13
Radzen.Blazor/Markdown/Strong.cs
Normal file
13
Radzen.Blazor/Markdown/Strong.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a strong node: <c>**strong**</c>.
|
||||
/// </summary>
|
||||
public class Strong : InlineContainer
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void Accept(INodeVisitor visitor)
|
||||
{
|
||||
visitor.VisitStrong(this);
|
||||
}
|
||||
}
|
||||
348
Radzen.Blazor/Markdown/Table.cs
Normal file
348
Radzen.Blazor/Markdown/Table.cs
Normal file
@@ -0,0 +1,348 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// The alignment of a table cell.
|
||||
/// </summary>
|
||||
public enum TableCellAlignment
|
||||
{
|
||||
/// <summary>
|
||||
/// No alignment specified. Default alignment is left.
|
||||
/// </summary>
|
||||
None,
|
||||
/// <summary>
|
||||
/// Left alignment.
|
||||
/// </summary>
|
||||
Left,
|
||||
/// <summary>
|
||||
/// Center alignment.
|
||||
/// </summary>
|
||||
Center,
|
||||
/// <summary>
|
||||
/// Right alignment.
|
||||
/// </summary>
|
||||
Right
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a table in a Markdown document.
|
||||
/// </summary>
|
||||
public class Table : Leaf
|
||||
{
|
||||
private readonly List<TableRow> rows = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the rows of the table.
|
||||
/// </summary>
|
||||
public IReadOnlyList<TableRow> Rows => rows;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Accept(INodeVisitor visitor)
|
||||
{
|
||||
visitor.VisitTable(this);
|
||||
}
|
||||
|
||||
private static readonly Regex DelimiterRegex = new(@"^\s*(\|?\s*:?-{1,}:?\s*)+(\|+\s*:?-{1,}:?\s*)*\|?\s*$");
|
||||
|
||||
internal override void Close(BlockParser parser)
|
||||
{
|
||||
base.Close(parser);
|
||||
|
||||
var header = rows[0];
|
||||
var headerCells = header.Cells;
|
||||
|
||||
var dataLines = Value.Split('\n');
|
||||
|
||||
foreach (var line in dataLines)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
var row = new TableRow();
|
||||
var cells = ParseRow(line);
|
||||
|
||||
// Trim excess cells
|
||||
var count = Math.Min(cells.Count, headerCells.Count);
|
||||
|
||||
for (int cellIndex = 0; cellIndex < count; cellIndex++)
|
||||
{
|
||||
var alignment = cellIndex < headerCells.Count ? headerCells[cellIndex].Alignment : TableCellAlignment.None;
|
||||
|
||||
row.Add(cells[cellIndex], alignment);
|
||||
}
|
||||
|
||||
for (int missingCellIndex = 0; missingCellIndex < header.Cells.Count - cells.Count; missingCellIndex++)
|
||||
{
|
||||
row.Add("", TableCellAlignment.None);
|
||||
}
|
||||
|
||||
rows.Add(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static List<string> ParseRow(string line)
|
||||
{
|
||||
// Remove leading and trailing pipes if present
|
||||
line = line.Trim();
|
||||
|
||||
if (line.StartsWith('|'))
|
||||
{
|
||||
line = line[1..];
|
||||
}
|
||||
if (line.EndsWith('|'))
|
||||
{
|
||||
line = line[..^1];
|
||||
}
|
||||
|
||||
// Split by pipe character, but not by escaped pipes
|
||||
var cells = new List<string>();
|
||||
var currentCell = "";
|
||||
var escaped = false;
|
||||
|
||||
for (int i = 0; i < line.Length; i++)
|
||||
{
|
||||
var c = line[i];
|
||||
|
||||
if (escaped)
|
||||
{
|
||||
// Add the escaped character (including escaped pipes)
|
||||
if (c == '|')
|
||||
{
|
||||
currentCell += '|'; // Replace \| with |
|
||||
}
|
||||
else
|
||||
{
|
||||
currentCell += $"\\{c}"; // Keep the escape character for other escaped chars
|
||||
}
|
||||
escaped = false;
|
||||
}
|
||||
else if (c == '\\')
|
||||
{
|
||||
escaped = true;
|
||||
}
|
||||
else if (c == '|')
|
||||
{
|
||||
// End of cell
|
||||
cells.Add(currentCell.Trim());
|
||||
currentCell = "";
|
||||
}
|
||||
else
|
||||
{
|
||||
currentCell += c;
|
||||
}
|
||||
}
|
||||
|
||||
// Add the last cell
|
||||
if (!string.IsNullOrEmpty(currentCell) || cells.Count > 0)
|
||||
{
|
||||
cells.Add(currentCell.Trim());
|
||||
}
|
||||
|
||||
return cells;
|
||||
}
|
||||
|
||||
internal override BlockMatch Matches(BlockParser parser)
|
||||
{
|
||||
return parser.Blank ? BlockMatch.Skip : BlockMatch.Match;
|
||||
}
|
||||
|
||||
internal static BlockStart Start(BlockParser parser, Block block)
|
||||
{
|
||||
if (parser.Indented || block is not Paragraph paragraph)
|
||||
{
|
||||
return BlockStart.Skip;
|
||||
}
|
||||
|
||||
var line = parser.CurrentLine[parser.NextNonSpace..];
|
||||
|
||||
// Check if the line contains a pipe character to be more specific about table delimiters
|
||||
// This helps avoid misinterpreting heading delimiters as table delimiters
|
||||
if (!line.Contains('|') && !line.Contains(':'))
|
||||
{
|
||||
return BlockStart.Skip;
|
||||
}
|
||||
|
||||
var match = DelimiterRegex.Match(line);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
// Parse the delimiter row to determine alignments
|
||||
var delimiterRow = line.Trim();
|
||||
|
||||
// Parse the header row from the paragraph text
|
||||
var headerLine = paragraph.Value.Trim();
|
||||
|
||||
// Parse header cells and delimiter cells
|
||||
var headerCells = ParseRow(headerLine);
|
||||
|
||||
// Parse delimiter cells to determine alignments
|
||||
var cleanDelimiterRow = delimiterRow;
|
||||
if (cleanDelimiterRow.StartsWith('|'))
|
||||
{
|
||||
cleanDelimiterRow = cleanDelimiterRow[1..];
|
||||
}
|
||||
if (cleanDelimiterRow.EndsWith('|'))
|
||||
{
|
||||
cleanDelimiterRow = cleanDelimiterRow[..^1];
|
||||
}
|
||||
|
||||
// Split by pipe character
|
||||
var delimiters = cleanDelimiterRow.Split('|');
|
||||
|
||||
// If the number of header cells and delimiter cells don't match, don't create a table
|
||||
if (headerCells.Count != delimiters.Length)
|
||||
{
|
||||
return BlockStart.Skip;
|
||||
}
|
||||
|
||||
// Initialize alignments list
|
||||
var alignments = new List<TableCellAlignment>(headerCells.Count);
|
||||
for (int i = 0; i < headerCells.Count; i++)
|
||||
{
|
||||
alignments.Add(TableCellAlignment.None);
|
||||
}
|
||||
|
||||
// Determine alignments from delimiters
|
||||
for (var i = 0; i < delimiters.Length && i < alignments.Count; i++)
|
||||
{
|
||||
var trimmed = delimiters[i].Trim();
|
||||
|
||||
if (trimmed.StartsWith(':') && trimmed.EndsWith(':'))
|
||||
{
|
||||
alignments[i] = TableCellAlignment.Center;
|
||||
}
|
||||
else if (trimmed.EndsWith(':'))
|
||||
{
|
||||
alignments[i] = TableCellAlignment.Right;
|
||||
}
|
||||
else if (trimmed.StartsWith(':'))
|
||||
{
|
||||
alignments[i] = TableCellAlignment.Left;
|
||||
}
|
||||
}
|
||||
|
||||
parser.CloseUnmatchedBlocks();
|
||||
|
||||
// resolve reference links
|
||||
while (paragraph.Value.Peek() == '[' && parser.TryParseLinkReference(paragraph.Value, out var position))
|
||||
{
|
||||
paragraph.Value = paragraph.Value[position..];
|
||||
}
|
||||
|
||||
if (paragraph.Value.Length > 0)
|
||||
{
|
||||
var table = new Table();
|
||||
|
||||
// Create header row
|
||||
|
||||
var header = new TableHeaderRow();
|
||||
table.rows.Add(header);
|
||||
|
||||
// Add header cells with alignments
|
||||
for (int cellindex = 0; cellindex < headerCells.Count; cellindex++)
|
||||
{
|
||||
var alignment = cellindex < alignments.Count ? alignments[cellindex] : TableCellAlignment.None;
|
||||
header.Add(headerCells[cellindex], alignment);
|
||||
}
|
||||
|
||||
paragraph.Parent.Replace(paragraph, table);
|
||||
parser.Tip = table;
|
||||
parser.AdvanceOffset(parser.CurrentLine.Length - parser.Offset, false);
|
||||
|
||||
return BlockStart.Leaf;
|
||||
}
|
||||
}
|
||||
|
||||
return BlockStart.Skip;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a table header row in a Markdown table.
|
||||
/// </summary>
|
||||
public class TableHeaderRow : TableRow
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void Accept(INodeVisitor visitor)
|
||||
{
|
||||
visitor.VisitTableHeaderRow(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a table row in a Markdown table.
|
||||
/// </summary>
|
||||
public class TableRow : INode
|
||||
{
|
||||
private readonly List<TableCell> children = [];
|
||||
|
||||
/// <summary>
|
||||
/// /// Gets the cells of the table row.
|
||||
/// </summary>
|
||||
|
||||
public IReadOnlyList<TableCell> Cells => children;
|
||||
|
||||
/// <summary>
|
||||
/// Adds a cell to the table row.
|
||||
/// </summary>
|
||||
public void Add(string value, TableCellAlignment alignment = TableCellAlignment.None)
|
||||
{
|
||||
var cell = new TableCell(value, alignment);
|
||||
children.Add(cell);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void Accept(INodeVisitor visitor)
|
||||
{
|
||||
visitor.VisitTableRow(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a table cell in a Markdown table.
|
||||
/// </summary>
|
||||
public class TableCell : INode, IBlockInlineContainer
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the alignment of the table cell.
|
||||
/// </summary>
|
||||
public TableCellAlignment Alignment { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TableCell"/> class.
|
||||
/// </summary>
|
||||
public TableCell(string value, TableCellAlignment alignment = TableCellAlignment.None)
|
||||
{
|
||||
Value = value;
|
||||
Alignment = alignment;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the inline content of the cell
|
||||
/// </summary>
|
||||
public string Value { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Accept(INodeVisitor visitor)
|
||||
{
|
||||
visitor.VisitTableCell(this);
|
||||
}
|
||||
|
||||
private readonly List<Inline> children = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the children of the table cell.
|
||||
/// </summary>
|
||||
public IReadOnlyList<Inline> Children => children;
|
||||
|
||||
/// <summary>
|
||||
/// Appends a child to table cell.
|
||||
/// </summary>
|
||||
public void Add(Inline node)
|
||||
{
|
||||
children.Add(node);
|
||||
}
|
||||
}
|
||||
18
Radzen.Blazor/Markdown/Text.cs
Normal file
18
Radzen.Blazor/Markdown/Text.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a text node.
|
||||
/// </summary>
|
||||
public class Text(string value) : Inline
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the text value.
|
||||
/// </summary>
|
||||
public string Value { get; set; } = value;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Accept(INodeVisitor visitor)
|
||||
{
|
||||
visitor.VisitText(this);
|
||||
}
|
||||
}
|
||||
41
Radzen.Blazor/Markdown/ThematicBreak.cs
Normal file
41
Radzen.Blazor/Markdown/ThematicBreak.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a thematic break node: <c>***</c>, <c>---</c>, or <c>___</c>.
|
||||
/// </summary>
|
||||
public class ThematicBreak : Block
|
||||
{
|
||||
private static readonly Regex ThematicBreakRegex = new (@"^(?:\*[ \t]*){3,}$|^(?:_[ \t]*){3,}$|^(?:-[ \t]*){3,}$");
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Accept(INodeVisitor visitor)
|
||||
{
|
||||
visitor.VisitThematicBreak(this);
|
||||
}
|
||||
|
||||
internal override BlockMatch Matches(BlockParser parser)
|
||||
{
|
||||
// A thematic break can never contain other blocks
|
||||
return BlockMatch.Skip;
|
||||
}
|
||||
|
||||
internal static BlockStart Start(BlockParser parser, Block container)
|
||||
{
|
||||
if (!parser.Indented)
|
||||
{
|
||||
var line = parser.CurrentLine[parser.NextNonSpace..];
|
||||
|
||||
if (ThematicBreakRegex.IsMatch(line))
|
||||
{
|
||||
parser.CloseUnmatchedBlocks();
|
||||
parser.AddChild<ThematicBreak>(parser.NextNonSpace);
|
||||
parser.AdvanceOffset(parser.CurrentLine.Length - parser.Offset, false);
|
||||
return BlockStart.Leaf;
|
||||
}
|
||||
}
|
||||
|
||||
return BlockStart.Skip;
|
||||
}
|
||||
}
|
||||
13
Radzen.Blazor/Markdown/UnorderedList.cs
Normal file
13
Radzen.Blazor/Markdown/UnorderedList.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace Radzen.Blazor.Markdown;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an unordered list: <c>- item</c>.
|
||||
/// </summary>
|
||||
public class UnorderedList : List
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void Accept(INodeVisitor visitor)
|
||||
{
|
||||
visitor.VisitUnorderedList(this);
|
||||
}
|
||||
}
|
||||
39
Radzen.Blazor/NonRenderingExtensions.cs
Normal file
39
Radzen.Blazor/NonRenderingExtensions.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace Radzen.Blazor;
|
||||
|
||||
static class NonRenderingExtensions
|
||||
{
|
||||
public static Action AsNonRenderingEventHandler(this ComponentBase _, Action callback)
|
||||
=> new SyncReceiver(callback).Invoke;
|
||||
public static Action<TValue> AsNonRenderingEventHandler<TValue>(this Action<TValue> callback) => new SyncReceiver<TValue>(callback).Invoke;
|
||||
public static Func<Task> AsNonRenderingEventHandler(this ComponentBase _, Func<Task> callback) => new AsyncReceiver(callback).Invoke;
|
||||
public static Func<TValue, Task> AsNonRenderingEventHandler<TValue>(this ComponentBase _, Func<TValue, Task> callback) => new AsyncReceiver<TValue>(callback).Invoke;
|
||||
|
||||
private record SyncReceiver(Action callback) : ReceiverBase
|
||||
{
|
||||
public void Invoke() => callback();
|
||||
}
|
||||
|
||||
private record SyncReceiver<T>(Action<T> callback) : ReceiverBase
|
||||
{
|
||||
public void Invoke(T arg) => callback(arg);
|
||||
}
|
||||
|
||||
private record AsyncReceiver(Func<Task> callback) : ReceiverBase
|
||||
{
|
||||
public Task Invoke() => callback();
|
||||
}
|
||||
|
||||
private record AsyncReceiver<T>(Func<T, Task> callback) : ReceiverBase
|
||||
{
|
||||
public Task Invoke(T arg) => callback(arg);
|
||||
}
|
||||
|
||||
private record ReceiverBase : IHandleEvent
|
||||
{
|
||||
public Task HandleEventAsync(EventCallbackWorkItem item, object arg) => item.InvokeAsync(arg);
|
||||
}
|
||||
}
|
||||
@@ -190,9 +190,21 @@ namespace Radzen
|
||||
/// Gets or sets the pager summary format.
|
||||
/// </summary>
|
||||
/// <value>The pager summary format.</value>
|
||||
/// <remarks>
|
||||
/// <see cref="PagingSummaryTemplate" /> has preference
|
||||
/// </remarks>
|
||||
[Parameter]
|
||||
public string PagingSummaryFormat { get; set; } = "Page {0} of {1} ({2} items)";
|
||||
|
||||
#nullable enable
|
||||
/// <summary>
|
||||
/// Gets or sets the pager summary template.
|
||||
/// </summary>
|
||||
/// <remarks>Has preference over <see cref="PagingSummaryFormat" /></remarks>
|
||||
[Parameter]
|
||||
public RenderFragment<PagingInformation>? PagingSummaryTemplate { get; set; }
|
||||
#nullable restore
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the pager's first page button's title attribute.
|
||||
/// </summary>
|
||||
|
||||
10
Radzen.Blazor/PagingInformation.cs
Normal file
10
Radzen.Blazor/PagingInformation.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Radzen.Blazor
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents paging information.
|
||||
/// </summary>
|
||||
/// <param name="CurrentPage">The current page number.</param>
|
||||
/// <param name="NumberOfPages">The total number of pages.</param>
|
||||
/// <param name="TotalCount">The total count of items.</param>
|
||||
public record PagingInformation(int CurrentPage, int NumberOfPages, int TotalCount);
|
||||
}
|
||||
@@ -71,7 +71,7 @@ namespace Radzen
|
||||
var parameter = Expression.Parameter(source.ElementType, "x");
|
||||
|
||||
return GroupByMany(source,
|
||||
properties.Select(p => Expression.Lambda<Func<T, object>>(Expression.Convert(GetNestedPropertyExpression(parameter, p), typeof(object)), parameter).Compile()).ToArray(),
|
||||
properties.Select(p => Expression.Lambda<Func<T, object>>(Expression.Convert(GetNestedPropertyExpression(parameter, p), typeof(object)), parameter).Compile()).ToArray(),
|
||||
0);
|
||||
}
|
||||
|
||||
@@ -154,18 +154,21 @@ namespace Radzen
|
||||
|
||||
string methodAsc = "OrderBy";
|
||||
string methodDesc = "OrderByDescending";
|
||||
string[] sortStrings = new string[] { "asc", "desc" };
|
||||
|
||||
foreach (var o in (selector ?? "").Split(',', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
var nameAndOrder = o.Trim();
|
||||
var name = string.Join(" ", nameAndOrder.Split(' ').Where(i => !sortStrings.Contains(i.Trim()))).Trim();
|
||||
var order = nameAndOrder.Split(' ').FirstOrDefault(i => sortStrings.Contains(i.Trim())) ?? sortStrings.First();
|
||||
|
||||
Expression property = !string.IsNullOrEmpty(nameAndOrder) ?
|
||||
GetNestedPropertyExpression(parameters.FirstOrDefault(), nameAndOrder.Split(' ').FirstOrDefault()) : parameters.FirstOrDefault();
|
||||
GetNestedPropertyExpression(parameters.FirstOrDefault(), name) : parameters.FirstOrDefault();
|
||||
|
||||
expression = Expression.Call(
|
||||
typeof(Queryable), o.Split(' ').Contains("desc") ? methodDesc : methodAsc,
|
||||
typeof(Queryable), order.Equals(sortStrings.First(), StringComparison.OrdinalIgnoreCase) ? methodAsc : methodDesc,
|
||||
new Type[] { source.ElementType, property.Type },
|
||||
expression, Expression.Quote(Expression.Lambda(notNullCheck(property), parameters)));
|
||||
expression, Expression.Quote(Expression.Lambda(property, parameters)));
|
||||
|
||||
methodAsc = "ThenBy";
|
||||
methodDesc = "ThenByDescending";
|
||||
@@ -194,8 +197,8 @@ namespace Radzen
|
||||
/// <returns>An <see cref="IQueryable"/> that contains each element of the source sequence converted to the specified type.</returns>
|
||||
public static IQueryable Cast(this IQueryable source, Type type)
|
||||
{
|
||||
return source.Provider.CreateQuery(Expression.Call(null,
|
||||
typeof(Queryable).GetTypeInfo().GetDeclaredMethods(nameof(Queryable.Cast)).FirstOrDefault(mi => mi.IsGenericMethod).MakeGenericMethod(type),
|
||||
return source.Provider.CreateQuery(Expression.Call(null,
|
||||
typeof(Queryable).GetTypeInfo().GetDeclaredMethods(nameof(Queryable.Cast)).FirstOrDefault(mi => mi.IsGenericMethod).MakeGenericMethod(type),
|
||||
source.Expression));
|
||||
}
|
||||
|
||||
@@ -231,7 +234,7 @@ namespace Radzen
|
||||
/// <summary>
|
||||
/// Filters using the specified filter descriptors.
|
||||
/// </summary>
|
||||
public static IQueryable<T> Where<T>(this IQueryable<T> source, IEnumerable<FilterDescriptor> filters,
|
||||
public static IQueryable<T> Where<T>(this IQueryable<T> source, IEnumerable<FilterDescriptor> filters,
|
||||
LogicalFilterOperator logicalFilterOperator, FilterCaseSensitivity filterCaseSensitivity)
|
||||
{
|
||||
if (filters == null || !filters.Any())
|
||||
@@ -266,14 +269,7 @@ namespace Radzen
|
||||
string currentPart = parts[0];
|
||||
Expression member;
|
||||
|
||||
if (expression.Type.IsInterface)
|
||||
{
|
||||
member = Expression.Property(expression,
|
||||
new[] { expression.Type }.Concat(expression.Type.GetInterfaces()).FirstOrDefault(t => t.GetProperty(currentPart) != null),
|
||||
currentPart
|
||||
);
|
||||
}
|
||||
else if (typeof(IDictionary<string, object>).IsAssignableFrom(expression.Type))
|
||||
if (typeof(IDictionary<string, object>).IsAssignableFrom(expression.Type))
|
||||
{
|
||||
var key = currentPart.Split('"')[1];
|
||||
var typeString = currentPart.Split('(')[0];
|
||||
@@ -311,6 +307,13 @@ namespace Radzen
|
||||
throw new ArgumentException($"Invalid index format: {indexString}");
|
||||
}
|
||||
}
|
||||
else if (expression.Type.IsInterface)
|
||||
{
|
||||
member = Expression.Property(expression,
|
||||
new[] { expression.Type }.Concat(expression.Type.GetInterfaces()).FirstOrDefault(t => t.GetProperty(currentPart) != null),
|
||||
currentPart
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
member = Expression.PropertyOrField(expression, currentPart);
|
||||
@@ -516,35 +519,6 @@ namespace Radzen
|
||||
return (IList)genericToList.Invoke(null, new[] { query });
|
||||
}
|
||||
|
||||
static string EnumerableAsString(IQueryable enumerableValue, string baseType, object value)
|
||||
{
|
||||
Func<IQueryable, IEnumerable<object>> values = (items) => {
|
||||
if (items.ElementType == typeof(string))
|
||||
{
|
||||
return items.Cast<string>().Select(i => $@"""{i}""");
|
||||
}
|
||||
else if (items.ElementType == typeof(bool) || items.ElementType == typeof(bool?))
|
||||
{
|
||||
return items.Cast<object>().Select(i => $@"{i}".ToLower());
|
||||
}
|
||||
else if (PropertyAccess.IsDate(items.ElementType))
|
||||
{
|
||||
return items.Cast<object>().Select(i => $@"DateTime.Parse(""{i}"")");
|
||||
}
|
||||
else if (PropertyAccess.IsEnum(items.ElementType) || PropertyAccess.IsNullableEnum(items.ElementType))
|
||||
{
|
||||
return items.Cast<object>().Select(i => i != null ? Convert.ChangeType(i, typeof(int)) : null);
|
||||
}
|
||||
|
||||
return items.Cast<object>();
|
||||
|
||||
};
|
||||
|
||||
var finalValues = value != null && !(IsEnumerable(value.GetType()) && !(value is string)) ? new object[] { value }.AsQueryable().Cast(value.GetType()) : enumerableValue;
|
||||
|
||||
return "new " + baseType + "[]{" + String.Join(",", values(finalValues)) + "}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts to filterstring.
|
||||
/// </summary>
|
||||
@@ -553,203 +527,78 @@ namespace Radzen
|
||||
/// <returns>System.String.</returns>
|
||||
public static string ToFilterString<T>(this IEnumerable<RadzenDataGridColumn<T>> columns)
|
||||
{
|
||||
var columnsWithFilter = GetFilterableColumns(columns);
|
||||
Func<RadzenDataGridColumn<T>, bool> canFilter = (c) => c.Filterable && c.FilterPropertyType != null &&
|
||||
(!(c.GetFilterValue() == null || c.GetFilterValue() as string == string.Empty)
|
||||
|| c.GetFilterOperator() == FilterOperator.IsNotNull || c.GetFilterOperator() == FilterOperator.IsNull
|
||||
|| c.GetFilterOperator() == FilterOperator.IsEmpty || c.GetFilterOperator() == FilterOperator.IsNotEmpty)
|
||||
&& c.GetFilterProperty() != null;
|
||||
|
||||
if (columnsWithFilter.Any())
|
||||
Func<RadzenDataGridColumn<T>, bool> canFilterCustom = (c) => c.Filterable && c.FilterPropertyType != null &&
|
||||
(c.GetFilterOperator() == FilterOperator.Custom && c.GetCustomFilterExpression() != null)
|
||||
&& c.GetFilterProperty() != null;
|
||||
|
||||
var columnsToFilter = columns.Where(canFilter);
|
||||
var grid = columns.FirstOrDefault()?.Grid;
|
||||
var gridLogicalFilterOperator = grid != null ? grid.LogicalFilterOperator : LogicalFilterOperator.And;
|
||||
var gridFilterCaseSensitivity = grid != null ? grid.FilterCaseSensitivity : FilterCaseSensitivity.Default;
|
||||
|
||||
var serializer = new ExpressionSerializer();
|
||||
var filterExpression = "";
|
||||
|
||||
if (columnsToFilter.Any())
|
||||
{
|
||||
var gridLogicalFilterOperator = columns.FirstOrDefault()?.Grid?.LogicalFilterOperator;
|
||||
var gridBooleanOperator = gridLogicalFilterOperator == LogicalFilterOperator.And ? "&&" : "||";
|
||||
|
||||
var whereList = new List<string>();
|
||||
|
||||
foreach (var column in columnsWithFilter)
|
||||
var filters = columnsToFilter.Select(c => new FilterDescriptor()
|
||||
{
|
||||
string value = "";
|
||||
string secondValue = "";
|
||||
Property = c.Property,
|
||||
FilterProperty = c.FilterProperty,
|
||||
Type = c.FilterPropertyType,
|
||||
FilterValue = c.GetFilterValue(),
|
||||
FilterOperator = c.GetFilterOperator(),
|
||||
SecondFilterValue = c.GetSecondFilterValue(),
|
||||
SecondFilterOperator = c.GetSecondFilterOperator(),
|
||||
LogicalFilterOperator = c.GetLogicalFilterOperator()
|
||||
});
|
||||
|
||||
var v = column.GetFilterValue();
|
||||
var sv = column.GetSecondFilterValue();
|
||||
if (filters.Any())
|
||||
{
|
||||
var parameter = Expression.Parameter(typeof(T), "x");
|
||||
Expression combinedExpression = null;
|
||||
|
||||
if (column.GetFilterOperator() == FilterOperator.Custom)
|
||||
foreach (var filter in filters)
|
||||
{
|
||||
var customFilterExpression = column.GetCustomFilterExpression();
|
||||
if (!string.IsNullOrEmpty(customFilterExpression))
|
||||
{
|
||||
whereList.Add(customFilterExpression);
|
||||
}
|
||||
}
|
||||
else if (v != null && IsEnumerable(v.GetType()) && v.GetType() != typeof(string) || IsEnumerable(column.FilterPropertyType) && column.FilterPropertyType != typeof(string))
|
||||
{
|
||||
var enumerableValue = ((IEnumerable)(v != null ? v : Enumerable.Empty<object>())).AsQueryable();
|
||||
var enumerableSecondValue = ((IEnumerable)(sv != null ? sv : Enumerable.Empty<object>())).AsQueryable();
|
||||
var expression = GetExpression<T>(parameter, filter, gridFilterCaseSensitivity, filter.Type);
|
||||
if (expression == null) continue;
|
||||
|
||||
|
||||
string baseType = column.FilterPropertyType.GetGenericArguments().Count() == 1
|
||||
? column.FilterPropertyType.GetGenericArguments()[0].Name
|
||||
: "";
|
||||
|
||||
if (column.Property != column.FilterProperty)
|
||||
{
|
||||
baseType = "";
|
||||
}
|
||||
|
||||
var enumerableValueAsString = EnumerableAsString(enumerableValue, baseType, v);
|
||||
|
||||
var enumerableSecondValueAsString = EnumerableAsString(enumerableSecondValue, baseType, sv);
|
||||
|
||||
if (enumerableValue?.Cast<object>().Any() == true)
|
||||
{
|
||||
var columnFilterOperator = column.GetFilterOperator();
|
||||
var columnSecondFilterOperator = column.GetSecondFilterOperator();
|
||||
var linqOperator = LinqFilterOperators[column.GetFilterOperator()];
|
||||
if (linqOperator == null)
|
||||
{
|
||||
linqOperator = "==";
|
||||
}
|
||||
|
||||
var booleanOperator = column.LogicalFilterOperator == LogicalFilterOperator.And ? "&&" : "||";
|
||||
|
||||
var filterProperty = column.GetFilterProperty();
|
||||
var itemInstanceName = !filterProperty.Contains("[") ? "it." : "";
|
||||
var property = itemInstanceName + PropertyAccess.GetProperty(column.GetFilterProperty());
|
||||
|
||||
if (sv == null)
|
||||
{
|
||||
if (columnFilterOperator == FilterOperator.Contains || columnFilterOperator == FilterOperator.DoesNotContain)
|
||||
{
|
||||
if (column.GetFilterValue() is string && column.Property != column.FilterProperty && !string.IsNullOrEmpty(column.FilterProperty))
|
||||
{
|
||||
whereList.Add($@"{(columnFilterOperator == FilterOperator.DoesNotContain ? "! " : "")}{itemInstanceName + column.Property}.Any(i => {"i." + column.FilterProperty}.Contains(""" + column.GetFilterValue() + "\"))");
|
||||
}
|
||||
else if (IsEnumerable(column.FilterPropertyType) && column.FilterPropertyType != typeof(string) &&
|
||||
IsEnumerable(column.PropertyType) && column.PropertyType != typeof(string))
|
||||
{
|
||||
whereList.Add($@"{(columnFilterOperator == FilterOperator.DoesNotContain ? "! " : "")}({enumerableValueAsString}).Contains({property})");
|
||||
}
|
||||
else if (IsEnumerable(column.FilterPropertyType) && column.FilterPropertyType != typeof(string) &&
|
||||
column.Property != column.FilterProperty && !string.IsNullOrEmpty(column.FilterProperty))
|
||||
{
|
||||
whereList.Add($@"({property}).{(columnFilterOperator == FilterOperator.NotIn ? "Except" : "Intersect")}({enumerableValueAsString}).Any()");
|
||||
}
|
||||
else
|
||||
{
|
||||
whereList.Add($@"{(columnFilterOperator == FilterOperator.DoesNotContain ? "! " : "")}({enumerableValueAsString}).Contains({property})");
|
||||
}
|
||||
}
|
||||
else if (columnFilterOperator == FilterOperator.In || columnFilterOperator == FilterOperator.NotIn)
|
||||
{
|
||||
if (IsEnumerable(column.FilterPropertyType) && column.FilterPropertyType != typeof(string) &&
|
||||
IsEnumerable(column.PropertyType) && column.PropertyType != typeof(string))
|
||||
{
|
||||
whereList.Add($@"{(columnFilterOperator == FilterOperator.NotIn ? "!" : "")}{property}.Any(i => ({enumerableValueAsString}).Contains(i))");
|
||||
}
|
||||
else if (IsEnumerable(column.FilterPropertyType) && column.FilterPropertyType != typeof(string) &&
|
||||
column.Property != column.FilterProperty && !string.IsNullOrEmpty(column.FilterProperty))
|
||||
{
|
||||
whereList.Add($@"{(columnFilterOperator == FilterOperator.NotIn ? "!" : "")}{itemInstanceName + column.Property}.Any(i => ({enumerableValueAsString}).Contains(i.{column.FilterProperty}))");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if ((columnFilterOperator == FilterOperator.Contains || columnFilterOperator == FilterOperator.DoesNotContain) &&
|
||||
(columnSecondFilterOperator == FilterOperator.Contains || columnSecondFilterOperator == FilterOperator.DoesNotContain))
|
||||
{
|
||||
whereList.Add($@"{(columnFilterOperator == FilterOperator.DoesNotContain ? "!" : "")}({enumerableValueAsString}).Contains({property}) {booleanOperator} {(columnSecondFilterOperator == FilterOperator.DoesNotContain ? "!" : "")}({enumerableSecondValueAsString}).Contains({property})");
|
||||
}
|
||||
else if ((columnFilterOperator == FilterOperator.In || columnFilterOperator == FilterOperator.NotIn) &&
|
||||
(columnSecondFilterOperator == FilterOperator.In || columnSecondFilterOperator == FilterOperator.NotIn))
|
||||
{
|
||||
whereList.Add($@"({property}).{(columnFilterOperator == FilterOperator.NotIn ? "Except" : "Intersect")}({enumerableValueAsString}).Any() {booleanOperator} ({property}).{(columnSecondFilterOperator == FilterOperator.NotIn ? "Except" : "Intersect")}({enumerableSecondValueAsString}).Any()");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (column.FilterPropertyType == typeof(TimeOnly) || column.FilterPropertyType == typeof(TimeOnly?))
|
||||
{
|
||||
value = v != null ? ((TimeOnly)v).ToString("HH:mm:ss") : "";
|
||||
secondValue = sv != null ? ((TimeOnly)sv).ToString("HH:mm:ss") : "";
|
||||
}
|
||||
else if (column.FilterPropertyType == typeof(Guid) || column.FilterPropertyType == typeof(Guid?))
|
||||
{
|
||||
value = $"{v}";
|
||||
secondValue = $"{sv}";
|
||||
}
|
||||
else if (PropertyAccess.IsDate(column.FilterPropertyType))
|
||||
{
|
||||
if (v != null)
|
||||
{
|
||||
|
||||
value =
|
||||
v is DateTime ? ((DateTime)v).ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture)
|
||||
: v is DateTimeOffset ? ((DateTimeOffset)v).UtcDateTime.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture)
|
||||
:
|
||||
#if NET6_0_OR_GREATER
|
||||
v is DateOnly ? ((DateOnly)v).ToString("yyy-MM-dd", CultureInfo.InvariantCulture) : "";
|
||||
#else
|
||||
"";
|
||||
#endif
|
||||
}
|
||||
if (sv != null)
|
||||
{
|
||||
secondValue =
|
||||
sv is DateTime ? ((DateTime)sv).ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture)
|
||||
: sv is DateTimeOffset ? ((DateTimeOffset)sv).UtcDateTime.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture)
|
||||
:
|
||||
#if NET6_0_OR_GREATER
|
||||
sv is DateOnly ? ((DateOnly)sv).ToString("yyy-MM-dd", CultureInfo.InvariantCulture) : "";
|
||||
#else
|
||||
"";
|
||||
#endif
|
||||
}
|
||||
}
|
||||
else if (!(v != null && IsEnumerable(v.GetType())) && (PropertyAccess.IsEnum(column.FilterPropertyType) || PropertyAccess.IsNullableEnum(column.FilterPropertyType)))
|
||||
{
|
||||
Type enumType = Enum.GetUnderlyingType(Nullable.GetUnderlyingType(column.FilterPropertyType) ?? column.FilterPropertyType);
|
||||
if (v != null)
|
||||
{
|
||||
value = Convert.ChangeType(v, enumType).ToString();
|
||||
}
|
||||
if (sv != null)
|
||||
{
|
||||
secondValue = Convert.ChangeType(sv, enumType).ToString();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
value = (string)Convert.ChangeType(column.GetFilterValue(), typeof(string), CultureInfo.InvariantCulture);
|
||||
secondValue = (string)Convert.ChangeType(column.GetSecondFilterValue(), typeof(string), CultureInfo.InvariantCulture);
|
||||
combinedExpression = combinedExpression == null
|
||||
? expression
|
||||
: gridLogicalFilterOperator == LogicalFilterOperator.And ?
|
||||
Expression.AndAlso(combinedExpression, expression) :
|
||||
Expression.OrElse(combinedExpression, expression);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(value) || column.GetFilterOperator() == FilterOperator.IsNotNull
|
||||
|| column.GetFilterOperator() == FilterOperator.IsNull
|
||||
|| column.GetFilterOperator() == FilterOperator.IsEmpty
|
||||
|| column.GetFilterOperator() == FilterOperator.IsNotEmpty)
|
||||
if (combinedExpression != null)
|
||||
{
|
||||
var linqOperator = LinqFilterOperators[column.GetFilterOperator()];
|
||||
if (linqOperator == null)
|
||||
{
|
||||
linqOperator = "==";
|
||||
}
|
||||
|
||||
var booleanOperator = column.LogicalFilterOperator == LogicalFilterOperator.And ? "&&" : "||";
|
||||
|
||||
if (string.IsNullOrEmpty(secondValue))
|
||||
{
|
||||
whereList.Add(GetColumnFilter(column, value));
|
||||
}
|
||||
else
|
||||
{
|
||||
whereList.Add($"({GetColumnFilter(column, value)} {booleanOperator} {GetColumnFilter(column, secondValue, true)})");
|
||||
}
|
||||
filterExpression = serializer.Serialize(Expression.Lambda<Func<T, bool>>(combinedExpression, parameter));
|
||||
}
|
||||
}
|
||||
|
||||
return whereList.Any() ?
|
||||
"it => " + string.Join($" {gridBooleanOperator} ", whereList.Where(i => !string.IsNullOrEmpty(i))) : "";
|
||||
}
|
||||
|
||||
return "";
|
||||
var columnsWithCustomFilter = columns.Where(canFilterCustom);
|
||||
|
||||
var customFilterExpression = "";
|
||||
|
||||
if (columnsToFilter.Any())
|
||||
{
|
||||
var expressions = columnsWithCustomFilter.Select(c => (c.GetCustomFilterExpression() ?? "").Replace(" or ", " || ").Replace(" and ", " && ")).Where(e => !string.IsNullOrEmpty(e)).ToList();
|
||||
customFilterExpression = string.Join($"{(gridLogicalFilterOperator == LogicalFilterOperator.And ? " && " : " || ")}", expressions);
|
||||
|
||||
return !string.IsNullOrEmpty(filterExpression) && !string.IsNullOrEmpty(customFilterExpression) ?
|
||||
$"{filterExpression} {(gridLogicalFilterOperator == LogicalFilterOperator.And ? " && " : " || ")} {customFilterExpression}" :
|
||||
!string.IsNullOrEmpty(customFilterExpression) ? "it => " + customFilterExpression : filterExpression;
|
||||
}
|
||||
|
||||
return filterExpression;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -761,377 +610,38 @@ namespace Radzen
|
||||
public static string ToFilterString<T>(this RadzenDataFilter<T> dataFilter)
|
||||
{
|
||||
Func<CompositeFilterDescriptor, bool> canFilter = (c) => dataFilter.properties.Where(col => col.Property == c.Property).FirstOrDefault()?.FilterPropertyType != null &&
|
||||
(!(c.FilterValue == null || c.FilterValue as string == string.Empty)
|
||||
|| c.FilterOperator == FilterOperator.IsNotNull || c.FilterOperator == FilterOperator.IsNull
|
||||
|| c.FilterOperator == FilterOperator.IsEmpty || c.FilterOperator == FilterOperator.IsNotEmpty)
|
||||
&& c.Property != null;
|
||||
(!(c.FilterValue == null || c.FilterValue as string == string.Empty)
|
||||
|| c.FilterOperator == FilterOperator.IsNotNull || c.FilterOperator == FilterOperator.IsNull
|
||||
|| c.FilterOperator == FilterOperator.IsEmpty || c.FilterOperator == FilterOperator.IsNotEmpty)
|
||||
&& c.Property != null;
|
||||
|
||||
if (dataFilter.Filters.Concat(dataFilter.Filters.SelectManyRecursive(i => i.Filters ?? Enumerable.Empty<CompositeFilterDescriptor>())).Where(canFilter).Any())
|
||||
{
|
||||
return CompositeFilterToFilterString<T>(dataFilter.Filters, dataFilter, dataFilter.LogicalFilterOperator);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
var serializer = new ExpressionSerializer();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Linq-compatible filter string for a list of CompositeFilterDescriptions
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type that is being filtered</typeparam>
|
||||
/// <param name="filters">The list if filters</param>
|
||||
/// <param name="Datafilter">The RadzenDataFilter component</param>
|
||||
/// <param name="filterOperator">Whether filter elements should be and-ed or or-ed</param>
|
||||
/// <returns></returns>
|
||||
private static string CompositeFilterToFilterString<T>(IEnumerable<CompositeFilterDescriptor> filters, RadzenDataFilter<T> Datafilter, LogicalFilterOperator filterOperator)
|
||||
{
|
||||
if (filters.Any())
|
||||
{
|
||||
var LogicalFilterOperator = filterOperator;
|
||||
var BooleanOperator = LogicalFilterOperator == LogicalFilterOperator.And ? "&&" : "||";
|
||||
var filterExpressions = new List<Expression>();
|
||||
|
||||
var whereList = new List<string>();
|
||||
foreach (var column in filters)
|
||||
var parameter = Expression.Parameter(typeof(T), "x");
|
||||
|
||||
foreach (var filter in dataFilter.Filters)
|
||||
{
|
||||
if (column.Filters is not null && column.Filters.Any())
|
||||
{
|
||||
whereList.Add($"({CompositeFilterToFilterString(column.Filters, Datafilter, column.LogicalFilterOperator)})");
|
||||
}
|
||||
if (column.Property is not null)
|
||||
{
|
||||
whereList.Add($"{GetColumnFilter(Datafilter, column, Datafilter.FilterCaseSensitivity == FilterCaseSensitivity.CaseInsensitive)}");
|
||||
}
|
||||
AddWhereExpression<T>(parameter, filter, ref filterExpressions, dataFilter.FilterCaseSensitivity);
|
||||
}
|
||||
|
||||
return string.Join($" {BooleanOperator} ", whereList.Where(i => !string.IsNullOrEmpty(i)));
|
||||
}
|
||||
Expression combinedExpression = null;
|
||||
|
||||
return "";
|
||||
foreach (var expression in filterExpressions)
|
||||
{
|
||||
combinedExpression = combinedExpression == null
|
||||
? expression
|
||||
: dataFilter.LogicalFilterOperator == LogicalFilterOperator.And ?
|
||||
Expression.AndAlso(combinedExpression, expression) :
|
||||
Expression.OrElse(combinedExpression, expression);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a single linq-compatible filter string for one datafilter element (includes sub-lists)
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type that is being filtered</typeparam>
|
||||
/// <param name="dataFilter">The RadzenDataFilter component</param>
|
||||
/// <param name="column">The filter elements for which to create the filter string</param>
|
||||
/// <param name="caseSensitive">Whether filtering is case sensitive or not</param>
|
||||
/// <returns></returns>
|
||||
private static string GetColumnFilter<T>(RadzenDataFilter<T> dataFilter, CompositeFilterDescriptor column, bool caseSensitive)
|
||||
{
|
||||
var property = column.Property;
|
||||
|
||||
if (property.Contains(".", StringComparison.CurrentCulture))
|
||||
{
|
||||
property = $"({property})";
|
||||
}
|
||||
|
||||
var columnInfo = dataFilter.properties.Where(c => c.Property == column.Property).FirstOrDefault();
|
||||
if (columnInfo == null) return "";
|
||||
|
||||
var columnType = columnInfo.FilterPropertyType;
|
||||
|
||||
var columnFilterOperator = column.FilterOperator;
|
||||
|
||||
var linqOperator = LinqFilterOperators[columnFilterOperator.Value];
|
||||
linqOperator ??= "==";
|
||||
|
||||
var value = "";
|
||||
|
||||
if (columnType == typeof(string))
|
||||
{
|
||||
value = (string)Convert.ChangeType(column.FilterValue, typeof(string));
|
||||
value = value?.Replace("\"", "\\\"");
|
||||
|
||||
string filterCaseSensitivityOperator = caseSensitive ? ".ToLower()" : "";
|
||||
|
||||
if (!string.IsNullOrEmpty(value) && column.FilterOperator == FilterOperator.Contains)
|
||||
if (combinedExpression != null)
|
||||
{
|
||||
return $@"({property} == null ? """" : {property}){filterCaseSensitivityOperator}.Contains(""{value}""{filterCaseSensitivityOperator})";
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(value) && columnFilterOperator == FilterOperator.DoesNotContain)
|
||||
{
|
||||
return $@"!({property} == null ? """" : {property}){filterCaseSensitivityOperator}.Contains(""{value}""{filterCaseSensitivityOperator})";
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(value) && columnFilterOperator == FilterOperator.StartsWith)
|
||||
{
|
||||
return $@"({property} == null ? """" : {property}){filterCaseSensitivityOperator}.StartsWith(""{value}""{filterCaseSensitivityOperator})";
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(value) && columnFilterOperator == FilterOperator.EndsWith)
|
||||
{
|
||||
return $@"({property} == null ? """" : {property}){filterCaseSensitivityOperator}.EndsWith(""{value}""{filterCaseSensitivityOperator})";
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(value) && columnFilterOperator == FilterOperator.Equals)
|
||||
{
|
||||
return $@"({property} == null ? """" : {property}){filterCaseSensitivityOperator} == ""{value}""{filterCaseSensitivityOperator}";
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(value) && columnFilterOperator == FilterOperator.NotEquals)
|
||||
{
|
||||
return $@"({property} == null ? """" : {property}){filterCaseSensitivityOperator} != ""{value}""{filterCaseSensitivityOperator}";
|
||||
}
|
||||
else if (columnFilterOperator == FilterOperator.IsNull)
|
||||
{
|
||||
return property + " == null";
|
||||
}
|
||||
else if (columnFilterOperator == FilterOperator.IsEmpty)
|
||||
{
|
||||
return property + @" == """"";
|
||||
}
|
||||
else if (columnFilterOperator == FilterOperator.IsNotEmpty)
|
||||
{
|
||||
return property + @" != """"";
|
||||
}
|
||||
else if (columnFilterOperator == FilterOperator.IsNotNull)
|
||||
{
|
||||
return property + @" != null";
|
||||
}
|
||||
}
|
||||
else if (PropertyAccess.IsNumeric(columnType))
|
||||
{
|
||||
value = (string)Convert.ChangeType(column.FilterValue, typeof(string));
|
||||
|
||||
if (columnFilterOperator == FilterOperator.IsNull || columnFilterOperator == FilterOperator.IsNotNull)
|
||||
{
|
||||
return $"{property} {linqOperator} null";
|
||||
}
|
||||
else if (columnFilterOperator == FilterOperator.IsEmpty || columnFilterOperator == FilterOperator.IsNotEmpty)
|
||||
{
|
||||
return $@"{property} {linqOperator} """"";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"{property} {linqOperator} {value}";
|
||||
}
|
||||
}
|
||||
else if (columnType == typeof(bool) || columnType == typeof(bool?))
|
||||
{
|
||||
value = (string)Convert.ChangeType(column.FilterValue, typeof(string));
|
||||
|
||||
return $"{property} {linqOperator} {(columnFilterOperator == FilterOperator.IsNull || columnFilterOperator == FilterOperator.IsNotNull ? "null" : value)}";
|
||||
}
|
||||
else if (PropertyAccess.IsDate(columnType))
|
||||
{
|
||||
var v = column.FilterValue;
|
||||
if (v != null)
|
||||
{
|
||||
value = $@"DateTime(""{(v is DateTime time ? time.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture) : v is DateTimeOffset offset ? offset.UtcDateTime.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture) : "")}"")";
|
||||
}
|
||||
}
|
||||
else if (PropertyAccess.IsEnum(columnType) || PropertyAccess.IsNullableEnum(columnType))
|
||||
{
|
||||
var v = column.FilterValue;
|
||||
if (v != null)
|
||||
{
|
||||
value = ((int)v).ToString();
|
||||
}
|
||||
}
|
||||
else if (IsEnumerable(columnType) && columnType != typeof(string))
|
||||
{
|
||||
var v = column.FilterValue;
|
||||
var enumerableValue = ((IEnumerable)(v ?? Enumerable.Empty<object>())).AsQueryable();
|
||||
|
||||
var enumerableValueAsString = "(" + String.Join(",",
|
||||
(enumerableValue.ElementType == typeof(string) ?
|
||||
enumerableValue.Cast<string>().Select(i => $@"""{i}""").Cast<object>()
|
||||
: PropertyAccess.IsDate(enumerableValue.ElementType) ?
|
||||
enumerableValue.Cast<object>().Select(i => $@"DateTime(""{i}"")").Cast<object>()
|
||||
: enumerableValue.Cast<object>())) + ")";
|
||||
|
||||
|
||||
if (enumerableValue?.Cast<object>().Any() == true)
|
||||
{
|
||||
if (property.Contains("."))
|
||||
{
|
||||
property = $"({property})";
|
||||
}
|
||||
|
||||
if (columnFilterOperator == FilterOperator.Contains || columnFilterOperator == FilterOperator.DoesNotContain)
|
||||
{
|
||||
return $@"{property} {(columnFilterOperator == FilterOperator.DoesNotContain ? "not " : "in")} {enumerableValueAsString}";
|
||||
}
|
||||
else if (columnFilterOperator == FilterOperator.In || columnFilterOperator == FilterOperator.NotIn)
|
||||
{
|
||||
return $@"({property}).{(columnFilterOperator == FilterOperator.NotIn ? "Except" : "Intersect")}({enumerableValueAsString}).Any()";
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
value = (string)Convert.ChangeType(column.FilterValue, typeof(string), CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(value) || column.FilterOperator == FilterOperator.IsNotNull
|
||||
|| column.FilterOperator == FilterOperator.IsNull
|
||||
|| column.FilterOperator == FilterOperator.IsEmpty
|
||||
|| column.FilterOperator == FilterOperator.IsNotEmpty)
|
||||
{
|
||||
return $"({property} {linqOperator} {(columnFilterOperator == FilterOperator.IsNull || columnFilterOperator == FilterOperator.IsNotNull ? "null" : value)})";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the column filter.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="column">The column.</param>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <param name="second">if set to <c>true</c> [second].</param>
|
||||
/// <returns>System.String.</returns>
|
||||
private static string GetColumnFilter<T>(RadzenDataGridColumn<T> column, string value, bool second = false)
|
||||
{
|
||||
var filterProperty = column.GetFilterProperty();
|
||||
var itemInstanceName = !filterProperty.Contains("[") ? "it." : "";
|
||||
var property = itemInstanceName + PropertyAccess.GetProperty(filterProperty);
|
||||
var propertyType = !string.IsNullOrEmpty(property) ? PropertyAccess.GetPropertyType(typeof(T), property) : null;
|
||||
|
||||
string npProperty = (propertyType != null ? Nullable.GetUnderlyingType(propertyType) != null : true) ? $@"({property} ?? null)" : property;
|
||||
|
||||
var columnFilterOperator = !second ? column.GetFilterOperator() : column.GetSecondFilterOperator();
|
||||
|
||||
if (columnFilterOperator == FilterOperator.Custom)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
var linqOperator = LinqFilterOperators[columnFilterOperator];
|
||||
if (linqOperator == null)
|
||||
{
|
||||
linqOperator = "==";
|
||||
}
|
||||
bool isDateOnly = false;
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
if (column.FilterPropertyType == typeof(DateOnly) || column.FilterPropertyType == typeof(DateOnly?))
|
||||
{
|
||||
isDateOnly = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (column.FilterPropertyType == typeof(string))
|
||||
{
|
||||
string filterCaseSensitivityOperator = column.Grid.FilterCaseSensitivity == FilterCaseSensitivity.CaseInsensitive ? ".ToLower()" : "";
|
||||
value = value?.Replace("\"", "\\\"");
|
||||
|
||||
if (!string.IsNullOrEmpty(value) && columnFilterOperator == FilterOperator.Contains)
|
||||
{
|
||||
return $@"({property} == null ? """" : {property}){filterCaseSensitivityOperator}.Contains(""{value}""{filterCaseSensitivityOperator})";
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(value) && columnFilterOperator == FilterOperator.DoesNotContain)
|
||||
{
|
||||
return $@"!({property} == null ? """" : {property}){filterCaseSensitivityOperator}.Contains(""{value}""{filterCaseSensitivityOperator})";
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(value) && columnFilterOperator == FilterOperator.StartsWith)
|
||||
{
|
||||
return $@"({property} == null ? """" : {property}){filterCaseSensitivityOperator}.StartsWith(""{value}""{filterCaseSensitivityOperator})";
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(value) && columnFilterOperator == FilterOperator.EndsWith)
|
||||
{
|
||||
return $@"({property} == null ? """" : {property}){filterCaseSensitivityOperator}.EndsWith(""{value}""{filterCaseSensitivityOperator})";
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(value) && columnFilterOperator == FilterOperator.Equals)
|
||||
{
|
||||
return $@"({property} == null ? """" : {property}){filterCaseSensitivityOperator} == ""{value}""{filterCaseSensitivityOperator}";
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(value) && columnFilterOperator == FilterOperator.NotEquals)
|
||||
{
|
||||
return $@"({property} == null ? """" : {property}){filterCaseSensitivityOperator} != ""{value}""{filterCaseSensitivityOperator}";
|
||||
}
|
||||
else if (columnFilterOperator == FilterOperator.IsNull)
|
||||
{
|
||||
return npProperty + " == null";
|
||||
}
|
||||
else if (columnFilterOperator == FilterOperator.IsEmpty)
|
||||
{
|
||||
return npProperty + @" == """"";
|
||||
}
|
||||
else if (columnFilterOperator == FilterOperator.IsNotEmpty)
|
||||
{
|
||||
return npProperty + @" != """"";
|
||||
}
|
||||
else if (columnFilterOperator == FilterOperator.IsNotNull)
|
||||
{
|
||||
return npProperty + @" != null";
|
||||
}
|
||||
}
|
||||
else if (PropertyAccess.IsEnum(column.FilterPropertyType) || PropertyAccess.IsNullableEnum(column.FilterPropertyType))
|
||||
{
|
||||
if (columnFilterOperator == FilterOperator.IsNull || columnFilterOperator == FilterOperator.IsNotNull)
|
||||
{
|
||||
return $"{property} {linqOperator} null";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"{property} {linqOperator} {value}";
|
||||
}
|
||||
}
|
||||
else if (PropertyAccess.IsNumeric(column.FilterPropertyType))
|
||||
{
|
||||
if (columnFilterOperator == FilterOperator.IsNull || columnFilterOperator == FilterOperator.IsNotNull)
|
||||
{
|
||||
return $"{property} {linqOperator} null";
|
||||
}
|
||||
else if (columnFilterOperator == FilterOperator.IsEmpty || columnFilterOperator == FilterOperator.IsNotEmpty)
|
||||
{
|
||||
return $@"{property} {linqOperator} """"";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"{property} {linqOperator} {value}";
|
||||
}
|
||||
}
|
||||
else if (column.FilterPropertyType == typeof(DateTime) ||
|
||||
column.FilterPropertyType == typeof(DateTime?) ||
|
||||
column.FilterPropertyType == typeof(DateTimeOffset) ||
|
||||
column.FilterPropertyType == typeof(DateTimeOffset?) || isDateOnly)
|
||||
{
|
||||
if (columnFilterOperator == FilterOperator.IsNull || columnFilterOperator == FilterOperator.IsNotNull)
|
||||
{
|
||||
return $"{property} {linqOperator} null";
|
||||
}
|
||||
else if (columnFilterOperator == FilterOperator.IsEmpty || columnFilterOperator == FilterOperator.IsNotEmpty)
|
||||
{
|
||||
return $@"{property} {linqOperator} """"";
|
||||
}
|
||||
else
|
||||
{
|
||||
var dateTimeValue = DateTime.Parse(value, CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.RoundtripKind);
|
||||
var finalDate = dateTimeValue.TimeOfDay == TimeSpan.Zero ? dateTimeValue.Date : dateTimeValue;
|
||||
var dateFormat = dateTimeValue.TimeOfDay == TimeSpan.Zero ? "yyyy-MM-dd" : "yyyy-MM-ddTHH:mm:ss.fffZ";
|
||||
|
||||
string dateFunction = "DateTime"; //fallback to datetime, if it's an offset or dateonly use that
|
||||
if (column.FilterPropertyType == typeof(DateTimeOffset) || column.FilterPropertyType == typeof(DateTimeOffset?))
|
||||
dateFunction = "DateTimeOffset";
|
||||
else
|
||||
{
|
||||
#if NET6_0_OR_GREATER
|
||||
if (column.FilterPropertyType == typeof(DateOnly) || column.FilterPropertyType == typeof(DateOnly?))
|
||||
dateFunction = "DateOnly";
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
return $@"{property} {linqOperator} {dateFunction}(""{finalDate.ToString(dateFormat, CultureInfo.InvariantCulture)}"")";
|
||||
}
|
||||
}
|
||||
else if (column.FilterPropertyType == typeof(bool) || column.FilterPropertyType == typeof(bool?))
|
||||
{
|
||||
value = $"{value}".ToLower();
|
||||
return $"{property} {linqOperator} {(columnFilterOperator == FilterOperator.IsNull || columnFilterOperator == FilterOperator.IsNotNull ? "null" : value)}";
|
||||
}
|
||||
else if (column.FilterPropertyType == typeof(Guid) || column.FilterPropertyType == typeof(Guid?))
|
||||
{
|
||||
if (columnFilterOperator == FilterOperator.IsNull || columnFilterOperator == FilterOperator.IsNotNull)
|
||||
{
|
||||
return $"{property} {linqOperator} null";
|
||||
}
|
||||
else if (columnFilterOperator == FilterOperator.IsEmpty || columnFilterOperator == FilterOperator.IsNotEmpty)
|
||||
{
|
||||
return $@"{property} {linqOperator} """"";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $@"{property} {linqOperator} Guid(""{value}"")";
|
||||
return serializer.Serialize(Expression.Lambda<Func<T, bool>>(combinedExpression, parameter));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1156,7 +666,7 @@ namespace Radzen
|
||||
? null
|
||||
: (string)Convert.ChangeType(filterValue is DateTimeOffset ?
|
||||
((DateTimeOffset)filterValue).UtcDateTime : filterValue is DateOnly ?
|
||||
((DateOnly)filterValue).ToString("yyy-MM-dd", CultureInfo.InvariantCulture) :
|
||||
((DateOnly)filterValue).ToString("yyy-MM-dd", CultureInfo.InvariantCulture) :
|
||||
filterValue, typeof(string), CultureInfo.InvariantCulture);
|
||||
|
||||
if (column.Grid.FilterCaseSensitivity == FilterCaseSensitivity.CaseInsensitive && column.FilterPropertyType == typeof(string))
|
||||
@@ -1273,7 +783,7 @@ namespace Radzen
|
||||
column.FilterPropertyType == typeof(DateTime?) ||
|
||||
column.FilterPropertyType == typeof(DateTimeOffset) ||
|
||||
column.FilterPropertyType == typeof(DateTimeOffset?) ||
|
||||
column.FilterPropertyType == typeof(DateOnly) ||
|
||||
column.FilterPropertyType == typeof(DateOnly) ||
|
||||
column.FilterPropertyType == typeof(DateOnly?))
|
||||
{
|
||||
if (columnFilterOperator == FilterOperator.IsNull || columnFilterOperator == FilterOperator.IsNotNull)
|
||||
@@ -1391,16 +901,16 @@ namespace Radzen
|
||||
if (columnsToFilter.Any())
|
||||
{
|
||||
source = source.Where(columnsToFilter.Select(c => new FilterDescriptor()
|
||||
{
|
||||
Property = c.Property,
|
||||
FilterProperty = c.FilterProperty,
|
||||
Type = c.FilterPropertyType,
|
||||
FilterValue = c.GetFilterValue(),
|
||||
FilterOperator = c.GetFilterOperator(),
|
||||
SecondFilterValue = c.GetSecondFilterValue(),
|
||||
SecondFilterOperator = c.GetSecondFilterOperator(),
|
||||
LogicalFilterOperator = c.GetLogicalFilterOperator()
|
||||
}), gridLogicalFilterOperator, gridFilterCaseSensitivity);
|
||||
{
|
||||
Property = c.Property,
|
||||
FilterProperty = c.FilterProperty,
|
||||
Type = c.FilterPropertyType,
|
||||
FilterValue = c.GetFilterValue(),
|
||||
FilterOperator = c.GetFilterOperator(),
|
||||
SecondFilterValue = c.GetSecondFilterValue(),
|
||||
SecondFilterOperator = c.GetSecondFilterOperator(),
|
||||
LogicalFilterOperator = c.GetLogicalFilterOperator()
|
||||
}), gridLogicalFilterOperator, gridFilterCaseSensitivity);
|
||||
}
|
||||
|
||||
var columnsWithCustomFilter = columns.Where(canFilterCustom);
|
||||
@@ -1408,7 +918,7 @@ namespace Radzen
|
||||
if (columnsToFilter.Any())
|
||||
{
|
||||
var expressions = columnsWithCustomFilter.Select(c => (c.GetCustomFilterExpression() ?? "").Replace(" or ", " || ").Replace(" and ", " && ")).Where(e => !string.IsNullOrEmpty(e)).ToList();
|
||||
source = expressions.Any() ?
|
||||
source = expressions.Any() ?
|
||||
System.Linq.Dynamic.Core.DynamicExtensions.Where(source, "it => " + string.Join($"{(gridLogicalFilterOperator == LogicalFilterOperator.And ? " && " : " || ")}", expressions)) : source;
|
||||
}
|
||||
|
||||
@@ -1469,7 +979,7 @@ namespace Radzen
|
||||
/// <returns>IQueryable<T>.</returns>
|
||||
public static IQueryable<T> Where<T>(this IQueryable<T> source, IEnumerable<CompositeFilterDescriptor> filters, LogicalFilterOperator logicalFilterOperator, FilterCaseSensitivity filterCaseSensitivity)
|
||||
{
|
||||
Func<CompositeFilterDescriptor, bool> canFilter = (c) =>
|
||||
Func<CompositeFilterDescriptor, bool> canFilter = (c) =>
|
||||
(!(c.FilterValue == null || c.FilterValue as string == string.Empty)
|
||||
|| c.FilterOperator == FilterOperator.IsNotNull || c.FilterOperator == FilterOperator.IsNull
|
||||
|| c.FilterOperator == FilterOperator.IsEmpty || c.FilterOperator == FilterOperator.IsNotEmpty)
|
||||
@@ -1817,4 +1327,4 @@ namespace Radzen
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
<IsPackable>true</IsPackable>
|
||||
<PackageId>Radzen.Blazor</PackageId>
|
||||
<Product>Radzen.Blazor</Product>
|
||||
<Version>6.2.2</Version>
|
||||
<Version>7.0.0</Version>
|
||||
<Copyright>Radzen Ltd.</Copyright>
|
||||
<Authors>Radzen Ltd.</Authors>
|
||||
<Description>Radzen Blazor is a set of 90+ free native Blazor UI controls packed with DataGrid, Scheduler, Charts and robust theming including Material design and Fluent UI.</Description>
|
||||
@@ -23,8 +23,8 @@
|
||||
<RepositoryUrl>https://github.com/radzenhq/radzen-blazor</RepositoryUrl>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
|
||||
<PackageReference Include="DartSassBuilder" Version="1.1.0" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components" Condition="'$(TargetFramework)' == 'net6.0'" Version="6.0.25" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Condition="'$(TargetFramework)' == 'net6.0'" Version="6.0.0" />
|
||||
@@ -34,6 +34,7 @@
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Condition="'$(TargetFramework)' == 'net8.0'" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components" Condition="'$(TargetFramework)' == 'net9.0'" Version="9.*-*" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Condition="'$(TargetFramework)' == 'net9.0'" Version="9.*-*" />
|
||||
<PackageReference Include="Radzen.Terser.MSBuild" Version="0.0.4" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -54,10 +55,10 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Sass Include="$(MSBuildProjectDirectory)/themes/*.scss" Exclude="$(MSBuildProjectDirectory)/themes/_*.scss" Condition="'$(TargetFramework)' == 'net8.0'" />
|
||||
<Sass Include="$(MSBuildProjectDirectory)/themes/*.scss" Exclude="$(MSBuildProjectDirectory)/themes/_*.scss" Condition="'$(TargetFramework)' == 'net9.0'" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="Sass" BeforeTargets="BeforeBuild" Condition="'$(TargetFramework)' == 'net8.0'">
|
||||
<Target Name="Sass" BeforeTargets="BeforeBuild" Condition="'$(TargetFramework)' == 'net9.0'">
|
||||
<PropertyGroup>
|
||||
<_SassFileList>@(Sass->'"%(FullPath)"', ' ')</_SassFileList>
|
||||
<DartSassBuilderArgs>files $(_SassFileList) --outputstyle $(DartSassOutputStyle) --level $(DartSassOutputLevel)</DartSassBuilderArgs>
|
||||
@@ -66,11 +67,21 @@
|
||||
<Message Text="Converted SassFile list to argument" Importance="$(DartSassMessageLevel)" />
|
||||
</Target>
|
||||
|
||||
<Target Name="MoveCss" AfterTargets="AfterCompile" Condition="'$(TargetFramework)' == 'net8.0'">
|
||||
<Target Name="MoveCss" AfterTargets="AfterCompile" Condition="'$(TargetFramework)' == 'net9.0'">
|
||||
<ItemGroup>
|
||||
<CssFile Include="$(MSBuildProjectDirectory)/themes/*.css" />
|
||||
</ItemGroup>
|
||||
<Move SourceFiles="@(CssFile)" DestinationFolder="$(MSBuildProjectDirectory)/wwwroot/css/" />
|
||||
</Target>
|
||||
|
||||
<Target Name="MinifyTest" BeforeTargets="Build" Condition="'$(TargetFramework)' == 'net9.0'">
|
||||
<TerserMinify InputFile="wwwroot\Radzen.Blazor.js" OutputFile="wwwroot\Radzen.Blazor.min.js" />
|
||||
</Target>
|
||||
|
||||
<ItemGroup Label="Allow internal method to be visible to the unit tests">
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>Radzen.Blazor.Tests</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@using System.Linq
|
||||
@using Radzen.Blazor.Rendering
|
||||
|
||||
@inherits RadzenComponent
|
||||
|
||||
@@ -11,8 +12,7 @@
|
||||
@if (Visible)
|
||||
{
|
||||
<div @ref="@Element" role="tablist" style=@Style @attributes="Attributes" class="@GetCssClass()" id="@GetId()"
|
||||
tabindex="0" @onkeydown="@((args) => OnKeyPress(args))" @onkeydown:preventDefault=preventKeyPress @onkeydown:stopPropagation=preventKeyPress
|
||||
@onfocus=@(args => focusedIndex = focusedIndex == -1 ? 0: focusedIndex)>
|
||||
tabindex="0" @onkeydown="@((args) => OnKeyPress(args))" @onkeydown:preventDefault=preventKeyPress @onkeydown:stopPropagation=preventKeyPress>
|
||||
@for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
var item = items[i];
|
||||
@@ -22,17 +22,10 @@
|
||||
<div @ref="@item.Element" id="@item.GetItemId()" @attributes="item.Attributes" class="@item.GetItemCssClass()" style="@item.Style" @onkeydown:stopPropagation>
|
||||
<a @onclick="@((args) => SelectItem(item))" aria-label="@ItemAriaLabel(i, item)" title="@ItemTitle(i, item)" @onclick:preventDefault="true" role="tab"
|
||||
id="@($"rz-accordiontab-{items.IndexOf(item)}")" aria-controls="@($"rz-accordiontab-{items.IndexOf(item)}-content")" aria-expanded="true">
|
||||
@if (IsSelected(i, item))
|
||||
{
|
||||
<span class="notranslate rz-accordion-toggle-icon rzi rzi-chevron-down"></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="notranslate rz-accordion-toggle-icon rzi rzi-chevron-right"></span>
|
||||
}
|
||||
<span class="@ToggleIconClass(item)">keyboard_arrow_down</span>
|
||||
@if (!string.IsNullOrEmpty(item.Icon))
|
||||
{
|
||||
<i class="notranslate rzi" style="@(!string.IsNullOrEmpty(item.IconColor) ? $"color:{item.IconColor}" : null)">@((MarkupString)item.Icon)</i>
|
||||
<i class="notranslate rzi" style="@(!string.IsNullOrEmpty(item.IconColor) ? $"color:{item.IconColor}" : null)">@item.Icon</i>
|
||||
}
|
||||
@if (item.Template != null)
|
||||
{
|
||||
@@ -40,19 +33,16 @@
|
||||
}
|
||||
else if(!string.IsNullOrEmpty(item.Text))
|
||||
{
|
||||
<span>@((MarkupString)item.Text)</span>
|
||||
<span>@item.Text</span>
|
||||
}
|
||||
</a>
|
||||
</div>
|
||||
@if (IsSelected(i, item))
|
||||
{
|
||||
<div class="rz-accordion-content-wrapper" role="tabpanel"
|
||||
id="@($"rz-accordiontab-{items.IndexOf(item)}-content")" aria-hidden="false" aria-labelledby="@($"rz-accordiontab-{items.IndexOf(item)}")">
|
||||
<div class="rz-accordion-content" @onkeydown:stopPropagation>
|
||||
@item.ChildContent
|
||||
</div>
|
||||
<Expander Expanded=@item.GetSelected() role="tabpanel"
|
||||
id="@($"rz-accordiontab-{items.IndexOf(item)}-content")" aria-labelledby="@($"rz-accordiontab-{items.IndexOf(item)}")">
|
||||
<div class="rz-accordion-content" @onkeydown:stopPropagation>
|
||||
@item.ChildContent
|
||||
</div>
|
||||
}
|
||||
</Expander>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Radzen.Blazor.Rendering;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -110,6 +111,11 @@ namespace Radzen.Blazor
|
||||
}
|
||||
}
|
||||
|
||||
string ToggleIconClass(RadzenAccordionItem item) => ClassList.Create("notranslate rz-accordion-toggle-icon rzi")
|
||||
.Add("rz-state-expanded", item.GetSelected())
|
||||
.Add("rz-state-collapsed", !item.GetSelected())
|
||||
.ToString();
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes this instance.
|
||||
/// </summary>
|
||||
@@ -251,5 +257,13 @@ namespace Radzen.Blazor
|
||||
|
||||
await base.SetParametersAsync(parameters);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
focusedIndex = focusedIndex == -1 ? 0 : focusedIndex;
|
||||
|
||||
base.OnInitialized();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,6 +177,12 @@ namespace Radzen.Blazor
|
||||
shouldRefresh = true;
|
||||
}
|
||||
|
||||
if (parameters.DidParameterChange(nameof(Visible), Visible))
|
||||
{
|
||||
_visible = parameters.GetValueOrDefault<bool>(nameof(Visible));
|
||||
shouldRefresh = true;
|
||||
}
|
||||
|
||||
await base.SetParametersAsync(parameters);
|
||||
|
||||
if (shouldRefresh)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Radzen.Blazor.Rendering;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -161,32 +162,23 @@ namespace Radzen.Blazor
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string GetComponentCssClass()
|
||||
{
|
||||
return $"rz-alert rz-alert-{GetAlertSize()} rz-variant-{Enum.GetName(typeof(Variant), Variant).ToLowerInvariant()} rz-{Enum.GetName(typeof(AlertStyle), AlertStyle).ToLowerInvariant()} rz-shade-{Enum.GetName(typeof(Shade), Shade).ToLowerInvariant()}";
|
||||
}
|
||||
protected override string GetComponentCssClass() => ClassList.Create("rz-alert")
|
||||
.Add($"rz-alert-{GetAlertSize()}")
|
||||
.AddVariant(Variant)
|
||||
.Add($"rz-{Enum.GetName(typeof(AlertStyle), AlertStyle).ToLowerInvariant()}")
|
||||
.AddShade(Shade)
|
||||
.ToString();
|
||||
|
||||
string GetIcon()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(Icon))
|
||||
{
|
||||
return Icon;
|
||||
}
|
||||
|
||||
switch (AlertStyle)
|
||||
{
|
||||
case AlertStyle.Success:
|
||||
return "check_circle";
|
||||
case AlertStyle.Danger:
|
||||
return "error";
|
||||
case AlertStyle.Warning:
|
||||
return "warning_amber";
|
||||
case AlertStyle.Info:
|
||||
return "info";
|
||||
default:
|
||||
return "lightbulb";
|
||||
}
|
||||
}
|
||||
string GetIcon() => !string.IsNullOrEmpty(Icon)
|
||||
? Icon
|
||||
: AlertStyle switch
|
||||
{
|
||||
AlertStyle.Success => "check_circle",
|
||||
AlertStyle.Danger => "error",
|
||||
AlertStyle.Warning => "warning_amber",
|
||||
AlertStyle.Info => "info",
|
||||
_ => "lightbulb",
|
||||
};
|
||||
|
||||
async Task OnClose()
|
||||
{
|
||||
@@ -221,4 +213,4 @@ namespace Radzen.Blazor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
<textarea @ref="@search" @attributes="InputAttributes" @onkeydown="@OnFilterKeyPress" value="@Value" disabled="@Disabled"
|
||||
oninput="@OpenScript()" tabindex="@(Disabled ? "-1" : $"{TabIndex}")" @onchange="@OnChange" onfocus="@(OpenOnFocus ? OpenScript() : null)"
|
||||
aria-autocomplete="list" aria-haspopup="true" autocomplete="off" role="combobox"
|
||||
class="@InputClassList" onblur="Radzen.activeElement = null"
|
||||
class=@InputClass onblur="Radzen.activeElement = null"
|
||||
id="@Name" aria-expanded="true" placeholder="@CurrentPlaceholder" maxlength="@MaxLength" />
|
||||
}
|
||||
else
|
||||
@@ -22,7 +22,7 @@
|
||||
<input @ref="@search" @attributes="InputAttributes" @onkeydown="@OnFilterKeyPress" value="@Value" disabled="@Disabled"
|
||||
oninput="@OpenScript()" tabindex="@(Disabled ? "-1" : $"{TabIndex}")" @onchange="@OnChange" onfocus="@(OpenOnFocus ? OpenScript() : null)"
|
||||
aria-autocomplete="list" aria-haspopup="true" autocomplete="off" role="combobox"
|
||||
class="@InputClassList" onblur="Radzen.activeElement = null"
|
||||
class=@InputClass onblur="Radzen.activeElement = null"
|
||||
type="@InputType" id="@Name" aria-expanded="true" placeholder="@CurrentPlaceholder" maxlength="@MaxLength" />
|
||||
}
|
||||
<div id="@PopupID" class="rz-autocomplete-panel" style="@PopupStyle">
|
||||
|
||||
@@ -279,8 +279,9 @@ namespace Radzen.Blazor
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
ClassList InputClassList => ClassList.Create("rz-inputtext rz-autocomplete-input")
|
||||
.AddDisabled(Disabled);
|
||||
string InputClass => ClassList.Create("rz-inputtext rz-autocomplete-input")
|
||||
.AddDisabled(Disabled)
|
||||
.ToString();
|
||||
|
||||
private string OpenScript()
|
||||
{
|
||||
@@ -293,10 +294,7 @@ namespace Radzen.Blazor
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string GetComponentCssClass()
|
||||
{
|
||||
return GetClassList("rz-autocomplete").ToString();
|
||||
}
|
||||
protected override string GetComponentCssClass() => GetClassList("rz-autocomplete").ToString();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Dispose()
|
||||
@@ -305,7 +303,7 @@ namespace Radzen.Blazor
|
||||
|
||||
if (IsJSRuntimeAvailable)
|
||||
{
|
||||
JSRuntime.InvokeVoidAsync("Radzen.destroyPopup", PopupID);
|
||||
JSRuntime.InvokeVoid("Radzen.destroyPopup", PopupID);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,4 +362,4 @@ namespace Radzen.Blazor
|
||||
await search.FocusAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using System.Collections.Generic;
|
||||
using Radzen.Blazor.Rendering;
|
||||
|
||||
namespace Radzen.Blazor
|
||||
{
|
||||
@@ -14,22 +14,12 @@ namespace Radzen.Blazor
|
||||
public partial class RadzenBadge : RadzenComponent
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override string GetComponentCssClass()
|
||||
{
|
||||
var classList = new List<string>();
|
||||
|
||||
classList.Add("rz-badge");
|
||||
classList.Add($"rz-badge-{BadgeStyle.ToString().ToLowerInvariant()}");
|
||||
classList.Add($"rz-variant-{Variant.ToString().ToLowerInvariant()}");
|
||||
classList.Add($"rz-shade-{Shade.ToString().ToLowerInvariant()}");
|
||||
|
||||
if (IsPill)
|
||||
{
|
||||
classList.Add("rz-badge-pill");
|
||||
}
|
||||
|
||||
return string.Join(" ", classList);
|
||||
}
|
||||
protected override string GetComponentCssClass() => ClassList.Create("rz-badge")
|
||||
.Add($"rz-badge-{BadgeStyle.ToString().ToLowerInvariant()}")
|
||||
.AddVariant(Variant)
|
||||
.AddShade(Shade)
|
||||
.Add("rz-badge-pill", IsPill)
|
||||
.ToString();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the child content.
|
||||
@@ -73,4 +63,4 @@ namespace Radzen.Blazor
|
||||
[Parameter]
|
||||
public bool IsPill { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,13 +24,9 @@ namespace Radzen.Blazor
|
||||
public override string Style { get; set; } = DefaultStyle;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string GetComponentCssClass()
|
||||
{
|
||||
var classList = ClassList.Create("rz-body")
|
||||
.Add("rz-body-expanded", Expanded);
|
||||
|
||||
return classList.ToString();
|
||||
}
|
||||
protected override string GetComponentCssClass() => ClassList.Create("rz-body")
|
||||
.Add("rz-body-expanded", Expanded)
|
||||
.ToString();
|
||||
|
||||
/// <summary>
|
||||
/// Toggles this instance width and left margin.
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
{
|
||||
@if (!string.IsNullOrEmpty(@Icon))
|
||||
{
|
||||
<i class="notranslate rz-button-icon-left rzi" style="@(!string.IsNullOrEmpty(IconColor) ? $"color:{IconColor}" : null)">@((MarkupString)Icon)</i>
|
||||
<i class="notranslate rz-button-icon-left rzi" style="@(!string.IsNullOrEmpty(IconColor) ? $"color:{IconColor}" : null)">@Icon</i>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(Image))
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Radzen.Blazor.Rendering;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -15,11 +16,6 @@ namespace Radzen.Blazor
|
||||
/// </example>
|
||||
public partial class RadzenButton : RadzenComponent
|
||||
{
|
||||
internal string getButtonSize()
|
||||
{
|
||||
return Size == ButtonSize.Medium ? "md" : Size == ButtonSize.Large ? "lg" : Size == ButtonSize.Small ? "sm" : "xs";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the child content.
|
||||
/// </summary>
|
||||
@@ -164,9 +160,13 @@ namespace Radzen.Blazor
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string GetComponentCssClass()
|
||||
{
|
||||
return $"rz-button rz-button-{getButtonSize()} rz-variant-{Enum.GetName(typeof(Variant), Variant).ToLowerInvariant()} rz-{Enum.GetName(typeof(ButtonStyle), ButtonStyle).ToLowerInvariant()} rz-shade-{Enum.GetName(typeof(Shade), Shade).ToLowerInvariant()}{(IsDisabled ? " rz-state-disabled" : "")}{(string.IsNullOrEmpty(Text) && !string.IsNullOrEmpty(Icon) ? " rz-button-icon-only" : "")}";
|
||||
}
|
||||
protected override string GetComponentCssClass() => ClassList.Create("rz-button")
|
||||
.AddButtonSize(Size)
|
||||
.AddVariant(Variant)
|
||||
.AddButtonStyle(ButtonStyle)
|
||||
.AddDisabled(IsDisabled)
|
||||
.AddShade(Shade)
|
||||
.Add($"rz-button-icon-only", string.IsNullOrEmpty(Text) && !string.IsNullOrEmpty(Icon))
|
||||
.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
using Radzen.Blazor.Rendering;
|
||||
|
||||
namespace Radzen.Blazor
|
||||
{
|
||||
@@ -12,14 +9,9 @@ namespace Radzen.Blazor
|
||||
public partial class RadzenCard : RadzenComponentWithChildren
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override string GetComponentCssClass()
|
||||
{
|
||||
var classList = new List<string>();
|
||||
classList.Add("rz-card");
|
||||
classList.Add($"rz-variant-{Variant.ToString().ToLowerInvariant()}");
|
||||
|
||||
return string.Join(" ", classList);
|
||||
}
|
||||
protected override string GetComponentCssClass() => ClassList.Create("rz-card")
|
||||
.AddVariant(Variant)
|
||||
.ToString();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the card variant.
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
using Radzen.Blazor.Rendering;
|
||||
|
||||
namespace Radzen.Blazor
|
||||
{
|
||||
@@ -19,17 +16,8 @@ namespace Radzen.Blazor
|
||||
public bool Responsive { get; set; } = true;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string GetComponentCssClass()
|
||||
{
|
||||
var classList = new List<string>();
|
||||
classList.Add("rz-card-group");
|
||||
|
||||
if (Responsive)
|
||||
{
|
||||
classList.Add("rz-card-group-responsive");
|
||||
}
|
||||
|
||||
return string.Join(" ", classList);
|
||||
}
|
||||
protected override string GetComponentCssClass() => ClassList.Create("rz-card-group")
|
||||
.Add("rz-card-group-responsive", Responsive)
|
||||
.ToString();
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
@implements IDisposable
|
||||
@inject IJSRuntime JSRuntime
|
||||
|
||||
<li @ref="element" @attributes=@Attributes class=@ClassList tabindex="0">
|
||||
<li @ref="element" @attributes=@Attributes class=@Class tabindex="0">
|
||||
@ChildContent
|
||||
<div class="rz-carousel-snapper"></div>
|
||||
</li>
|
||||
@@ -1,5 +1,4 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
using Radzen.Blazor.Rendering;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -16,9 +15,9 @@ namespace Radzen.Blazor
|
||||
/// Gets the class list.
|
||||
/// </summary>
|
||||
/// <value>The class list.</value>
|
||||
ClassList ClassList => ClassList.Create()
|
||||
.Add("rz-carousel-item")
|
||||
.Add(Attributes);
|
||||
string Class => ClassList.Create("rz-carousel-item")
|
||||
.Add(Attributes)
|
||||
.ToString();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the arbitrary attributes.
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
|
||||
if (chart.ShouldInvertAxes())
|
||||
{
|
||||
return AxisMeasurer.XAxis(chart.ValueAxis.Title);
|
||||
return AxisMeasurer.XAxis(chart.ValueScale, chart.ValueAxis, chart.ValueAxis.Title);
|
||||
}
|
||||
else
|
||||
{
|
||||
return AxisMeasurer.XAxis(Title);
|
||||
return AxisMeasurer.XAxis(chart.CategoryScale, this, Title);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user