-
Notifications
You must be signed in to change notification settings - Fork 43
/
Copy pathInlineContentItemsProcessor.cs
135 lines (121 loc) · 6.85 KB
/
InlineContentItemsProcessor.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using AngleSharp.Dom;
using AngleSharp.Html.Dom;
using AngleSharp.Html.Parser;
using Kentico.Kontent.Delivery.Abstractions;
using Kentico.Kontent.Delivery.Builders.DeliveryClient;
using Microsoft.Extensions.DependencyInjection;
namespace Kentico.Kontent.Delivery.ContentItems.InlineContentItems
{
/// <summary>
/// Processor responsible for parsing HTML input and resolving inline content items referenced in them using registered resolvers
/// </summary>
internal class InlineContentItemsProcessor : IInlineContentItemsProcessor
{
private readonly IDictionary<Type, Func<object, string>> _inlineContentItemsResolvers;
private readonly HtmlParser _htmlParser;
private readonly HtmlParser _strictHtmlParser;
internal IReadOnlyDictionary<Type, Func<object, string>> ContentItemResolvers => new ReadOnlyDictionary<Type, Func<object, string>>(_inlineContentItemsResolvers);
/// <summary>
/// Inline content item processor, going through HTML and replacing content items marked as object elements with output of resolvers.
/// </summary>
/// <remarks>
/// The collection of resolvers may contain a custom <see cref="object"/> resolver used when no content type specific resolver was registered,
/// a custom resolver for <see cref="UnretrievedContentItem"/>s used when an item was not retrieved from Delivery API,
/// and a resolver for <see cref="UnknownContentItem"/> that a <see cref="IModelProvider"/> was unable to strongly type.
/// If these resolvers are not specified using <see cref="DeliveryClientBuilder"/> or the <see cref="IServiceCollection"/> registration
/// (<see cref="Extensions.ServiceCollectionExtensions.AddDeliveryInlineContentItemsResolver{TContentItem,TInlineContentItemsResolver}"/>),
/// the default implementations resulting in warning messages will be used.
/// </remarks>
/// <param name="inlineContentItemsResolvers">Collection of inline content item resolvers.</param>
public InlineContentItemsProcessor(IEnumerable<ITypelessInlineContentItemsResolver> inlineContentItemsResolvers)
{
_inlineContentItemsResolvers = inlineContentItemsResolvers
.GroupBy(descriptor => descriptor.ContentItemType)
.Select(descriptorGroup => descriptorGroup.Last())
.ToDictionary<ITypelessInlineContentItemsResolver, Type, Func<object, string>>(
descriptor => descriptor.ContentItemType,
descriptor => descriptor.ResolveItem);
//TODO: inject parser (performance)
_htmlParser = new HtmlParser();
_strictHtmlParser = new HtmlParser(new HtmlParserOptions
{
IsStrictMode = true
});
}
/// <summary>
/// Processes HTML input and returns it with inline content items replaced with resolvers output.
/// </summary>
/// <param name="value">HTML code</param>
/// <param name="inlineContentItemMap">Content items referenced as inline content items</param>
/// <returns>HTML with inline content items replaced with resolvers output</returns>
public async Task<string> ProcessAsync(string value, Dictionary<string, object> inlineContentItemMap)
{
var document = await _htmlParser.ParseDocumentAsync(value);
var inlineContentItemElements = GetInlineContentItemElements(document);
foreach (var inlineContentItemElement in inlineContentItemElements)
{
var contentItemCodename = inlineContentItemElement.GetAttribute("data-codename");
if (inlineContentItemMap.TryGetValue(contentItemCodename, out object inlineContentItem))
{
var fragmentText = GetFragmentText(inlineContentItem);
ReplaceElementWithFragmentNodes(document, inlineContentItemElement, contentItemCodename, inlineContentItem, fragmentText);
}
}
return document.Body.InnerHtml;
}
/// <summary>
/// Removes all content items from given HTML content.
/// </summary>
/// <param name="value">HTML content</param>
/// <returns>HTML without inline content items</returns>
public async Task<string> RemoveAllAsync(string value)
{
var htmlInput = await _htmlParser.ParseDocumentAsync(value);
List<IElement> inlineContentItems = GetInlineContentItemElements(htmlInput);
foreach (var contentItem in inlineContentItems)
{
contentItem.Remove();
}
return htmlInput.Body.InnerHtml;
}
private static List<IElement> GetInlineContentItemElements(IHtmlDocument htmlInput)
=> htmlInput
.Body
.GetElementsByTagName("object")
.Where(o => o.GetAttribute("type") == "application/kenticocloud" && o.GetAttribute("data-type") == "item")
.ToList();
private string GetFragmentText(object inlineContentItem)
{
var inlineContentItemType = GetInlineContentItemType(inlineContentItem);
if (_inlineContentItemsResolvers.TryGetValue(inlineContentItemType, out var inlineContentItemResolver))
{
return inlineContentItemResolver(inlineContentItem);
}
if (_inlineContentItemsResolvers.TryGetValue(typeof(object), out var defaultContentItemResolver))
{
return defaultContentItemResolver(inlineContentItem);
}
return "Default inline content item resolver for non specific content type was not registered.";
}
private static Type GetInlineContentItemType(object inlineContentItem)
=> inlineContentItem?.GetType() ?? typeof(UnknownContentItem);
private void ReplaceElementWithFragmentNodes(IHtmlDocument document, IElement inlineContentItemElement, string contentItemCodename, object inlineContentItem, string fragmentText)
{
try
{
var fragmentNodes = _strictHtmlParser.ParseFragment(fragmentText, inlineContentItemElement.ParentElement);
inlineContentItemElement.Replace(fragmentNodes.ToArray());
}
catch (HtmlParseException exception)
{
var errorNode = document.CreateTextNode($"[Inline content item resolver provided an invalid HTML 5 fragment ({exception.Position.Line}:{exception.Position.Column}). Please check the output for a content item {contentItemCodename} of type {GetInlineContentItemType(inlineContentItem)}.]");
inlineContentItemElement.Replace(errorNode);
}
}
}
}