From c1d533f4929938feace468e23e983765304eb537 Mon Sep 17 00:00:00 2001 From: raj pandey Date: Tue, 3 Jun 2025 17:49:28 +0530 Subject: [PATCH] Fix: Newtonsoft to JSON converter --- .../Helpers/NodeParser.cs | 26 +++-- .../Mocks/CustomRenderOptionMock.cs | 12 ++- Contentstack.Utils.Tests/Mocks/GQLModel.cs | 72 ++++---------- Contentstack.Utils/Contentstack.Utils.csproj | 1 + .../Converters/NodeJsonConverter.cs | 77 +++++++++++---- .../Converters/RTEJsonConverter.cs | 94 +++++++++++++------ Contentstack.Utils/Interfaces/IEdges.cs | 10 +- Contentstack.Utils/Models/JsonRTENode.cs | 8 +- Contentstack.Utils/Models/JsonRTENodes.cs | 8 +- Contentstack.Utils/Models/Metadata.cs | 27 ++++-- Contentstack.Utils/Models/Node.cs | 4 +- Contentstack.Utils/Models/Options.cs | 58 ++++++------ 12 files changed, 231 insertions(+), 166 deletions(-) diff --git a/Contentstack.Utils.Tests/Helpers/NodeParser.cs b/Contentstack.Utils.Tests/Helpers/NodeParser.cs index 4ee6ac7..4191c13 100644 --- a/Contentstack.Utils.Tests/Helpers/NodeParser.cs +++ b/Contentstack.Utils.Tests/Helpers/NodeParser.cs @@ -1,8 +1,9 @@ -using Contentstack.Utils.Interfaces; +using System.Text.Json; +using System.Text.Json.Serialization; +using Contentstack.Utils.Interfaces; using Contentstack.Utils.Models; using Contentstack.Utils.Tests.Constants; using Contentstack.Utils.Tests.Mocks; -using Newtonsoft.Json; namespace Contentstack.Utils.Tests.Helpers { @@ -10,10 +11,13 @@ public class NodeParser { public static Node parse(string jsonNode) { - JsonSerializerSettings SerializerSettings = new JsonSerializerSettings(); - JsonSerializer Serializer = JsonSerializer.Create(SerializerSettings); - - return JsonConvert.DeserializeObject(jsonNode, SerializerSettings); + // Remove trailing commas before closing brackets/braces + string cleanedJson = System.Text.RegularExpressions.Regex.Replace( + jsonNode, + @",(\s*[}\]])", + "$1" + ); + return JsonSerializer.Deserialize(cleanedJson); } } public class GQLParser @@ -21,9 +25,13 @@ public class GQLParser public static GQLModel parse(string jsonNode, string embedConnection = null) where T: IEmbeddedObject { var data = JsonToHtmlConstants.KGQLModel(jsonNode, embedConnection); - JsonSerializerSettings SerializerSettings = new JsonSerializerSettings(); - JsonSerializer Serializer = JsonSerializer.Create(SerializerSettings); - return JsonConvert.DeserializeObject>(data, SerializerSettings); + // Remove trailing commas before closing brackets/braces + string cleanedJson = System.Text.RegularExpressions.Regex.Replace( + data, + @",(\s*[}\]])", + "$1" + ); + return JsonSerializer.Deserialize>(cleanedJson); } } diff --git a/Contentstack.Utils.Tests/Mocks/CustomRenderOptionMock.cs b/Contentstack.Utils.Tests/Mocks/CustomRenderOptionMock.cs index 53b37da..827aaa8 100644 --- a/Contentstack.Utils.Tests/Mocks/CustomRenderOptionMock.cs +++ b/Contentstack.Utils.Tests/Mocks/CustomRenderOptionMock.cs @@ -12,14 +12,22 @@ public CustomRenderOptionMock(IEntryEmbedable entry) : base(entry) } public override string RenderNode(string nodeType, Node node, NodeChildrenCallBack callBack) { + string GetAttrString(string key) + { + if (!node.attrs.ContainsKey(key) || node.attrs[key] == null) return ""; + var val = node.attrs[key]; + if (val is string s) return s; + if (val is System.Text.Json.JsonElement je && je.ValueKind == System.Text.Json.JsonValueKind.String) return je.GetString(); + return val.ToString(); + } switch (nodeType) { case "a": if (node.attrs.ContainsKey("target")) { - return $"{callBack(node.children)}"; + return $"{callBack(node.children)}"; } - return $"{callBack(node.children)}"; + return $"{callBack(node.children)}"; } return base.RenderNode(nodeType, node, callBack); } diff --git a/Contentstack.Utils.Tests/Mocks/GQLModel.cs b/Contentstack.Utils.Tests/Mocks/GQLModel.cs index 586ff39..4d3b9a4 100644 --- a/Contentstack.Utils.Tests/Mocks/GQLModel.cs +++ b/Contentstack.Utils.Tests/Mocks/GQLModel.cs @@ -1,74 +1,42 @@ using System; +using System.Text.Json.Serialization; using Contentstack.Utils.Converters; using Contentstack.Utils.Interfaces; using Contentstack.Utils.Models; -using Newtonsoft.Json; namespace Contentstack.Utils.Tests.Mocks { public class GQLModel where T: IEmbeddedObject { - [Newtonsoft.Json.JsonConverter(typeof(RTEJsonConverter))] + [JsonConverter(typeof(RTEJsonConverter))] public JsonRTENodes multiplerte { get; set; } public JsonRTENode singlerte { get; set; } } - [Newtonsoft.Json.JsonConverter(typeof(RTEJsonConverter))] + [JsonConverter(typeof(RTEJsonConverter))] public class EntryModel : IEmbeddedEntry { - [JsonProperty("system.uid")] - public string Uid - { - get; - set; - } - [JsonProperty("system.content_type_uid")] - public string ContentTypeUid - { - get; - set; - } - [JsonProperty("title")] - public string Title - { - get; - set; - } + [JsonPropertyName("system.uid")] + public string Uid { get; set; } + [JsonPropertyName("system.content_type_uid")] + public string ContentTypeUid { get; set; } + [JsonPropertyName("title")] + public string Title { get; set; } } - [Newtonsoft.Json.JsonConverter(typeof(RTEJsonConverter))] + [JsonConverter(typeof(RTEJsonConverter))] public class AssetModel : IEmbeddedAsset { - [JsonProperty("system.uid")] - public string Uid - { - get; - set; - } - [JsonProperty("system.content_type_uid")] - public string ContentTypeUid - { - get; - set; - } - [JsonProperty("title")] - public string Title - { - get; - set; - } - [JsonProperty("filename")] - public string FileName - { - get; - set; - } - [JsonProperty("url")] - public string Url - { - get; - set; - } + [JsonPropertyName("system.uid")] + public string Uid { get; set; } + [JsonPropertyName("system.content_type_uid")] + public string ContentTypeUid { get; set; } + [JsonPropertyName("title")] + public string Title { get; set; } + [JsonPropertyName("filename")] + public string FileName { get; set; } + [JsonPropertyName("url")] + public string Url { get; set; } } } diff --git a/Contentstack.Utils/Contentstack.Utils.csproj b/Contentstack.Utils/Contentstack.Utils.csproj index b01be4c..ddd2ac3 100644 --- a/Contentstack.Utils/Contentstack.Utils.csproj +++ b/Contentstack.Utils/Contentstack.Utils.csproj @@ -40,5 +40,6 @@ + diff --git a/Contentstack.Utils/Converters/NodeJsonConverter.cs b/Contentstack.Utils/Converters/NodeJsonConverter.cs index 88d22a7..e85e0ba 100644 --- a/Contentstack.Utils/Converters/NodeJsonConverter.cs +++ b/Contentstack.Utils/Converters/NodeJsonConverter.cs @@ -1,31 +1,76 @@ using System; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Collections.Generic; using Contentstack.Utils.Models; namespace Contentstack.Utils.Converters { public class NodeJsonConverter : JsonConverter { - public override Node ReadJson(JsonReader reader, Type objectType, Node existingValue, bool hasExistingValue, JsonSerializer serializer) + public override Node Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - Node node = null; - JObject jObject = JObject.Load(reader); - if (jObject["type"] == null) + using (var jsonDoc = JsonDocument.ParseValue(ref reader)) { - node = new TextNode(); - node.type = "text"; - }else - { - node = new Node(); + var root = jsonDoc.RootElement; + Node node; + if (!root.TryGetProperty("type", out _)) + { + var textNode = new TextNode(); + textNode.type = "text"; + if (root.TryGetProperty("text", out var textProp)) + textNode.text = textProp.GetString() ?? string.Empty; + if (root.TryGetProperty("bold", out var boldProp)) + textNode.bold = boldProp.GetBoolean(); + if (root.TryGetProperty("italic", out var italicProp)) + textNode.italic = italicProp.GetBoolean(); + if (root.TryGetProperty("underline", out var underlineProp)) + textNode.underline = underlineProp.GetBoolean(); + if (root.TryGetProperty("strikethrough", out var strikeProp)) + textNode.strikethrough = strikeProp.GetBoolean(); + if (root.TryGetProperty("inlineCode", out var inlineCodeProp)) + textNode.inlineCode = inlineCodeProp.GetBoolean(); + if (root.TryGetProperty("subscript", out var subscriptProp)) + textNode.subscript = subscriptProp.GetBoolean(); + if (root.TryGetProperty("superscript", out var superscriptProp)) + textNode.superscript = superscriptProp.GetBoolean(); + if (root.TryGetProperty("classname", out var classnameProp)) + textNode.classname = classnameProp.GetString(); + if (root.TryGetProperty("id", out var idProp)) + textNode.id = idProp.GetString(); + node = textNode; + } + else + { + node = new Node(); + } + foreach (var prop in root.EnumerateObject()) + { + switch (prop.Name) + { + case "type": + node.type = prop.Value.GetString(); + break; + case "attrs": + node.attrs = JsonSerializer.Deserialize>(prop.Value.GetRawText(), options); + break; + case "children": + node.children = JsonSerializer.Deserialize>(prop.Value.GetRawText(), options); + break; + } + } + return node; } - serializer.Populate(jObject.CreateReader(), node); - return node; } - - public override void WriteJson(JsonWriter writer, Node value, JsonSerializer serializer) + public override void Write(Utf8JsonWriter writer, Node value, JsonSerializerOptions options) { - + writer.WriteStartObject(); + writer.WriteString("type", value.type); + writer.WritePropertyName("attrs"); + JsonSerializer.Serialize(writer, value.attrs, options); + writer.WritePropertyName("children"); + JsonSerializer.Serialize(writer, value.children, options); + writer.WriteEndObject(); } } } diff --git a/Contentstack.Utils/Converters/RTEJsonConverter.cs b/Contentstack.Utils/Converters/RTEJsonConverter.cs index 57323be..a0cfa5f 100644 --- a/Contentstack.Utils/Converters/RTEJsonConverter.cs +++ b/Contentstack.Utils/Converters/RTEJsonConverter.cs @@ -1,49 +1,83 @@ using System; using System.Linq; using System.Reflection; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; namespace Contentstack.Utils.Converters { - public class RTEJsonConverter : JsonConverter + public class RTEJsonConverter : JsonConverter { - public override bool CanConvert(Type objectType) + public override bool CanConvert(Type typeToConvert) { - throw new NotImplementedException(); + return true; } - - public override object ReadJson(JsonReader reader, Type objectType, - object existingValue, JsonSerializer serializer) + public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - JObject jo = JObject.Load(reader); - object targetObj = Activator.CreateInstance(objectType); - - foreach (PropertyInfo prop in objectType.GetProperties() - .Where(p => p.CanRead && p.CanWrite)) + using (var jsonDoc = JsonDocument.ParseValue(ref reader)) { - JsonPropertyAttribute att = prop.GetCustomAttributes(true) - .OfType() - .FirstOrDefault(); - - string jsonPath = (att != null ? att.PropertyName : prop.Name); - JToken token = jo.SelectToken(jsonPath); - - if (token != null && token.Type != JTokenType.Null) + var root = jsonDoc.RootElement; + object targetObj = Activator.CreateInstance(typeToConvert); + foreach (PropertyInfo prop in typeToConvert.GetProperties(BindingFlags.Public | BindingFlags.Instance)) { - object value = token.ToObject(prop.PropertyType, serializer); - prop.SetValue(targetObj, value, null); + var attr = prop.GetCustomAttribute(); + string jsonPath = attr != null ? attr.Name : prop.Name; + JsonElement token = root; + bool found = false; + // Support nested property names like 'system.uid' + if (jsonPath.Contains(".")) + { + var parts = jsonPath.Split('.'); + JsonElement current = root; + foreach (var part in parts) + { + if (current.ValueKind == JsonValueKind.Object && current.TryGetProperty(part, out var next)) + { + current = next; + found = true; + } + else + { + found = false; + break; + } + } + if (found) + token = current; + } + else if (root.TryGetProperty(jsonPath, out var directToken)) + { + token = directToken; + found = true; + } + if (found) + { + object value = JsonSerializer.Deserialize(token.GetRawText(), prop.PropertyType, options); + prop.SetValue(targetObj, value); + } + else + { + // Set default value for missing properties + if (prop.PropertyType.IsValueType) + prop.SetValue(targetObj, Activator.CreateInstance(prop.PropertyType)); + else + prop.SetValue(targetObj, null); + } } + return targetObj; } - - return targetObj; } - - - public override void WriteJson(JsonWriter writer, object value, - JsonSerializer serializer) + public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) { - + writer.WriteStartObject(); + foreach (PropertyInfo prop in value.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + var attr = prop.GetCustomAttribute(); + string jsonPath = attr != null ? attr.Name : prop.Name; + writer.WritePropertyName(jsonPath); + JsonSerializer.Serialize(writer, prop.GetValue(value), options); + } + writer.WriteEndObject(); } } } diff --git a/Contentstack.Utils/Interfaces/IEdges.cs b/Contentstack.Utils/Interfaces/IEdges.cs index a127b6b..4914dcf 100644 --- a/Contentstack.Utils/Interfaces/IEdges.cs +++ b/Contentstack.Utils/Interfaces/IEdges.cs @@ -1,16 +1,12 @@ using System; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Contentstack.Utils.Interfaces { public class IEdges where T: IEmbeddedObject { - [JsonProperty("node")] - public T Node - { - get; - set; - } + [JsonPropertyName("node")] + public T Node { get; set; } } } diff --git a/Contentstack.Utils/Models/JsonRTENode.cs b/Contentstack.Utils/Models/JsonRTENode.cs index b69a1bc..ef6d05c 100644 --- a/Contentstack.Utils/Models/JsonRTENode.cs +++ b/Contentstack.Utils/Models/JsonRTENode.cs @@ -1,17 +1,17 @@ using System; using System.Collections.Generic; +using System.Text.Json.Serialization; using Contentstack.Utils.Converters; using Contentstack.Utils.Interfaces; -using Newtonsoft.Json; namespace Contentstack.Utils.Models { - [Newtonsoft.Json.JsonConverter(typeof(RTEJsonConverter))] + [JsonConverter(typeof(RTEJsonConverter))] public class JsonRTENode where T: IEmbeddedObject { - [JsonProperty("json")] + [JsonPropertyName("json")] public Node Json { get; set; } - [JsonProperty("embedded_itemsConnection.edges")] + [JsonPropertyName("embedded_itemsConnection.edges")] public List> Edges { get; set; } } } diff --git a/Contentstack.Utils/Models/JsonRTENodes.cs b/Contentstack.Utils/Models/JsonRTENodes.cs index 31dbfcd..fbc90e9 100644 --- a/Contentstack.Utils/Models/JsonRTENodes.cs +++ b/Contentstack.Utils/Models/JsonRTENodes.cs @@ -1,17 +1,17 @@ using System; using System.Collections.Generic; +using System.Text.Json.Serialization; using Contentstack.Utils.Converters; using Contentstack.Utils.Interfaces; -using Newtonsoft.Json; namespace Contentstack.Utils.Models { - [Newtonsoft.Json.JsonConverter(typeof(RTEJsonConverter))] + [JsonConverter(typeof(RTEJsonConverter))] public class JsonRTENodes where T : IEmbeddedObject { - [JsonProperty("json")] + [JsonPropertyName("json")] public List Json { get; set; } - [JsonProperty("embedded_itemsConnection.edges")] + [JsonPropertyName("embedded_itemsConnection.edges")] public List> Edges { get; set; } } } diff --git a/Contentstack.Utils/Models/Metadata.cs b/Contentstack.Utils/Models/Metadata.cs index e87ee17..f0ca222 100644 --- a/Contentstack.Utils/Models/Metadata.cs +++ b/Contentstack.Utils/Models/Metadata.cs @@ -1,7 +1,8 @@ -using System; +using System; using System.Runtime.CompilerServices; using Contentstack.Utils.Enums; using HtmlAgilityPack; +using System.Text.Json; [assembly: InternalsVisibleTo("Contentstack.Utils.Tests")] namespace Contentstack.Utils.Models @@ -58,7 +59,7 @@ public static implicit operator Metadata(HtmlNode node) } return new Metadata() { - Text = node.InnerText ?? "", + Text = node.InnerText ?? string.Empty, OuterHTML = node.OuterHtml ?? "", StyleType = styleType, ItemType = embedItemType, @@ -71,30 +72,36 @@ public static implicit operator Metadata(HtmlNode node) public static implicit operator Metadata(Node node) { StyleType styleType; - if (!node.attrs.ContainsKey("display-type") || !(Enum.TryParse((string)node.attrs["display-type"], true, out styleType))) + string GetAttrString(string key) + { + if (!node.attrs.ContainsKey(key) || node.attrs[key] == null) return ""; + var val = node.attrs[key]; + if (val is string s) return s; + if (val is JsonElement je && je.ValueKind == JsonValueKind.String) return je.GetString(); + return val.ToString(); + } + if (!node.attrs.ContainsKey("display-type") || !(Enum.TryParse(GetAttrString("display-type"), true, out styleType))) { styleType = StyleType.Block; } - EmbedItemType embedItemType; - if (!node.attrs.ContainsKey("type") || !(Enum.TryParse((string)node.attrs["type"], true, out embedItemType))) + if (!node.attrs.ContainsKey("type") || !(Enum.TryParse(GetAttrString("type"), true, out embedItemType))) { embedItemType = EmbedItemType.Entry; } string text = ""; if (node.children != null && node.children.Count > 0 && node.children[0].GetType() == typeof(TextNode)) { - text = ((TextNode)node.children[0]).text; + text = ((TextNode)node.children[0]).text ?? string.Empty; } string itemUID = ""; if (node.attrs.ContainsKey("entry-uid")) { - itemUID = (string)node.attrs["entry-uid"]; + itemUID = GetAttrString("entry-uid"); }else if (node.attrs.ContainsKey("asset-uid")) { - itemUID = (string)node.attrs["asset-uid"]; + itemUID = GetAttrString("asset-uid"); } - return new Metadata() { Text = text, @@ -102,7 +109,7 @@ public static implicit operator Metadata(Node node) StyleType = styleType, ItemType = embedItemType, ItemUid = itemUID, - ContentTypeUid = node.attrs.ContainsKey("content-type-uid") ? (string)node.attrs["content-type-uid"] : "", + ContentTypeUid = node.attrs.ContainsKey("content-type-uid") ? GetAttrString("content-type-uid") : "", attributes = node.attrs }; diff --git a/Contentstack.Utils/Models/Node.cs b/Contentstack.Utils/Models/Node.cs index b64a70e..ef8b054 100644 --- a/Contentstack.Utils/Models/Node.cs +++ b/Contentstack.Utils/Models/Node.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; using Contentstack.Utils.Converters; using Contentstack.Utils.Enums; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace Contentstack.Utils.Models { diff --git a/Contentstack.Utils/Models/Options.cs b/Contentstack.Utils/Models/Options.cs index 7cab2fc..c520f5d 100644 --- a/Contentstack.Utils/Models/Options.cs +++ b/Contentstack.Utils/Models/Options.cs @@ -1,6 +1,7 @@ -using System; +using System; using System.Collections.Generic; -using Contentstack.Utils.Enums; +using System.Text.Json; +using Contentstack.Utils.Enums; using Contentstack.Utils.Interfaces; using Newtonsoft.Json.Linq; @@ -23,14 +24,14 @@ public virtual string RenderOption(IEmbeddedObject embeddedObject, Metadata meta switch (metadata.StyleType) { case Enums.StyleType.Block: - string renderString = "

" + embeddedObject.Uid + "

"; + string renderString = "

" + embeddedObject.Uid + "

"; if (embeddedObject is IEmbeddedEntry) { - renderString += "

Content type: " + ((IEmbeddedEntry)embeddedObject).Title + "

"; + renderString += "

Content type: " + ((IEmbeddedEntry)embeddedObject).Title + "

"; } else { - renderString += "

Content type: " + embeddedObject.ContentTypeUid + "

"; + renderString += "

Content type: " + embeddedObject.ContentTypeUid + "

"; } renderString = renderString + "
"; return renderString; @@ -38,32 +39,32 @@ public virtual string RenderOption(IEmbeddedObject embeddedObject, Metadata meta case Enums.StyleType.Inline: if (embeddedObject is IEmbeddedEntry) { - return "" + ((IEmbeddedEntry)embeddedObject).Title + ""; + return "" + ((IEmbeddedEntry)embeddedObject).Title + ""; } - return "" + embeddedObject.Uid + ""; + return "" + embeddedObject.Uid + ""; case Enums.StyleType.Link: if (embeddedObject is IEmbeddedEntry) { - return "" + (metadata.Text ?? ((IEmbeddedEntry)embeddedObject).Title) + ""; + return "" + (metadata.Text ?? ((IEmbeddedEntry)embeddedObject).Title) + ""; } - return "" + (metadata.Text ?? embeddedObject.Uid) + ""; + return "" + (metadata.Text ?? embeddedObject.Uid) + ""; case Enums.StyleType.Display: if (embeddedObject is IEmbeddedAsset) { - return "\"""; + return "\"""; } - return "\"""; + return "\"""; case Enums.StyleType.Download: if (embeddedObject is IEmbeddedAsset) { - return "" + (metadata.Text ?? ((IEmbeddedAsset)embeddedObject).Title) + ""; + return "" + (metadata.Text ?? ((IEmbeddedAsset)embeddedObject).Title) + ""; } - return "" + (metadata.Text ?? embeddedObject.Uid) + ""; + return "" + (metadata.Text ?? embeddedObject.Uid) + ""; } - return ""; + return ""; } public virtual string RenderMark(MarkType markType, string text, string className = "", string id = "") @@ -97,7 +98,6 @@ public virtual string RenderNode(string nodeType, Node node, NodeChildrenCallBac { string href = ""; string styleAttrs = ""; - if (node.attrs.ContainsKey("style")) { var styleVal = node.attrs["style"]; @@ -107,10 +107,9 @@ public virtual string RenderNode(string nodeType, Node node, NodeChildrenCallBac { styleAttrs = $" style=\"{styleVal}\""; } - else if (styleVal is JObject) + else if (styleVal is JsonElement styleElement && styleElement.ValueKind == JsonValueKind.Object) { - var styleObject = (JObject)styleVal; - var styleDictionary = styleObject.ToObject>(); + var styleDictionary = JsonSerializer.Deserialize>(styleElement.GetRawText()); styleAttrs = " style=\""; foreach (var pair in styleDictionary) { @@ -120,27 +119,26 @@ public virtual string RenderNode(string nodeType, Node node, NodeChildrenCallBac } } } + string GetAttrString(string key) + { + if (!node.attrs.ContainsKey(key) || node.attrs[key] == null) return ""; + var val = node.attrs[key]; + if (val is string s) return s; + if (val is JsonElement je && je.ValueKind == JsonValueKind.String) return je.GetString(); + return val.ToString(); + } switch (nodeType) { case "p": return $"{callBack(node.children)}

"; case "a": - if (node.attrs.ContainsKey("url")) - { - href = (string)node.attrs["url"]; - } + href = GetAttrString("url"); return $"{callBack(node.children)}"; case "img": - if (node.attrs.ContainsKey("url")) - { - href = (string)node.attrs["url"]; - } + href = GetAttrString("url"); return $"{callBack(node.children)}"; case "embed": - if (node.attrs.ContainsKey("url")) - { - href = (string)node.attrs["url"]; - } + href = GetAttrString("url"); return $"{callBack(node.children)}"; case "fragment": return $"{callBack(node.children)}";