mirror of https://github.com/abpframework/abp.git
committed by
GitHub
25 changed files with 541 additions and 170 deletions
@ -1,5 +1,5 @@ |
|||||
{ |
{ |
||||
"ConnectionStrings": { |
"ConnectionStrings": { |
||||
"Default": "Server=(localdb)\\.\\MSSQLLocalDB;Database=CmsKit_Unified;Trusted_Connection=True" |
"Default": "Server=(localdb)\\MSSQLLocalDB;Database=CmsKit_Unified;Trusted_Connection=True" |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -0,0 +1,8 @@ |
|||||
|
namespace Volo.CmsKit.Contents; |
||||
|
public static class ContentConsts |
||||
|
{ |
||||
|
public static string Delimeter { get; set; } = "----"; |
||||
|
public static string Widget { get; set; } = "Widget"; |
||||
|
public static string Markdown { get; set; } = "Markdown"; |
||||
|
public static string Content { get; set; } = "Content"; |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
using System; |
||||
|
using Volo.Abp.ObjectExtending; |
||||
|
|
||||
|
namespace Volo.CmsKit.Contents; |
||||
|
|
||||
|
[Serializable] |
||||
|
public class ContentFragment : ExtensibleObject |
||||
|
{ |
||||
|
public string Type { get; set; } |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Volo.CmsKit.Contents; |
||||
|
|
||||
|
public class CmsKitContentWidgetOptions |
||||
|
{ |
||||
|
public Dictionary<string, ContentWidgetConfig> WidgetConfigs { get; } |
||||
|
|
||||
|
public CmsKitContentWidgetOptions() |
||||
|
{ |
||||
|
WidgetConfigs = new(); |
||||
|
} |
||||
|
|
||||
|
public void AddWidget(string widgetName, string widgetKey) |
||||
|
{ |
||||
|
WidgetConfigs.Add(widgetName, new ContentWidgetConfig(widgetKey)); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,120 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Text.RegularExpressions; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Volo.Abp.Data; |
||||
|
using Volo.Abp.DependencyInjection; |
||||
|
using static Volo.CmsKit.Contents.ContentConsts; |
||||
|
|
||||
|
namespace Volo.CmsKit.Contents; |
||||
|
|
||||
|
public class ContentParser : ITransientDependency |
||||
|
{ |
||||
|
private readonly CmsKitContentWidgetOptions _options; |
||||
|
|
||||
|
public ContentParser(IOptions<CmsKitContentWidgetOptions> options) |
||||
|
{ |
||||
|
_options = options.Value; |
||||
|
} |
||||
|
|
||||
|
public Task<List<ContentFragment>> ParseAsync(string content) |
||||
|
{ |
||||
|
if (!_options.WidgetConfigs.Any()) |
||||
|
{ |
||||
|
return Task.FromResult(new List<ContentFragment> |
||||
|
{ |
||||
|
new ContentFragment { Type = Markdown }.SetProperty(Content, content), |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
var parsedList = new List<string>(); |
||||
|
ParseContent(content, parsedList); |
||||
|
|
||||
|
var contentFragments = new List<ContentFragment>(); |
||||
|
FillContentFragment(content, parsedList, contentFragments); |
||||
|
|
||||
|
return Task.FromResult(contentFragments); |
||||
|
} |
||||
|
|
||||
|
private void ParseContent(string content, List<string> parsedList) |
||||
|
{ |
||||
|
var replacedText = Regex.Replace(content, @"\[.*?\]", Delimeter); |
||||
|
if (!replacedText.Contains(Delimeter)) |
||||
|
{ |
||||
|
parsedList.Add(replacedText); |
||||
|
} |
||||
|
|
||||
|
while (replacedText.Contains(Delimeter)) |
||||
|
{ |
||||
|
//For parsing delimeter
|
||||
|
var index = replacedText.IndexOf(Delimeter); |
||||
|
if (index != 0) |
||||
|
{ |
||||
|
parsedList.Add(replacedText.Substring(0, index)); |
||||
|
replacedText = replacedText.Substring(index, replacedText.Length - index); |
||||
|
index = 0; |
||||
|
} |
||||
|
|
||||
|
parsedList.Add(replacedText.Substring(index, Delimeter.Length)); |
||||
|
replacedText = replacedText.Substring(Delimeter.Length, replacedText.Length - Delimeter.Length); |
||||
|
|
||||
|
//for parsing the other side
|
||||
|
index = replacedText.IndexOf(Delimeter); |
||||
|
if (index != -1) |
||||
|
{ |
||||
|
parsedList.Add(replacedText.Substring(0, index)); |
||||
|
replacedText = replacedText.Substring(index, replacedText.Length - index); |
||||
|
} |
||||
|
else if (replacedText.Length > 0) |
||||
|
{ |
||||
|
parsedList.Add(replacedText); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void FillContentFragment(string content, List<string> parsedList, List<ContentFragment> contentFragments) |
||||
|
{ |
||||
|
content = Regex.Replace(content, @"=\s*""", @"="""); |
||||
|
content = Regex.Replace(content, @"""\s*=", @"""="); |
||||
|
var widgets = Regex.Matches(content, @"(?<=\[Widget)(.*?)(?=\])").Cast<Match>().Select(p => p.Value).ToList(); |
||||
|
for (int i = 0, k = 0; i < parsedList.Count; i++) |
||||
|
{ |
||||
|
if (parsedList[i] == Delimeter) |
||||
|
{ |
||||
|
if (widgets.Count > k) |
||||
|
{ |
||||
|
var preparedContent = string.Join("", widgets[k]); |
||||
|
var keys = Regex.Matches(preparedContent, @"(?<=\s)(.*?)(?==\s*"")").Cast<Match>() |
||||
|
.Select(p => p.Value).Where(p => p != string.Empty).ToList(); |
||||
|
var values = Regex.Matches(preparedContent, @"(?<=\s*[a-zA-Z]*=\s*"")(.*?)(?="")").Cast<Match>() |
||||
|
.Select(p => p.Value).ToList(); |
||||
|
|
||||
|
var widgetTypeIndex = keys.IndexOf("Type"); |
||||
|
if (widgetTypeIndex != -1) |
||||
|
{ |
||||
|
var widgetType = values[widgetTypeIndex]; |
||||
|
var name = _options.WidgetConfigs.Where(p => p.Key == widgetType).Select(p => p.Value.Name).FirstOrDefault(); |
||||
|
if (name is not null && widgets.Count > k) |
||||
|
{ |
||||
|
values[0] = name; |
||||
|
var contentFragment = new ContentFragment() { Type = Widget }; |
||||
|
contentFragments.Add(contentFragment); |
||||
|
for (int kv = 0; kv < values.Count; kv++) |
||||
|
{ |
||||
|
contentFragments.FindLast(p => p == contentFragment) |
||||
|
.SetProperty(keys[kv], values[kv]); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
k++; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
contentFragments.Add(new ContentFragment() { Type = Markdown } |
||||
|
.SetProperty(Content, parsedList[i])); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
namespace Volo.CmsKit.Contents; |
||||
|
|
||||
|
public class ContentWidgetConfig |
||||
|
{ |
||||
|
public string Name { get; } |
||||
|
|
||||
|
public ContentWidgetConfig(string widgetName) |
||||
|
{ |
||||
|
Name = widgetName; |
||||
|
} |
||||
|
} |
||||
@ -1,17 +0,0 @@ |
|||||
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bootstrap |
|
||||
|
|
||||
@using Volo.CmsKit.Public.Web.Renderers |
|
||||
@using Volo.Abp.AspNetCore.Mvc.UI.Packages.HighlightJs |
|
||||
|
|
||||
@inject IMarkdownToHtmlRenderer MarkdownRenderer |
|
||||
|
|
||||
@model Volo.CmsKit.Public.Web.Pages.CmsKit.Shared.Components.Pages.PageViewModel |
|
||||
|
|
||||
<abp-card> |
|
||||
<abp-card-body> |
|
||||
@if (!Model.Content.IsNullOrEmpty()) |
|
||||
{ |
|
||||
@Html.Raw(await MarkdownRenderer.RenderAsync(Model.Content)) |
|
||||
} |
|
||||
</abp-card-body> |
|
||||
</abp-card> |
|
||||
@ -1,54 +0,0 @@ |
|||||
using System; |
|
||||
using System.Threading.Tasks; |
|
||||
using Microsoft.AspNetCore.Mvc; |
|
||||
using Volo.Abp.AspNetCore.Mvc; |
|
||||
using Volo.CmsKit.Public.Pages; |
|
||||
using Volo.CmsKit.Public.Web.Renderers; |
|
||||
using Volo.Abp.AspNetCore.Mvc.UI.Packages.HighlightJs; |
|
||||
using Volo.Abp.AspNetCore.Mvc.UI.Widgets; |
|
||||
|
|
||||
namespace Volo.CmsKit.Public.Web.Pages.CmsKit.Shared.Components.Pages; |
|
||||
|
|
||||
[Widget( |
|
||||
StyleTypes = new[] |
|
||||
{ |
|
||||
typeof(HighlightJsStyleContributor) |
|
||||
}, |
|
||||
ScriptTypes = new[] |
|
||||
{ |
|
||||
typeof(HighlightJsScriptContributor) |
|
||||
}, |
|
||||
ScriptFiles = new[] |
|
||||
{ |
|
||||
"/Pages/Public/CmsKit/highlightOnLoad.js" |
|
||||
})] |
|
||||
public class DefaultPageViewComponent : AbpViewComponent |
|
||||
{ |
|
||||
protected IPagePublicAppService PagePublicAppService { get; } |
|
||||
|
|
||||
public DefaultPageViewComponent(IPagePublicAppService pagePublicAppService) |
|
||||
{ |
|
||||
PagePublicAppService = pagePublicAppService; |
|
||||
} |
|
||||
|
|
||||
public virtual async Task<IViewComponentResult> InvokeAsync(Guid pageId, string title, string content) |
|
||||
{ |
|
||||
var model = new PageViewModel |
|
||||
{ |
|
||||
Id = pageId, |
|
||||
Title = title, |
|
||||
Content = content |
|
||||
}; |
|
||||
|
|
||||
return View("~/Pages/CmsKit/Shared/Components/Pages/Default.cshtml", model); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public class PageViewModel |
|
||||
{ |
|
||||
public Guid Id { get; set; } |
|
||||
|
|
||||
public string Title { get; set; } |
|
||||
|
|
||||
public string Content { get; set; } |
|
||||
} |
|
||||
@ -1,31 +1,46 @@ |
|||||
@page |
@page |
||||
@using Microsoft.AspNetCore.Mvc.Localization |
@addTagHelper *, Volo.Abp.AspNetCore.Mvc.UI.Bootstrap |
||||
@using Volo.CmsKit.Localization |
@using Volo.CmsKit.Contents |
||||
@using Volo.CmsKit.Public.Web.Pages.CmsKit.Shared.Components.Pages |
@using System.Dynamic |
||||
|
@using Volo.CmsKit.Public.Web.Renderers |
||||
|
@using Volo.Abp.Data |
||||
|
@using Volo.Abp.AspNetCore.Mvc.UI.Packages.HighlightJs; |
||||
|
@using Volo.Abp.AspNetCore.Mvc.UI.Widgets; |
||||
|
|
||||
|
@inject IMarkdownToHtmlRenderer MarkdownRenderer |
||||
|
|
||||
|
|
||||
@model Volo.CmsKit.Public.Web.Pages.Public.CmsKit.Pages.IndexModel |
@model Volo.CmsKit.Public.Web.Pages.Public.CmsKit.Pages.IndexModel |
||||
|
|
||||
@inject IHtmlLocalizer<CmsKitResource> L |
|
||||
|
|
||||
@section styles{ |
@section styles{ |
||||
|
|
||||
<abp-style src="/Pages/Public/CmsKit/Pages/index.css" /> |
<abp-style src="/Pages/Public/CmsKit/Pages/index.css" /> |
||||
|
|
||||
<style> |
<style> |
||||
@Html.Raw(Model.Page.Style) |
@Html.Raw(Model.PageDto.Style) |
||||
</style> |
</style> |
||||
} |
} |
||||
|
|
||||
@section scripts{ |
@section scripts{ |
||||
<script> |
<script> |
||||
@Html.Raw(Model.Page.Script) |
@Html.Raw(Model.PageDto.Script) |
||||
</script> |
</script> |
||||
} |
} |
||||
|
|
||||
@await Component.InvokeAsync(typeof(DefaultPageViewComponent), |
<abp-card> |
||||
new |
<abp-card-body> |
||||
{ |
@foreach (ContentFragment contentFragment in Model.PageDto.ContentFragments) |
||||
pageId = Model.Page.Id, |
{ |
||||
title = Model.Page.Title, |
if (contentFragment.Type == ContentConsts.Markdown) |
||||
content = Model.Page.Content |
{ |
||||
}) |
@Html.Raw(await MarkdownRenderer.RenderAsync(contentFragment.GetProperty<string>("Content"))) |
||||
|
} |
||||
|
else if (contentFragment.Type == ContentConsts.Widget) |
||||
|
{ |
||||
|
@await Component.InvokeAsync(contentFragment.GetProperty<string>("Type"), contentFragment.ExtraProperties.ConvertToDynamicObject()) |
||||
|
} |
||||
|
} |
||||
|
</abp-card-body> |
||||
|
</abp-card> |
||||
|
|
||||
|
|||||
@ -0,0 +1,14 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using Volo.CmsKit.Contents; |
||||
|
|
||||
|
namespace Volo.CmsKit.Public.Web.Pages.Public.CmsKit.Pages; |
||||
|
|
||||
|
public class PageViewModel |
||||
|
{ |
||||
|
public Guid Id { get; set; } |
||||
|
|
||||
|
public string Title { get; set; } |
||||
|
|
||||
|
public List<ContentFragment> ContentFragments { get; set; } |
||||
|
} |
||||
@ -0,0 +1,138 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Shouldly; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Volo.CmsKit.Contents; |
||||
|
public class ContentParser_Test : CmsKitDomainTestBase |
||||
|
{ |
||||
|
private readonly CmsKitTestData testData; |
||||
|
private readonly IOptions<CmsKitContentWidgetOptions> _options; |
||||
|
private ContentParser contentParser; |
||||
|
|
||||
|
public ContentParser_Test() |
||||
|
{ |
||||
|
testData = GetRequiredService<CmsKitTestData>(); |
||||
|
_options = GetRequiredService<IOptions<CmsKitContentWidgetOptions>>(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task ParseAsync_ShouldWorkWithDifferentWidgetTypes() |
||||
|
{ |
||||
|
_options.Value.AddWidget(testData.PollName, testData.WidgetName); |
||||
|
_options.Value.AddWidget("ImageGallery", "ImageGallery"); |
||||
|
contentParser = new ContentParser(_options); |
||||
|
|
||||
|
var content = @"**ABP Framework** is completely open source and developed in a community-driven manner.
|
||||
|
[Widget Type=""Poll"" Code=""poll-name""] |
||||
|
Thanks _for_ *your * feedback. |
||||
|
[Widget GalleryName=""Xyz"" Type=""ImageGallery"" Source=""GoogleDrive""]";
|
||||
|
|
||||
|
var widgets = await contentParser.ParseAsync(content); |
||||
|
|
||||
|
widgets.ShouldNotBeNull(); |
||||
|
widgets.Count.ShouldBe(4); |
||||
|
widgets[1].ExtraProperties.Count.ShouldBe(2); |
||||
|
widgets[3].ExtraProperties.Count.ShouldBe(3); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task ParseAsync_ShouldWorkWithoutConfigOptions() |
||||
|
{ |
||||
|
var content = @"**ABP Framework** is completely open source and developed in a community-driven manner.
|
||||
|
[Widget Type= ""Poll"" Code =""poll-name""] |
||||
|
Thanks _for_ *your * feedback.";
|
||||
|
|
||||
|
contentParser = new ContentParser(_options); |
||||
|
var widgets = await contentParser.ParseAsync(content); |
||||
|
|
||||
|
widgets.ShouldNotBeNull(); |
||||
|
widgets.Count.ShouldBe(1);//Ignored Widget
|
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task ParseAsync_ShouldWorkWithWrongConfigOptions() |
||||
|
{ |
||||
|
_options.Value.AddWidget(testData.WidgetName, testData.PollName); |
||||
|
contentParser = new ContentParser(_options); |
||||
|
|
||||
|
var content = @"**ABP Framework** is completely open source and developed in a community-driven manner.
|
||||
|
[Widget Type= ""Poll"" Code =""poll-name""] |
||||
|
Thanks _for_ *your * feedback.";
|
||||
|
|
||||
|
var widgets = await contentParser.ParseAsync(content); |
||||
|
|
||||
|
widgets.ShouldNotBeNull(); |
||||
|
widgets.Count.ShouldBe(2); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task ParseAsync_ShouldWorkWithWrongWidgetType() |
||||
|
{ |
||||
|
_options.Value.AddWidget(testData.PollName, testData.WidgetName); |
||||
|
contentParser = new ContentParser(_options); |
||||
|
|
||||
|
var content = @"**ABP Framework** is completely open source and developed in a community-driven manner.
|
||||
|
[Widget Wrong Type= ""Poll"" Code =""poll-name""] |
||||
|
Thanks _for_ *your * feedback.";
|
||||
|
|
||||
|
var widgets = await contentParser.ParseAsync(content); |
||||
|
|
||||
|
widgets.ShouldNotBeNull(); |
||||
|
widgets.Count.ShouldBe(2); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task ParseAsync_ShouldWorkWithWrongPollName() |
||||
|
{ |
||||
|
_options.Value.AddWidget(testData.PollName, testData.WidgetName); |
||||
|
contentParser = new ContentParser(_options); |
||||
|
|
||||
|
var content = @"**ABP Framework** is completely open source and developed in a community-driven manner.
|
||||
|
[Widget Type= ""Poll"" PollWrongName =""poll-name""] |
||||
|
Thanks _for_ *your * feedback.";
|
||||
|
|
||||
|
var widgets = await contentParser.ParseAsync(content); |
||||
|
|
||||
|
widgets.ShouldNotBeNull(); |
||||
|
widgets.Count.ShouldBe(3); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[MemberData(nameof(ExampleData))] |
||||
|
public async Task ParseAsync_ShouldWorkProperlyWithCorrectInputs(string content, int expectedLine) |
||||
|
{ |
||||
|
_options.Value.AddWidget(testData.PollName, testData.WidgetName); |
||||
|
contentParser = new ContentParser(_options); |
||||
|
|
||||
|
var widgets = await contentParser.ParseAsync(content); |
||||
|
|
||||
|
widgets.ShouldNotBeNull(); |
||||
|
widgets.Count.ShouldBe(expectedLine); |
||||
|
} |
||||
|
|
||||
|
public static IEnumerable<object[]> ExampleData => |
||||
|
new List<object[]> |
||||
|
{ |
||||
|
new object[] { @"**ABP Framework** is completely open source and developed in a community-driven manner.
|
||||
|
[Widget Type=""Poll"" Code=""poll-name""] |
||||
|
Thanks _for_ *your * feedback.", 3},
|
||||
|
|
||||
|
new object[] { @"**ABP Framework** is completely open source and developed in a community-driven manner.
|
||||
|
[Widget Type=""Poll"" Code=""poll-name""] |
||||
|
Thanks _for_ *your * feedback. |
||||
|
[Widget Type=""Poll"" Code=""poll-name1""]", 4 },
|
||||
|
|
||||
|
new object[] { @"**ABP Framework** is completely open source and developed in a community-driven manner.
|
||||
|
Thanks _for_ *your * feedback. |
||||
|
[Widget Type=""Poll"" Code=""poll-name""]", 2 },
|
||||
|
|
||||
|
new object[] { @"[Widget Type=""Poll"" Code=""poll-name""] gg [Widget Type=""Poll"" Code=""poll-name1""]**ABP Framework** is completely open source and developed in a community-driven manner.
|
||||
|
Thanks _for_ *your * feedback. |
||||
|
Thanks _for_ *your * feedback.", 4},
|
||||
|
|
||||
|
new object[] { @"Thanks _for_ *your * feedback.
|
||||
|
Thanks _for_ *your * feedback.", 1}
|
||||
|
}; |
||||
|
} |
||||
Loading…
Reference in new issue