using System.Globalization; using System.Text.RegularExpressions; using HtmlAgilityPack; namespace wp2ghost { // ============================================================================= // HtmlGhostConverter — WordPress HTML zu Ghost-kompatibles HTML konvertieren // ============================================================================= public static class HtmlGhostConverter { private static readonly Dictionary WpClassMap = new(StringComparer.OrdinalIgnoreCase) { // Block-Elemente → Ghost-Äquivalent oder null (Klasse entfernen) { "wp-block-paragraph", null }, { "wp-block-heading", null }, { "wp-block-list", null }, { "wp-block-quote", null }, { "wp-block-code", null }, { "wp-block-separator", null }, { "wp-block-group", null }, { "wp-block-columns", null }, { "wp-block-column", null }, { "wp-block-freeform", null }, { "wp-block-verse", null }, { "wp-block-preformatted", null }, // Ghost-Kompatible Klassen { "wp-block-image", "kg-card kg-image-card" }, { "wp-block-gallery", "kg-card kg-gallery-card" }, { "wp-block-pullquote", "kg-card kg-pullquote" }, { "wp-block-embed", "kg-card kg-embed-card" }, { "wp-block-embed-wrapper", "kg-card kg-embed-card" }, { "wp-block-video", "kg-card kg-embed-card" }, { "wp-block-audio", "kg-card kg-embed-card" }, { "wp-block-cover", "kg-card kg-header-card" }, { "wp-block-media-text", "kg-card kg-width-wide" }, { "wp-block-table", "kg-card kg-table-card" }, { "wp-block-button", "kg-card kg-button-card" }, { "wp-block-buttons", "kg-card kg-button-card" }, { "wp-block-file", "kg-card kg-file-card" }, // Sub-Elemente { "wp-block-button__link", "gh-btn" }, { "wp-block-button__link__inline", "gh-btn" }, { "wp-element-button", "gh-btn" }, { "has-text-align-center", "kg-width-wide" }, { "has-text-align-left", "" }, { "has-text-align-right", "" }, { "is-style-wide", "kg-width-wide" }, { "is-style-full", "kg-width-wide" }, { "is-style-large", "kg-width-wide" }, // Theme-spezifisch { "wp-block-post-title", null }, { "wp-block-post-content", null }, { "wp-block-site-title", null }, { "wp-block-site-tagline", null }, { "wp-block-query", null }, { "wp-block-comments", null }, { "wp-block-comment-template", null }, { "wp-block-template-part", null }, // CoBlocks { "wp-block-coblocks-gallery-stacked", "kg-card kg-gallery-card" }, { "wp-block-coblocks-gallery-collage", "kg-card kg-gallery-card" }, { "wp-block-coblocks-gallery-masonry", "kg-card kg-gallery-card" }, { "wp-block-coblocks-gallery-thumbnails", "kg-card kg-gallery-card" }, { "wp-block-coblocks-alert", "kg-card kg-alert-card" }, { "wp-block-coblocks-accordion", "kg-card kg-accordion-card" }, { "wp-block-coblocks-accordion-item", "kg-accordion-item" }, { "wp-block-coblocks-row", "kg-width-wide" }, { "wp-block-coblocks-column", "" }, { "wp-block-coblocks-click-to-tweet", "kg-card kg-click-to-tweet" }, { "wp-block-coblocks-social-profiles", "kg-card kg-social-link" }, // Jetpack { "wp-block-jetpack-tiled-gallery", "kg-card kg-gallery-card" }, { "wp-block-jetpack-slideshow", "kg-card kg-gallery-card" }, { "wp-block-jetpack-story", "kg-card kg-gallery-card" }, }; public static string Convert(string html) { if (string.IsNullOrWhiteSpace(html)) return string.Empty; var htmlDoc = new HtmlDocument(); htmlDoc.LoadHtml(html); int totalGalleries = 0; totalGalleries += ConvertClassicGalleries(htmlDoc); totalGalleries += ConvertGutenbergGalleries(htmlDoc); totalGalleries += ConvertJetpackGalleries(htmlDoc); totalGalleries += ConvertCoBlocksGalleries(htmlDoc); if (totalGalleries > 0) Console.WriteLine($" [Gallery] {totalGalleries} Galerie(n) konvertiert"); ConvertSingleImages(htmlDoc); ConvertFigures(htmlDoc); ConvertWpClasses(htmlDoc); return htmlDoc.DocumentNode.OuterHtml; } // ========================================================================= // Gallery-Typ 1: WordPress Classic Gallery // HTML: // ========================================================================= private static int ConvertClassicGalleries(HtmlDocument htmlDoc) { var galleryNodes = htmlDoc.DocumentNode.SelectNodes( "//div[contains(@class, 'gallery') and not(contains(@class, 'wp-block'))]" ); if (galleryNodes == null) return 0; int count = 0; foreach (var gallery in galleryNodes.ToList()) { var items = gallery.SelectNodes(".//figure[contains(@class, 'gallery-item')]"); if (items == null || items.Count == 0) continue; var ghostGallery = htmlDoc.CreateElement("figure"); SetClasses(ghostGallery, "kg-card kg-gallery-card"); AddWideClass(ghostGallery, gallery); var container = htmlDoc.CreateElement("div"); container.AddClass("kg-gallery-container"); var wrappers = new List(); foreach (var item in items) { var img = item.SelectSingleNode(".//img"); if (img == null) continue; string src = GetBestImageUrl(img); if (string.IsNullOrEmpty(src)) continue; wrappers.Add(BuildGalleryImage(htmlDoc, img, src)); } if (wrappers.Count == 0) continue; AppendGalleryRows(htmlDoc, container, wrappers, GetGalleryColumns(gallery)); ghostGallery.AppendChild(container); var captionNode = gallery.SelectSingleNode(".//*[contains(@class, 'gallery-caption')] | .//figcaption"); if (captionNode != null && !string.IsNullOrWhiteSpace(captionNode.InnerText)) { var newCaption = htmlDoc.CreateElement("figcaption"); newCaption.InnerHtml = captionNode.InnerHtml; ghostGallery.AppendChild(newCaption); ghostGallery.AddClass("kg-card-hascaption"); } gallery.ParentNode?.ReplaceChild(ghostGallery, gallery); count++; } return count; } // ========================================================================= // Gallery-Typ 2: WordPress Gutenberg Gallery // ========================================================================= private static int ConvertGutenbergGalleries(HtmlDocument htmlDoc) { var galleryNodes = htmlDoc.DocumentNode.SelectNodes( "//figure[contains(@class, 'wp-block-gallery')] | " + "//div[contains(@class, 'wp-block-gallery')] | " + "//ul[contains(@class, 'wp-block-gallery')]" ); if (galleryNodes == null) return 0; int count = 0; foreach (var gallery in galleryNodes.ToList()) { var imgs = gallery.SelectNodes(".//img"); if (imgs == null || imgs.Count == 0) continue; var ghostGallery = htmlDoc.CreateElement("figure"); SetClasses(ghostGallery, "kg-card kg-gallery-card"); AddWideClass(ghostGallery, gallery); var container = htmlDoc.CreateElement("div"); container.AddClass("kg-gallery-container"); var wrappers = new List(); foreach (var img in imgs) { string src = GetBestImageUrl(img); if (string.IsNullOrEmpty(src)) continue; wrappers.Add(BuildGalleryImage(htmlDoc, img, src)); } if (wrappers.Count == 0) continue; AppendGalleryRows(htmlDoc, container, wrappers, GetGalleryColumns(gallery)); ghostGallery.AppendChild(container); var captionNode = gallery.SelectSingleNode(".//figcaption"); if (captionNode != null && !string.IsNullOrWhiteSpace(captionNode.InnerText)) { var newCaption = htmlDoc.CreateElement("figcaption"); newCaption.InnerHtml = captionNode.InnerHtml; ghostGallery.AppendChild(newCaption); ghostGallery.AddClass("kg-card-hascaption"); } gallery.ParentNode?.ReplaceChild(ghostGallery, gallery); count++; } return count; } // ========================================================================= // Gallery-Typ 3: Jetpack Tiled Gallery // ========================================================================= private static int ConvertJetpackGalleries(HtmlDocument htmlDoc) { var galleryNodes = htmlDoc.DocumentNode.SelectNodes( "//div[contains(@class, 'wp-block-jetpack-tiled-gallery')] | " + "//figure[contains(@class, 'wp-block-jetpack-tiled-gallery')]" ); if (galleryNodes == null) return 0; int count = 0; foreach (var gallery in galleryNodes.ToList()) { var imgs = gallery.SelectNodes(".//img"); if (imgs == null || imgs.Count == 0) continue; var ghostGallery = htmlDoc.CreateElement("figure"); SetClasses(ghostGallery, "kg-card kg-gallery-card"); AddWideClass(ghostGallery, gallery); var container = htmlDoc.CreateElement("div"); container.AddClass("kg-gallery-container"); var wrappers = new List(); foreach (var img in imgs) { string src = GetBestImageUrl(img); if (string.IsNullOrEmpty(src)) continue; wrappers.Add(BuildGalleryImage(htmlDoc, img, src)); } if (wrappers.Count == 0) continue; AppendGalleryRows(htmlDoc, container, wrappers, GetGalleryColumns(gallery)); ghostGallery.AppendChild(container); gallery.ParentNode?.ReplaceChild(ghostGallery, gallery); count++; } return count; } // ========================================================================= // Gallery-Typ 4: CoBlocks Gallery // ========================================================================= private static int ConvertCoBlocksGalleries(HtmlDocument htmlDoc) { var galleryNodes = htmlDoc.DocumentNode.SelectNodes( "//figure[contains(@class, 'wp-block-coblocks-gallery-masonry')] | " + "//figure[contains(@class, 'wp-block-coblocks-gallery-stacked')] | " + "//figure[contains(@class, 'wp-block-coblocks-gallery-collage')] | " + "//figure[contains(@class, 'wp-block-coblocks-gallery-thumbnails')]" ); if (galleryNodes == null) return 0; int count = 0; foreach (var gallery in galleryNodes.ToList()) { var imgs = gallery.SelectNodes(".//img"); if (imgs == null || imgs.Count == 0) continue; var ghostGallery = htmlDoc.CreateElement("figure"); SetClasses(ghostGallery, "kg-card kg-gallery-card"); AddWideClass(ghostGallery, gallery); var container = htmlDoc.CreateElement("div"); container.AddClass("kg-gallery-container"); var wrappers = new List(); foreach (var img in imgs) { string src = GetBestImageUrl(img); if (string.IsNullOrEmpty(src)) continue; wrappers.Add(BuildGalleryImage(htmlDoc, img, src)); } if (wrappers.Count == 0) continue; AppendGalleryRows(htmlDoc, container, wrappers, GetGalleryColumns(gallery)); ghostGallery.AppendChild(container); gallery.ParentNode?.ReplaceChild(ghostGallery, gallery); count++; } return count; } // ========================================================================= // Galerie-Helfer: Spaltenanzahl, Wide-Klasse, Bild-Wrapper, Zeilen // ========================================================================= private static int GetGalleryColumns(HtmlNode gallery) { string classes = gallery.GetAttributeValue("class", ""); var match = Regex.Match(classes, @"columns-(\d+)"); if (match.Success && int.TryParse(match.Groups[1].Value, out int cols) && cols >= 1 && cols <= 9) return cols; return 3; } private static void AddWideClass(HtmlNode ghostGallery, HtmlNode originalGallery) { string origClass = originalGallery.GetAttributeValue("class", ""); if (origClass.Contains("is-style-wide") || origClass.Contains("alignwide")) ghostGallery.AddClass("kg-width-wide"); } private static HtmlNode BuildGalleryImage(HtmlDocument htmlDoc, HtmlNode originalImg, string src) { var imgWrapper = htmlDoc.CreateElement("div"); imgWrapper.AddClass("kg-gallery-image"); var newImg = htmlDoc.CreateElement("img"); newImg.SetAttributeValue("src", src); double w = 0, h = 0; string origSize = originalImg.GetAttributeValue("data-orig-size", ""); if (!string.IsNullOrEmpty(origSize)) { var parts = origSize.Split(','); if (parts.Length == 2 && double.TryParse(parts[0].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out double ow) && double.TryParse(parts[1].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out double oh)) { w = ow; h = oh; } } if (w <= 0 || h <= 0) { if (double.TryParse(originalImg.GetAttributeValue("width", ""), NumberStyles.Float, CultureInfo.InvariantCulture, out double aw) && double.TryParse(originalImg.GetAttributeValue("height", ""), NumberStyles.Float, CultureInfo.InvariantCulture, out double ah)) { w = aw; h = ah; } } if (w > 0 && h > 0) { newImg.SetAttributeValue("width", ((int)w).ToString(CultureInfo.InvariantCulture)); newImg.SetAttributeValue("height", ((int)h).ToString(CultureInfo.InvariantCulture)); string ratio = (w / h).ToString("G6", CultureInfo.InvariantCulture); imgWrapper.SetAttributeValue("style", $"flex: {ratio} 1 0%;"); } else { imgWrapper.SetAttributeValue("style", "flex: 1 1 0%;"); } newImg.SetAttributeValue("loading", "lazy"); newImg.SetAttributeValue("alt", originalImg.GetAttributeValue("alt", "")); imgWrapper.AppendChild(newImg); return imgWrapper; } private static void AppendGalleryRows(HtmlDocument htmlDoc, HtmlNode container, List wrappers, int columns) { for (int i = 0; i < wrappers.Count; i += columns) { var row = htmlDoc.CreateElement("div"); row.AddClass("kg-gallery-row"); int end = Math.Min(i + columns, wrappers.Count); for (int j = i; j < end; j++) row.AppendChild(wrappers[j]); container.AppendChild(row); } } // ========================================================================= // Helper: Beste Bild-URL aus img-Element holen // Priorität: data-orig-file → data-large-file → srcset (letzter/Widest) → src // ========================================================================= private static string GetBestImageUrl(HtmlNode img) { // 1. data-orig-file (Vollbild) string src = img.GetAttributeValue("data-orig-file", ""); if (!string.IsNullOrEmpty(src)) return src; // 2. data-large-file src = img.GetAttributeValue("data-large-file", ""); if (!string.IsNullOrEmpty(src)) return src; // 3. srcset (größte Variante nehmen) string srcset = img.GetAttributeValue("srcset", ""); if (!string.IsNullOrEmpty(srcset)) { src = GetLargestFromSrcset(srcset); if (!string.IsNullOrEmpty(src)) return src; } // 4. src (Fallback) return img.GetAttributeValue("src", ""); } private static string GetLargestFromSrcset(string srcset) { string bestUrl = ""; int bestWidth = 0; foreach (string part in srcset.Split(',', StringSplitOptions.RemoveEmptyEntries)) { string trimmed = part.Trim(); string[] tokens = trimmed.Split(' ', StringSplitOptions.RemoveEmptyEntries); if (tokens.Length < 2) continue; string url = tokens[0]; string descriptor = tokens[1]; if (int.TryParse(descriptor.Replace("w", ""), out int width) && width > bestWidth) { bestWidth = width; bestUrl = url; } } return bestUrl; } // ========================================================================= // Einzelbilder (nicht in Galerien) // ========================================================================= private static void ConvertSingleImages(HtmlDocument htmlDoc) { var singleImgNodes = htmlDoc.DocumentNode.SelectNodes("//img"); if (singleImgNodes == null) return; foreach (var img in singleImgNodes.ToList()) { if (img.Ancestors("figure").Any(a => HasClass(a, "kg-gallery-card"))) continue; string src = GetBestImageUrl(img); if (string.IsNullOrEmpty(src)) continue; var parentFigure = img.Ancestors("figure").FirstOrDefault(); if (parentFigure != null) { SetClasses(parentFigure, "kg-card kg-image-card"); img.AddClass("kg-image"); } else { var wrapper = htmlDoc.CreateElement("figure"); SetClasses(wrapper, "kg-card kg-image-card"); img.AddClass("kg-image"); var imgParent = img.ParentNode; if (imgParent != null) { imgParent.InsertBefore(wrapper, img); imgParent.RemoveChild(img); wrapper.AppendChild(img); } } } } // ========================================================================= // Figure ohne kg-card → kg-width-wide // ========================================================================= private static void ConvertFigures(HtmlDocument htmlDoc) { var figures = htmlDoc.DocumentNode.SelectNodes("//figure"); if (figures == null) return; foreach (var figure in figures.ToList()) { if (HasClass(figure, "kg-card")) continue; SetClasses(figure, "kg-width-wide kg-card-width-wide"); } } // ========================================================================= // WP-Klassen mappen // ========================================================================= private static void ConvertWpClasses(HtmlDocument htmlDoc) { var allNodes = htmlDoc.DocumentNode.SelectNodes("//*[contains(@class, 'wp-block')]"); if (allNodes == null) return; foreach (var node in allNodes.ToList()) { string classes = node.GetAttributeValue("class", ""); if (string.IsNullOrEmpty(classes)) continue; var classList = classes.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList(); var newClasses = new List(); bool changed = false; foreach (string cls in classList) { if (WpClassMap.TryGetValue(cls, out string? ghostClass)) { changed = true; if (ghostClass != null && ghostClass.Length > 0) newClasses.AddRange(ghostClass.Split(' ')); } else { newClasses.Add(cls); } } if (changed) { string finalClasses = string.Join(" ", newClasses.Where(c => c.Length > 0).Distinct()); if (finalClasses.Length > 0) node.SetAttributeValue("class", finalClasses); else node.Attributes.Remove("class"); } } // Separator var separators = htmlDoc.DocumentNode.SelectNodes("//div[contains(@class, 'wp-block-separator')]"); if (separators != null) { foreach (var sep in separators.ToList()) { var hr = htmlDoc.CreateElement("hr"); foreach (var attr in sep.Attributes) hr.SetAttributeValue(attr.Name, attr.Value); sep.ParentNode?.ReplaceChild(hr, sep); } } } private static bool HasClass(HtmlNode node, string className) { string classes = node.GetAttributeValue("class", ""); return classes.Split(' ', StringSplitOptions.RemoveEmptyEntries) .Any(c => c.Equals(className, StringComparison.OrdinalIgnoreCase)); } private static void SetClasses(HtmlNode node, string classes) { node.SetAttributeValue("class", classes); } } }