Loading...
05-09-2024

Voor het lezen en tonen van RSS feeds, dacht ik dat het praktisch zou zijn om de inhoud van de link van de feed te tonen in een zogenaamde "readermode", zoals deze in veel browsers is ingebouwd. Na een kleine zoektocht lijkt het erop dat de reader mode van Safari gebaseerd is op de implementatie van Mozilla. En zo blijkt dat deze open-source is en op GitHub te vinden is: readability.

Dit klinkt natuurlijk als een interessante optie om te testen, maar ik gebruik .Net, en geen Node.js. Na wat zoekwerk kwam ik al snel meerdere implementaties tegen op nuget, waaronder SmartReader:

This library supports the .NET Standard 2.0. The core algorithm is a port of the Mozilla Readability library. The original library is stable and used in production inside Firefox. This way we can piggyback on the hard and well-tested work of Mozilla.

SmartReader also added some improvements on the original library, getting more and better metadata:

  • site name
  • an author and publication date
  • the language
  • the excerpt of the article
  • the featured image
  • a list of images found (it can optionally also download them and store as data URI)
  • an estimate of the time needed to read the article

Some of these fields are now present in the original library.

It also allows to perform custom operations before and after extracting the article.

Het inzetten van SmartReader is nogal simpel. Voeg de package toe aan je project en laat SmartReader de pagina ophalen en beoordelen:

var article = SmartReader.Reader.ParseArticle("https://arstechnica.com/information-technology/2017/02/humans-must-become-cyborgs-to-survive-says-elon-musk/");

if(article.IsReadable)
{
    Console.WriteLine($"Article title {article.Title}");
}

Er zijn nog diverse opties in te stellen, maar die sla ik voorlopig over. Voor het gebruik in mijn RSS reader, wil ik liever geen HTML hebben, maar Markdown tekst. Ik gebruik al langer de ReverseMarkdown packages, wat tot prima resultaten leid. Hier loop ik helaas wel tegen een probleem aan. Het blijkt dat de HTML soms veel tabs (of spaties) bevat, tussen een tekst node en een andere HTML node. Bijvoorbeeld een tekst gevolgd door een link. Maar ook een linefeed samen met tabs na een paragraph openings tag <p>. Hierbij worden de tabs niet verwijderd wat resulteert dat de markdown naar HTML deze tekst als <pre> formatteert.

Hierom is het verstandig om de HTML toch eerst nog wat op te schonen. AngleSharp is hier een prima oplossing voor. Ik heb ervoor gekozen om alle HTML element langs te lopen en de tekst in de textnode's op te ruimen:

public static void HtmlTextNodeTrim(INode node)
{
    if (node is IElement elementNode)
    {
        foreach (var childNode in elementNode.ChildNodes)
        {
            HtmlTextNodeTrim(childNode);
        }
    }
    else if (node is IText textNode)
    {
        var updatedText = 
            textNode.NodeValue = (textNode.Text ?? string.Empty).Trim() + " ";
    }
}

Tijdens het schrijven van dit artikel bedenk ik me wel dat dit problemen kan opleveren met een <pre> of <code> tag. Dit is voor latere zorg. Door dit te combineren met het herformatteren van de HTML zonder indenting, komt er HTML uit die prima door ReverseMarkdown kan worden omgezet:

using System.IO;
using AngleSharp;
using AngleSharp.Dom;
using AngleSharp.Html.Dom;
using AngleSharp.Html.Parser;

protected virtual string GetReadable(string url)
{
    try
    {
        var uri = new Uri(url);
        
        var article = Reader.ParseArticle(uri.ToString());
        if (!article.IsReadable)
            return null;
        
        // Get and sanitize HTML
        var html = HtmlSanitizeHelper.SanitizeHtml(article.Content);

        // Parse to markdown
        return ReadabilityHelper.HtmlToMarkdown(html);
    }
    catch (Exception ex)
    {
        // ... do some logging  
    }

    return null;
}

public class HtmlSanitizeHelper
{
    public static string SanitizeHtml(string html)
    {
        var parser = new HtmlParser();
        var document = parser.ParseDocument(html);

        HtmlTextNodeTrim(document.Body);
        
        return DocumentToHtml(document);
    }
    
    public static void HtmlTextNodeTrim(INode node)
    {
        if (node is IElement elementNode)
        {
            foreach (var childNode in elementNode.ChildNodes)
            {
                HtmlTextNodeTrim(childNode);
            }
        }
        else if (node is IText textNode)
        {
            var updatedText = 
                textNode.NodeValue = (textNode.Text ?? string.Empty).Trim() + " ";
        }
    }
    
    public static string DocumentToHtml(IHtmlDocument document)
    {
        using (var writer = new StringWriter())
        {
            document.ToHtml(writer);
            return writer.ToString();
        }
    }

    public static string HtmlToMarkdown(string html)
    {
        return new Converter(new Config()
        {
            UnknownTags = Config.UnknownTagsOption.Bypass,
            GithubFlavored = true,
            RemoveComments = true,
            SmartHrefHandling = true,
            TableWithoutHeaderRowHandling = Config.TableWithoutHeaderRowHandlingOption.Default
        }).Convert(html);
    }
}
  • HTML
  • Markdown
  • RSS