Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Apps.Contentful/Apps.Contentful.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
<Product>Contentful</Product>
<Description>The headless content management system</Description>
<Version>1.8.13</Version>
<Version>1.8.14</Version>
<AssemblyName>Apps.Contentful</AssemblyName>
</PropertyGroup>

Expand Down
14 changes: 13 additions & 1 deletion Apps.Contentful/HtmlHelpers/EntryToHtmlConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,19 @@ private string ConvertCustomFieldToHtml(JToken quoteToken)
if (content is null)
return default;

var richTextToHtmlConverter = new RichTextToHtmlConverter(content, spaceId);
Func<string, ManagementAsset?>? assetResolver = null;
if (includeReferencedAssets)
{
var client = new ContentfulClient(invocationContext.AuthenticationCredentialsProviders, environment);
assetResolver = assetId =>
{
var assetTask = client.GetAsset(assetId);
assetTask.Wait();
return assetTask.Result;
Comment thread
RiabushenkoA marked this conversation as resolved.
};
}

var richTextToHtmlConverter = new RichTextToHtmlConverter(content, spaceId, assetResolver);
var fieldContent = richTextToHtmlConverter.ToHtml();

return WrapFieldInDiv(doc, entryId, field, fieldContent);
Expand Down
92 changes: 91 additions & 1 deletion Apps.Contentful/HtmlHelpers/HtmlToRichTextConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ private void ParseHtmlToContentful(HtmlNode node, List<IContent> contentList)
case "a":
content = CreateHyperlink(childNode);
break;
case "img":
content = CreateAssetFromImage(childNode);
break;
default:
ParseHtmlToContentful(childNode, contentList);
break;
Expand Down Expand Up @@ -513,6 +516,78 @@ private IContent CreateHyperlink(HtmlNode node)
}
}

private IContent? CreateAssetFromImage(HtmlNode node)
{
var assetId = node.GetAttributeValue("data-contentful-link-id", "");
var nodeType = node.GetAttributeValue("data-contentful-rich-text-node-type", "");

if (string.IsNullOrEmpty(assetId))
{
var id = node.GetAttributeValue("id", "");
if (TryParseRichTextAssetId(id, out nodeType, out assetId) == false)
return null;
}

if (string.IsNullOrEmpty(nodeType))
nodeType = "embedded-asset-block";

if (nodeType != "embedded-asset-block" && nodeType != "embedded-asset-inline")
return null;

return new AssetHyperlink
{
NodeType = nodeType,
Content = new List<IContent>(),
Data = new AssetHyperlinkData
{
Target = new Asset
{
SystemProperties = new SystemProperties
{
Id = assetId,
Type = "Link",
LinkType = "Asset"
}
}
}
};
}

private static bool TryParseRichTextAssetId(string id, out string nodeType, out string assetId)
{
nodeType = "";
assetId = "";

const string blockPrefix = "embedded-asset-block_";
const string inlinePrefix = "embedded-asset-inline_";

if (id.StartsWith(blockPrefix))
{
nodeType = "embedded-asset-block";
assetId = id[blockPrefix.Length..];
return !string.IsNullOrEmpty(assetId);
}

if (id.StartsWith(inlinePrefix))
{
nodeType = "embedded-asset-inline";
assetId = id[inlinePrefix.Length..];
return !string.IsNullOrEmpty(assetId);
}

return false;
}

private static string GetAssetNodeTypeFromImage(HtmlNode node)
{
var nodeType = node.GetAttributeValue("data-contentful-rich-text-node-type", "");
if (!string.IsNullOrEmpty(nodeType))
return nodeType;

var id = node.GetAttributeValue("id", "");
return TryParseRichTextAssetId(id, out nodeType, out _) ? nodeType : "embedded-asset-block";
}

private string GetHyperlinkTextWithNewlines(HtmlNode node)
{
var text = new StringBuilder();
Expand Down Expand Up @@ -651,6 +726,21 @@ private void GetMarksFromHtmlNode(HtmlNode node, List<string> marks)
parentContentList.Add(CreateHyperlink(child));
}
}
else if (child.Name == "img")
{
var assetContent = CreateAssetFromImage(child);
if (assetContent == null)
continue;

if (GetAssetNodeTypeFromImage(child) == "embedded-asset-block")
{
parentContentList.Add(assetContent);
}
else
{
paragraph.Content.Add(assetContent);
}
}
else
{
ParseHtmlToContentful(child, paragraph.Content);
Expand Down Expand Up @@ -683,4 +773,4 @@ private void GetMarksFromHtmlNode(HtmlNode node, List<string> marks)

return paragraph;
}
}
}
79 changes: 77 additions & 2 deletions Apps.Contentful/HtmlHelpers/RichTextToHtmlConverter.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
using System.Text;
using Contentful.Core.Models.Management;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Apps.Contentful.HtmlHelpers;

public class RichTextToHtmlConverter(JArray content, string spaceId)
public class RichTextToHtmlConverter(
JArray content,
string spaceId,
Func<string, ManagementAsset?>? assetResolver = null)
{
public string ToHtml()
{
Expand Down Expand Up @@ -80,12 +84,83 @@ private string ConvertJsonObjectToHtml(JObject jsonObject)
case "embedded-asset-block":
assetId = jsonObject["data"]["target"]["sys"]["id"].ToString();
uri = $"https://app.contentful.com/spaces/{spaceId}/assets/{assetId}";
var imageHtml = ConvertEmbeddedAssetToImageHtml(assetId, nodeType);
if (imageHtml != null)
{
return imageHtml;
}

return $"<a id=\"{nodeType}_{assetId}\" href=\"{uri}\">Asset {assetId}</a>";
default:
return ConvertContentToHtml(jsonObject["content"]);
}
}

private string? ConvertEmbeddedAssetToImageHtml(string assetId, string nodeType)
{
if (assetResolver == null)
return null;

ManagementAsset? asset;
try
{
asset = assetResolver(assetId);
}
catch
{
return null;
}

if (asset?.Files == null)
return null;

foreach (var fileEntry in asset.Files)
{
var file = fileEntry.Value;
if (!file.ContentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
continue;

var imageUrl = NormalizeImageUrl(file.Url);
var htmlBuilder = new StringBuilder();
htmlBuilder.Append($"<img id=\"{nodeType}_{assetId}\"");
htmlBuilder.Append($" src=\"{HtmlEncode(imageUrl)}\"");
htmlBuilder.Append(" data-contentful-link-type=\"Asset\"");
htmlBuilder.Append($" data-contentful-link-id=\"{HtmlEncode(assetId)}\"");
htmlBuilder.Append($" data-contentful-rich-text-node-type=\"{HtmlEncode(nodeType)}\"");

var fileLocale = fileEntry.Key;
var altText = asset.Title?.ContainsKey(fileLocale) ?? false
? asset.Title[fileLocale]
: "";
if (!string.IsNullOrWhiteSpace(altText))
{
htmlBuilder.Append($" alt=\"{HtmlEncode(altText)}\"");
}

htmlBuilder.Append(" />");
return htmlBuilder.ToString();
}

return null;
}

private static string NormalizeImageUrl(string imageUrl)
{
if (imageUrl.StartsWith("//"))
{
return "https:" + imageUrl;
}

if (imageUrl.StartsWith("/"))
{
return "https://images.ctfassets.net" + imageUrl;
}

return imageUrl;
}

private static string HtmlEncode(string value) => System.Net.WebUtility.HtmlEncode(value);

private string ConvertHeadingToHtml(JObject jsonObject, string nodeType)
{
var tagName = nodeType.Replace("heading-", "h");
Expand Down Expand Up @@ -221,4 +296,4 @@ private void GetMarksHtml(JToken marks, out string openingMarks, out string clos
openingMarks = openingMarksBuilder.ToString();
closingMarks = closingMarksBuilder.ToString();
}
}
}