HtmlAgilityPack subcadena de todos por longitud

c# html-agility-pack

Pregunta

Tengo html con elementos anidados (en su mayoría solo elementos div yp ) Necesito devolver el mismo html, pero con una determinada cantidad de letras. Obviamente, el recuento de letras no debe enumerar a través de etiquetas html, sino solo contar las letras de InnerText de cada elemento html. El resultado de HTML debe preservar la estructura adecuada: cualquier etiqueta de cierre para mantener el formato HTML válido.

Ejemplo de entrada:

<div>
    <p>some text</p>
    <p>some more text some more text some more text some more text some more text</p>
    <div>
        <p>some more text some more text some more text some more text some more text</p>
        <p>some more text some more text some more text some more text some more text</p>
    </div>
</div>

Dada la int length = 16 la salida debería verse así:

<div>
    <p>some text</p> // 9 characters in the InnerText here
    <p>some mo</p> // 7 characters in the InnerText here; 9 + 7 = 16;
</div>

Observe que el número de letras (incluidos los espacios) es 16. El siguiente <div> se elimina ya que el recuento de letras ha alcanzado la length variable. Tenga en cuenta que la salida html sigue siendo válida.

He intentado lo siguiente, pero eso realmente no funciona. El resultado no es el esperado: algunos elementos html se repiten.

public static string SubstringHtml(this string html, int length)
{
    HtmlDocument doc = new HtmlDocument();
    doc.LoadHtml(html);
    int totalLength = 0;
    StringBuilder output = new StringBuilder();
    foreach (var node in doc.DocumentNode.Descendants())
    {
        totalLength += node.InnerText.Length;
        if(totalLength >= length)
        {
            int difference = totalLength - length;
            string lastPiece = node.InnerText.ToString().Substring(0, difference);
            output.Append(lastPiece);
            break;
        }
        else
        {
            output.Append(node.InnerHtml);
        }
    }
    return output.ToString();
}

ACTUALIZAR

@SergeBelov proporcionó una solución que funciona para la primera entrada de muestra, sin embargo, las pruebas posteriores presentaron un problema con una entrada como la que se muestra a continuación.

Ejemplo de entrada # 2:

some more text some more text 
<div>
    <p>some text</p>
    <p>some more text some more text some more text some more text some more text</
</div>

Dada esa variable int maxLength = 7; una salida debe ser igual a algunos mo . No funciona así debido a este código donde ParentNode = null :

lastNode
    .Node
    .ParentNode
    .ReplaceChild(HtmlNode.CreateNode(lastNodeText.InnerText.Substring(0, lastNode.NodeLength - lastNode.TotalLength + maxLength)), lastNode.Node);

La creación de un nuevo HtmlNode no parece ayudar porque su propiedad InnterText es de solo lectura.

Respuesta aceptada

El siguiente programa de consola pequeño ilustra un posible enfoque, que es:

  1. Seleccione los nodos de texto relevantes y calcule el total acumulado de longitud para ellos;
  2. Tome tantos nodos como sea necesario para llegar al total acumulado más allá de la longitud máxima;
  3. Elimine todos los nodos de elementos del documento, excepto los que son ancestros de los nodos que seleccionamos durante los pasos ## 1, 2;
  4. Corte el texto en el último nodo de la lista para que se ajuste a la longitud máxima.

ACTUALIZACIÓN: Esto todavía debería funcionar con un nodo de texto siendo el primero; probablemente, se requiere un Trim() para eliminar el espacio en blanco de la misma como se muestra a continuación.

    static void Main(string[] args)
    {
        int maxLength = 9;
        string input = @"
            some more text some more text 
            <div>
                <p>some text</p>
                <p>some more text some more text some more text some more text some more text</
            </div>";

        var doc = new HtmlDocument();
        doc.LoadHtml(input);

        // Get text nodes with the appropriate running total
        var acc = 0;
        var nodes = doc.DocumentNode
            .Descendants()
            .Where(n => n.NodeType == HtmlNodeType.Text && n.InnerText.Trim().Length > 0)
            .Select(n => 
            {
                var length = n.InnerText.Trim().Length;
                acc += length;
                return new { Node = n, TotalLength = acc, NodeLength = length }; 
            })
            .TakeWhile(n => (n.TotalLength - n.NodeLength) < maxLength)
            .ToList();

        // Select element nodes we intend to keep
        var nodesToKeep = nodes
            .SelectMany(n => n.Node.AncestorsAndSelf()
                .Where(m => m.NodeType == HtmlNodeType.Element));

        // Select and remove element nodes we don't need
        var nodesToDrop = doc.DocumentNode
            .Descendants()
            .Where(m => m.NodeType == HtmlNodeType.Element)
            .Except(nodesToKeep)
            .ToList();

        foreach (var r in nodesToDrop)
            r.Remove();

        // Shorten the last node as required
        var lastNode = nodes.Last();
        var lastNodeText = lastNode.Node;
        var text = lastNodeText.InnerText.Trim().Substring(0,
                lastNode.NodeLength - lastNode.TotalLength + maxLength);
        lastNodeText
            .ParentNode
            .ReplaceChild(HtmlNode.CreateNode(text), lastNodeText);

        doc.Save(Console.Out);
    }


Related

Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow
¿Es esto KB legal? Sí, aprende por qué
Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow
¿Es esto KB legal? Sí, aprende por qué