Summary
public static class RtePropertyConverter
{
private static readonly Regex RteBlockPattern = new(
@“<umb-rte-block.(?data-content-udi)=“”(?[^“”])”“.*”,
RegexOptions.IgnoreCase | RegexOptions.Singleline);
private static readonly Regex LegacyLocalLinkPattern = new(
@"/{localLink:([a-fA-F0-9-]+)}",
RegexOptions.IgnoreCase);
private static readonly Regex FaultyHrefPattern = new(
@"<a\s+(?<faultyHref>href=['""]?\s*(?<typeAttribute>type=['""][^'""]*?['""]?)?(?<localLink>/{localLink:[a-fA-F0-9-]+}['""]?)).*?>",
RegexOptions.IgnoreCase | RegexOptions.Singleline);
made
public static ConversionResult ConvertRtePropertyData(string originalJson)
{
var result = new ConversionResult
{
OriginalJson = originalJson,
UpdatedJson = originalJson,
HasChanges = false,
ChangesSummary = new List()
};
if (string.IsNullOrWhiteSpace(originalJson))
{
result.ChangesSummary.Add("Skipped: Empty or null JSON");
return result;
}
try
{
using var jsonDoc = JsonDocument.Parse(originalJson);
if (!jsonDoc.RootElement.TryGetProperty("markup", out var markupElement))
{
result.ChangesSummary.Add("Skipped: No 'markup' property found");
return result;
}
string originalMarkup = markupElement.GetString() ?? "";
var convertedJson = ConvertJsonStructure(jsonDoc, result.ChangesSummary);
if (convertedJson != originalJson)
{
result.UpdatedJson = convertedJson;
result.HasChanges = true;
result.ChangesSummary.Add("Successfully converted JSON structure to V15 format");
}
else
{
result.ChangesSummary.Add("No structural changes needed");
}
return result;
}
catch (JsonException ex)
{
result.HasError = true;
result.ErrorMessage = $"Invalid JSON: {ex.Message}";
result.ChangesSummary.Add($"Error: {ex.Message}");
return result;
}
catch (Exception ex)
{
result.HasError = true;
result.ErrorMessage = $"Conversion error: {ex.Message}";
result.ChangesSummary.Add($"Error: {ex.Message}");
return result;
}
}
public static Dictionary<int, ConversionResult> ConvertMultipleRteProperties(Dictionary<int, string> propertyDataEntries)
{
var results = new Dictionary<int, ConversionResult>();
foreach (var entry in propertyDataEntries)
{
results[entry.Key] = ConvertRtePropertyData(entry.Value);
}
return results;
}
private static string ConvertJsonStructure(JsonDocument originalDoc, List<string> changesSummary)
{
// First pass: Build UDI to Key mapping from blocks
var udiToKeyMap = new Dictionary<string, string>();
if (originalDoc.RootElement.TryGetProperty("blocks", out var blocksElement) &&
blocksElement.TryGetProperty("contentData", out var contentDataElement))
{
foreach (var contentItem in contentDataElement.EnumerateArray())
{
if (contentItem.TryGetProperty("udi", out var udiProp))
{
var udiValue = udiProp.GetString();
if (!string.IsNullOrEmpty(udiValue))
{
if (TryParseGuidFromUdi(udiValue, out var existingGuid))
{
udiToKeyMap[udiValue] = existingGuid.ToString("D");
}
else
{
udiToKeyMap[udiValue] = Guid.NewGuid().ToString("D");
}
}
}
else
{
var newKey = Guid.NewGuid().ToString("D");
}
}
}
var options = new JsonWriterOptions { Indented = false };
using var stream = new MemoryStream();
using var writer = new Utf8JsonWriter(stream, options);
writer.WriteStartObject();
foreach (var property in originalDoc.RootElement.EnumerateObject())
{
switch (property.Name.ToLower())
{
case "markup":
var convertedMarkup = ConvertMarkup(property.Value.GetString() ?? "", udiToKeyMap, changesSummary);
writer.WriteString("markup", convertedMarkup);
break;
case "blocks":
ConvertBlocksStructure(writer, property.Value, udiToKeyMap, changesSummary);
break;
default:
property.WriteTo(writer);
break;
}
}
writer.WriteEndObject();
writer.Flush();
return System.Text.Encoding.UTF8.GetString(stream.ToArray());
}
private static string ConvertMarkup(string markup, Dictionary<string, string> udiToKeyMap, List<string> changesSummary)
{
int rteBlockConversions = 0;
var convertedMarkup = RteBlockPattern.Replace(markup, match =>
{
var udiValue = match.Groups["udi"].Value;
string newKey = null;
if (udiToKeyMap.TryGetValue(udiValue, out var mappedKey))
{
newKey = mappedKey;
}
else if (TryParseGuidFromUdi(udiValue, out var guid))
{
newKey = guid.ToString("D");
}
if (newKey != null)
{
rteBlockConversions++;
return match.Value
.Replace(match.Groups["attribute"].Value, "data-content-key")
.Replace(match.Groups["udi"].Value, newKey);
}
return match.Value;
});
convertedMarkup = ConvertLegacyLocalLinks(convertedMarkup, changesSummary);
convertedMarkup = FixFaultyHrefAttributes(convertedMarkup, changesSummary);
if (rteBlockConversions > 0)
{
changesSummary.Add($"Converted {rteBlockConversions} RTE block UDI references to content keys");
}
return convertedMarkup;
}
private static void ConvertBlocksStructure(Utf8JsonWriter writer, JsonElement blocksElement, Dictionary<string, string> udiToKeyMap, List<string> changesSummary)
{
writer.WritePropertyName("blocks");
writer.WriteStartObject();
var contentDataConverted = 0;
var exposeEntries = new List<string>();
foreach (var blocksProperty in blocksElement.EnumerateObject())
{
switch (blocksProperty.Name.ToLower())
{
case "contentdata":
writer.WritePropertyName("contentData");
writer.WriteStartArray();
foreach (var contentItem in blocksProperty.Value.EnumerateArray())
{
var contentKey = ConvertContentDataItem(writer, contentItem, udiToKeyMap, changesSummary);
if (!string.IsNullOrEmpty(contentKey))
{
exposeEntries.Add(contentKey);
}
contentDataConverted++;
}
writer.WriteEndArray();
break;
case "settingsdata":
writer.WritePropertyName("settingsData");
writer.WriteStartArray();
foreach (var settingsItem in blocksProperty.Value.EnumerateArray())
{
ConvertSettingsDataItem(writer, settingsItem, udiToKeyMap);
}
writer.WriteEndArray();
break;
case "layout":
ConvertLayoutStructure(writer, blocksProperty.Value, udiToKeyMap, changesSummary);
break;
default:
blocksProperty.WriteTo(writer);
break;
}
}
writer.WritePropertyName("expose");
writer.WriteStartArray();
foreach (var contentKey in exposeEntries)
{
writer.WriteStartObject();
writer.WriteString("contentKey", contentKey);
writer.WriteNull("culture");
writer.WriteNull("segment");
writer.WriteEndObject();
}
writer.WriteEndArray();
if (exposeEntries.Any())
{
changesSummary.Add($"Added expose array with {exposeEntries.Count} entries");
}
writer.WriteEndObject();
if (contentDataConverted > 0)
{
changesSummary.Add($"Converted {contentDataConverted} content data items to V15 structure");
}
}
private static string ConvertContentDataItem(Utf8JsonWriter writer, JsonElement contentItem, Dictionary<string, string> udiToKeyMap, List<string> changesSummary)
{
writer.WriteStartObject();
string? contentKey = null;
var valuesProperties = new List<(string name, JsonElement value)>();
foreach (var property in contentItem.EnumerateObject())
{
switch (property.Name.ToLower())
{
case "udi":
var udiValue = property.Value.GetString();
if (!string.IsNullOrEmpty(udiValue))
{
if (udiToKeyMap.TryGetValue(udiValue, out var mappedKey))
{
contentKey = mappedKey;
}
else if (TryParseGuidFromUdi(udiValue, out var guid))
{
contentKey = guid.ToString("D");
udiToKeyMap[udiValue] = contentKey; // Update mapping
}
else
{
contentKey = Guid.NewGuid().ToString("D");
udiToKeyMap[udiValue] = contentKey;
}
writer.WriteString("key", contentKey);
writer.WriteNull("udi"); // Set udi to null in V15+
}
else
{
contentKey = Guid.NewGuid().ToString("D");
writer.WriteString("key", contentKey);
writer.WriteNull("udi");
}
break;
case "contenttypekey":
property.WriteTo(writer);
break;
default:
valuesProperties.Add((property.Name, property.Value));
break;
}
}
if (contentKey == null)
{
contentKey = Guid.NewGuid().ToString("D");
writer.WriteString("key", contentKey);
writer.WriteNull("udi");
}
if (valuesProperties.Any())
{
writer.WritePropertyName("values");
writer.WriteStartArray();
foreach (var (name, value) in valuesProperties)
{
writer.WriteStartObject();
var editorAlias = DetermineEditorAlias(name, value);
writer.WriteString("editorAlias", editorAlias);
writer.WriteNull("culture");
writer.WriteNull("segment");
writer.WriteString("alias", name);
var convertedValue = ConvertPropertyValue(value, editorAlias);
writer.WriteString("value", convertedValue);
writer.WriteEndObject();
}
writer.WriteEndArray();
}
writer.WriteEndObject();
return contentKey ?? "";
}
private static void ConvertSettingsDataItem(Utf8JsonWriter writer, JsonElement settingsItem, Dictionary<string, string> udiToKeyMap)
{
writer.WriteStartObject();
foreach (var property in settingsItem.EnumerateObject())
{
if (property.Name.ToLower() == "udi")
{
var udiValue = property.Value.GetString();
if (!string.IsNullOrEmpty(udiValue))
{
if (udiToKeyMap.TryGetValue(udiValue, out var mappedKey))
{
writer.WriteString("key", mappedKey);
}
else if (TryParseGuidFromUdi(udiValue, out var guid))
{
var key = guid.ToString("D");
writer.WriteString("key", key);
udiToKeyMap[udiValue] = key;
}
else
{
var newKey = Guid.NewGuid().ToString("D");
writer.WriteString("key", newKey);
udiToKeyMap[udiValue] = newKey;
}
writer.WriteNull("udi");
}
else
{
writer.WriteString("key", Guid.NewGuid().ToString("D"));
writer.WriteNull("udi");
}
}
else
{
property.WriteTo(writer);
}
}
writer.WriteEndObject();
}
private static void ConvertLayoutStructure(Utf8JsonWriter writer, JsonElement layoutElement, Dictionary<string, string> udiToKeyMap, List<string> changesSummary)
{
writer.WritePropertyName("Layout");
writer.WriteStartObject();
foreach (var layoutProperty in layoutElement.EnumerateObject())
{
var propertyName = layoutProperty.Name == "Umbraco.TinyMCE" ? "Umbraco.RichText" : layoutProperty.Name;
writer.WritePropertyName(propertyName);
writer.WriteStartArray();
foreach (var layoutItem in layoutProperty.Value.EnumerateArray())
{
writer.WriteStartObject();
foreach (var itemProperty in layoutItem.EnumerateObject())
{
switch (itemProperty.Name.ToLower())
{
case "contentudi":
var udiValue = itemProperty.Value.GetString();
if (!string.IsNullOrEmpty(udiValue))
{
if (udiToKeyMap.TryGetValue(udiValue, out var mappedKey))
{
writer.WriteString("contentKey", mappedKey);
}
else if (TryParseGuidFromUdi(udiValue, out var guid))
{
var key = guid.ToString("D");
writer.WriteString("contentKey", key);
udiToKeyMap[udiValue] = key;
}
else
{
writer.WriteNull("contentKey");
}
}
else
{
writer.WriteNull("contentKey");
}
writer.WriteNull("contentUdi");
break;
case "settingsudi":
var settingsUdiValue = itemProperty.Value.GetString();
if (!string.IsNullOrEmpty(settingsUdiValue))
{
if (udiToKeyMap.TryGetValue(settingsUdiValue, out var mappedSettingsKey))
{
writer.WriteString("settingsKey", mappedSettingsKey);
}
else if (TryParseGuidFromUdi(settingsUdiValue, out var settingsGuid))
{
var settingsKey = settingsGuid.ToString("D");
writer.WriteString("settingsKey", settingsKey);
udiToKeyMap[settingsUdiValue] = settingsKey;
}
else
{
writer.WriteNull("settingsKey");
}
}
else
{
writer.WriteNull("settingsKey");
}
writer.WriteNull("settingsUdi");
break;
default:
itemProperty.WriteTo(writer);
break;
}
}
writer.WriteEndObject();
}
writer.WriteEndArray();
}
writer.WriteEndObject();
changesSummary.Add("Converted layout structure to use keys instead of UDIs");
}
private static string DetermineEditorAlias(string propertyName, JsonElement value)
{
switch (propertyName.ToLower())
{
case "content" when IsRichTextValue(value):
return "Umbraco.RichText";
case "title":
case "headline":
case "name":
return "Umbraco.TextBox";
case var name when name.Contains("items") && IsBlockListValue(value):
return "Umbraco.BlockList";
case var name when name.Contains("grid") && IsBlockGridValue(value):
return "Umbraco.BlockGrid";
default:
// Try to infer from value structure
if (IsRichTextValue(value))
return "Umbraco.RichText";
if (IsBlockListValue(value))
return "Umbraco.BlockList";
if (IsBlockGridValue(value))
return "Umbraco.BlockGrid";
return "Umbraco.TextBox"; // Default fallback
}
}
private static bool IsRichTextValue(JsonElement value)
{
if (value.ValueKind != JsonValueKind.String)
return false;
var stringValue = value.GetString() ?? "";
if (string.IsNullOrEmpty(stringValue))
return false;
try
{
using var doc = JsonDocument.Parse(stringValue);
return doc.RootElement.TryGetProperty("markup", out _);
}
catch
{
return false;
}
}
private static bool IsBlockListValue(JsonElement value)
{
if (value.ValueKind != JsonValueKind.String)
return false;
var stringValue = value.GetString() ?? "";
if (string.IsNullOrEmpty(stringValue))
return false;
try
{
using var doc = JsonDocument.Parse(stringValue);
return doc.RootElement.TryGetProperty("contentData", out _) &&
doc.RootElement.TryGetProperty("Layout", out var layout) &&
layout.TryGetProperty("Umbraco.BlockList", out _);
}
catch
{
return false;
}
}
private static bool IsBlockGridValue(JsonElement value)
{
if (value.ValueKind != JsonValueKind.String)
return false;
var stringValue = value.GetString() ?? "";
if (string.IsNullOrEmpty(stringValue))
return false;
try
{
using var doc = JsonDocument.Parse(stringValue);
return doc.RootElement.TryGetProperty("contentData", out _) &&
doc.RootElement.TryGetProperty("Layout", out var layout) &&
layout.TryGetProperty("Umbraco.BlockGrid", out _);
}
catch
{
return false;
}
}
private static string ConvertPropertyValue(JsonElement value, string editorAlias)
{
if (value.ValueKind != JsonValueKind.String)
return value.ToString() ?? "";
var stringValue = value.GetString() ?? "";
if (editorAlias == "Umbraco.BlockList" && !string.IsNullOrEmpty(stringValue))
{
return ConvertNestedBlockStructure(stringValue);
}
if (editorAlias == "Umbraco.RichText" && IsRichTextValue(value))
{
var nestedResult = ConvertRtePropertyData(stringValue);
return nestedResult.HasChanges ? nestedResult.UpdatedJson : stringValue;
}
if (editorAlias == "Umbraco.BlockGrid" && IsBlockGridValue(value))
{
return ConvertNestedBlockStructure(stringValue);
}
return stringValue;
}
private static string ConvertNestedBlockStructure(string blockJson)
{
try
{
using var doc = JsonDocument.Parse(blockJson);
var nestedUdiToKeyMap = new Dictionary<string, string>();
if (doc.RootElement.TryGetProperty("contentData", out var contentData))
{
foreach (var item in contentData.EnumerateArray())
{
if (item.TryGetProperty("udi", out var udiProp))
{
var udiValue = udiProp.GetString();
if (!string.IsNullOrEmpty(udiValue))
{
if (TryParseGuidFromUdi(udiValue, out var guid))
{
nestedUdiToKeyMap[udiValue] = guid.ToString("D");
}
else
{
nestedUdiToKeyMap[udiValue] = Guid.NewGuid().ToString("D");
}
}
}
}
}
var changesSummary = new List<string>();
var convertedJson = ConvertNestedJsonStructure(doc, nestedUdiToKeyMap, changesSummary);
return convertedJson;
}
catch (JsonException)
{
return blockJson;
}
}
private static string ConvertNestedJsonStructure(JsonDocument originalDoc, Dictionary<string, string> udiToKeyMap, List<string> changesSummary)
{
var options = new JsonWriterOptions { Indented = false };
using var stream = new MemoryStream();
using var writer = new Utf8JsonWriter(stream, options);
writer.WriteStartObject();
foreach (var property in originalDoc.RootElement.EnumerateObject())
{
switch (property.Name.ToLower())
{
case "contentdata":
writer.WritePropertyName("contentData");
writer.WriteStartArray();
var exposeEntries = new List<string>();
foreach (var contentItem in property.Value.EnumerateArray())
{
var contentKey = ConvertContentDataItem(writer, contentItem, udiToKeyMap, changesSummary);
if (!string.IsNullOrEmpty(contentKey))
{
exposeEntries.Add(contentKey);
}
}
writer.WriteEndArray();
if (!originalDoc.RootElement.TryGetProperty("expose", out _))
{
foreach (var key in exposeEntries)
{
udiToKeyMap[$"expose_{key}"] = key; // Store for expose section
}
}
break;
case "settingsdata":
writer.WritePropertyName("settingsData");
writer.WriteStartArray();
foreach (var settingsItem in property.Value.EnumerateArray())
{
ConvertSettingsDataItem(writer, settingsItem, udiToKeyMap);
}
writer.WriteEndArray();
break;
case "layout":
ConvertNestedLayoutStructure(writer, property.Value, udiToKeyMap);
break;
case "expose":
property.WriteTo(writer);
break;
default:
property.WriteTo(writer);
break;
}
}
if (!originalDoc.RootElement.TryGetProperty("expose", out _))
{
writer.WritePropertyName("expose");
writer.WriteStartArray();
var exposeKeys = udiToKeyMap.Where(kvp => kvp.Key.StartsWith("expose_")).Select(kvp => kvp.Value);
foreach (var key in exposeKeys)
{
writer.WriteStartObject();
writer.WriteString("contentKey", key);
writer.WriteNull("culture");
writer.WriteNull("segment");
writer.WriteEndObject();
}
writer.WriteEndArray();
}
writer.WriteEndObject();
writer.Flush();
return System.Text.Encoding.UTF8.GetString(stream.ToArray());
}
private static void ConvertNestedLayoutStructure(Utf8JsonWriter writer, JsonElement layoutElement, Dictionary<string, string> udiToKeyMap)
{
writer.WritePropertyName("Layout");
writer.WriteStartObject();
foreach (var layoutProperty in layoutElement.EnumerateObject())
{
writer.WritePropertyName(layoutProperty.Name);
writer.WriteStartArray();
foreach (var layoutItem in layoutProperty.Value.EnumerateArray())
{
writer.WriteStartObject();
foreach (var itemProperty in layoutItem.EnumerateObject())
{
switch (itemProperty.Name.ToLower())
{
case "contentudi":
var udiValue = itemProperty.Value.GetString();
if (!string.IsNullOrEmpty(udiValue) && udiToKeyMap.TryGetValue(udiValue, out var mappedKey))
{
writer.WriteString("contentKey", mappedKey);
}
else
{
writer.WriteNull("contentKey");
}
writer.WriteNull("contentUdi");
break;
case "settingsudi":
var settingsUdiValue = itemProperty.Value.GetString();
if (!string.IsNullOrEmpty(settingsUdiValue) && udiToKeyMap.TryGetValue(settingsUdiValue, out var mappedSettingsKey))
{
writer.WriteString("settingsKey", mappedSettingsKey);
}
else
{
writer.WriteNull("settingsKey");
}
writer.WriteNull("settingsUdi");
break;
default:
itemProperty.WriteTo(writer);
break;
}
}
writer.WriteEndObject();
}
writer.WriteEndArray();
}
writer.WriteEndObject();
}
private static string ConvertLegacyLocalLinks(string markup, List<string> changesSummary)
{
int conversions = 0;
var result = LegacyLocalLinkPattern.Replace(markup, match =>
{
var guidValue = match.Groups[1].Value;
if (Guid.TryParse(guidValue, out var guid))
{
conversions++;
return $"/{{{guid}}}\" type=\"Document";
}
return match.Value;
});
return result;
}
private static string FixFaultyHrefAttributes(string markup, List<string> changesSummary)
{
int fixes = 0;
var result = FaultyHrefPattern.Replace(markup, match =>
{
var typeAttribute = match.Groups["typeAttribute"].Value;
var localLink = match.Groups["localLink"].Value;
if (!string.IsNullOrEmpty(typeAttribute) && !string.IsNullOrEmpty(localLink))
{
fixes++;
var correctedHref = $"href=\"{localLink.Trim('\"')}\" {typeAttribute.Trim()}";
return match.Value.Replace(match.Groups["faultyHref"].Value, correctedHref);
}
return match.Value;
});
return result;
}
private static bool TryParseGuidFromUdi(string udiString, out Guid guid)
{
guid = Guid.Empty;
if (udiString.StartsWith("umb://"))
{
var parts = udiString.Split('/');
if (parts.Length >= 3 && Guid.TryParse(parts[2], out guid))
{
return true;
}
}
else if (Guid.TryParse(udiString, out guid))
{
return true;
}
return false;
}
public class ConversionResult
{
public string OriginalJson { get; set; } = "";
public string UpdatedJson { get; set; } = "";
public bool HasChanges { get; set; }
public bool HasError { get; set; }
public string? ErrorMessage { get; set; }
public List<string> ChangesSummary { get; set; } = new();
…