Importer - Kategorien werden als Ghost-Tags angelegt; bei mehreren wird die primäre abgefragt, die übrigen folgen als sekundäre Tags - Schlagwörter werden ohne Nachfrage als <meta name="keywords"> in den Code-Injection-Header geschrieben (Ghost kennt kein meta_keywords-Feld) - Tag-Suche lädt alle Seiten, HTML-kodierte Namen werden dekodiert Bilder - Varianten eines WordPress-Bildes (Original, -scaled, -WxH, ?w=) werden zusammengefasst, einmal geladen und im HTML alle umgeschrieben - src bei Einzelbildern setzen, srcset/sizes entfernen - Cache-Schlüssel ist der Pfad statt nur der Dateiname; Dateinamen eindeutig - ZIP-Export enthält Cache-Bilder und feature_image - Warnung bei Bild-URLs, die weiter auf WordPress zeigen - Maximale Bildgröße 2048 px Plugin / GhostImageManager - Plugin liefert primäre Kategorie - Kategorie- und Keyword-Felder samt Filter im Image Manager Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
638 lines
27 KiB
C#
638 lines
27 KiB
C#
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<string, string?> 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);
|
|
AddTargetBlankToExternalLinks(htmlDoc);
|
|
|
|
return htmlDoc.DocumentNode.OuterHtml;
|
|
}
|
|
|
|
// =========================================================================
|
|
// Gallery-Typ 1: WordPress Classic Gallery
|
|
// HTML: <div class='gallery gallery-columns-3'>
|
|
// <figure class='gallery-item'>
|
|
// <div class='gallery-icon portrait'>
|
|
// <a href='...'><img src='thumb' data-orig-file='full.jpg' /></a>
|
|
// </div>
|
|
// </figure>
|
|
// </div>
|
|
// =========================================================================
|
|
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<HtmlNode>();
|
|
var imageCaptions = new List<string>();
|
|
|
|
foreach (var item in items)
|
|
{
|
|
var img = item.SelectSingleNode(".//img");
|
|
if (img == null) continue;
|
|
|
|
// Bild-Titel aus data-image-caption Attribut extrahieren
|
|
string dataCaption = img.GetAttributeValue("data-image-caption", "").Trim();
|
|
if (!string.IsNullOrEmpty(dataCaption))
|
|
imageCaptions.Add(dataCaption);
|
|
|
|
// Auch figcaption innerhalb des gallery-items prüfen
|
|
var figCap = item.SelectSingleNode(".//figcaption");
|
|
if (figCap != null && !string.IsNullOrWhiteSpace(figCap.InnerText))
|
|
{
|
|
string capText = figCap.InnerText.Trim();
|
|
if (!string.IsNullOrEmpty(capText) && !imageCaptions.Contains(capText))
|
|
imageCaptions.Add(capText);
|
|
}
|
|
|
|
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-Bild-Titel übertragen, wenn vorhanden
|
|
if (imageCaptions.Count > 0)
|
|
{
|
|
// Ersten nicht-leeren Titel nehmen als Gallery-Kapitel
|
|
string firstCaption = imageCaptions[0];
|
|
|
|
var newCaption = htmlDoc.CreateElement("figcaption");
|
|
newCaption.InnerHtml = firstCaption;
|
|
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);
|
|
|
|
// Neue Format-Prüfung: data-carousel-extra Attribut speichern
|
|
string carouselExtra = gallery.GetAttributeValue("data-carousel-extra", "");
|
|
|
|
var container = htmlDoc.CreateElement("div");
|
|
container.AddClass("kg-gallery-container");
|
|
|
|
var wrappers = new List<HtmlNode>();
|
|
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);
|
|
|
|
// Alle individuellen Bildunterschriften aus der Gutenberg-Gallery sammeln
|
|
var captionNodes = gallery.SelectNodes(".//figcaption");
|
|
if (captionNodes != null && captionNodes.Count > 0)
|
|
{
|
|
var distinctCaptions = captionNodes
|
|
.Select(n => n.InnerText.Trim())
|
|
.Where(t => !string.IsNullOrWhiteSpace(t))
|
|
.Distinct()
|
|
.ToList();
|
|
|
|
if (distinctCaptions.Count > 0)
|
|
{
|
|
var newCaption = htmlDoc.CreateElement("figcaption");
|
|
newCaption.InnerHtml = string.Join(" · ", distinctCaptions);
|
|
ghostGallery.AppendChild(newCaption);
|
|
ghostGallery.AddClass("kg-card-hascaption");
|
|
}
|
|
}
|
|
|
|
// data-carousel-extra als Ghost-Attribut übertragen (falls vorhanden)
|
|
if (!string.IsNullOrEmpty(carouselExtra))
|
|
{
|
|
var extraAttr = htmlDoc.CreateAttribute("data-carousel-extra");
|
|
extraAttr.Value = carouselExtra;
|
|
ghostGallery.Attributes.Append(extraAttr);
|
|
}
|
|
|
|
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<HtmlNode>();
|
|
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<HtmlNode>();
|
|
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<HtmlNode> 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;
|
|
|
|
// Beste Variante als src setzen; srcset/sizes verweisen auf WordPress-Größenvarianten
|
|
img.SetAttributeValue("src", src);
|
|
img.Attributes.Remove("srcset");
|
|
img.Attributes.Remove("sizes");
|
|
|
|
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<string>();
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// Externe Links mit target="_blank" versehen
|
|
// =========================================================================
|
|
private static void AddTargetBlankToExternalLinks(HtmlDocument htmlDoc)
|
|
{
|
|
var anchors = htmlDoc.DocumentNode.SelectNodes("//a[@href]");
|
|
if (anchors == null) return;
|
|
|
|
foreach (var anchor in anchors.ToList())
|
|
{
|
|
string href = anchor.GetAttributeValue("href", "").Trim();
|
|
if (string.IsNullOrEmpty(href)) continue;
|
|
|
|
// Nur absolute externe URLs (http/https)
|
|
if (href.StartsWith("http://") || href.StartsWith("https://"))
|
|
{
|
|
anchor.SetAttributeValue("target", "_blank");
|
|
anchor.SetAttributeValue("rel", "noopener noreferrer");
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|