Как сделать вложенный список из элементов одного уровня на основе начального текста

Я пытаюсь создать вложенный список на основе элементов списка (того же уровня), но с другим текстом метки, который будет начальным текстом каждого элемента списка. Я сделал некоторые замены регулярных выражений, чтобы соответствовать вложенности. Я предполагаю, что мой код не соответствует требованиям группировки|вложенности.

IN.xml:

<?xml version = "1.0" encoding = "UTF-8"?>
<article>
   <p>The Simple list sample</p>
   <list-item>1. First</list-item>
   <list-item>2. Second</list-item>
   <list-item>3. Third</list-item>
   <p>The Nested list sample</p>
   <list-item>1. FirstLevel First Text</list-item>
   <list-item>1.1 SecondLevel First Text</list-item>
   <list-item>1.1.1 ThirdLevel First Text</list-item>
   <list-item>1.1.2 ThirdLevel Second Text</list-item>
   <list-item>1.2 SecondLevel Second Text</list-item>
   <list-item>2. FirstLevel Second Text</list-item>
   <list-item>2.1 SecondLevel First Text</list-item>
   <list-item>2.2 SecondLevel Second Text</list-item>
   <list-item>3. FirstLevel Third Text</list-item>
   <list-item>4. FirstLevel Fourth Text</list-item>
</article>

С# (пробный код):

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;
using System.Xml.XPath;
using System.Linq;
using System.Linq.Expressions;

namespace ListNesting1
{
    class Program
    {
        static void Main(string[] args)
        {
            XmlDocument XMLDoc1 = new XmlDocument();
            XmlNodeList NDL1;
            XmlElement XEle1;
           
            String S1, S2, StrFinal, StrEle1;
            StreamReader SR1;
            StreamWriter SW1;

            try
            {
                SR1 = new StreamReader(args[0]);
                S1 = SR1.ReadToEnd();
                SR1.Close();
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
                return;
            }

            XMLDoc1.LoadXml(S1);

            NDL1 = XMLDoc1.SelectNodes("//list-item");

            for(int i=0; i<NDL1.Count; i++)
            {
                if (Regex.IsMatch(NDL1[i].InnerText, @"^[0-9]\. "))
                    {
                    StrEle1 = "List1";
                }
                
                else if (Regex.IsMatch(NDL1[i].InnerText, @"^[0-9]\.[0-9] "))
                {
                    StrEle1 = "List2";
                }
                else if (Regex.IsMatch(NDL1[i].InnerText, @"^[0-9]\.[0-9]\.[0-9] "))
                {
                    StrEle1 = "List3";
                }
                else
                {
                    StrEle1 = "List4";
                }
                XEle1 = XMLDoc1.CreateElement(StrEle1);
                S2 = NDL1[i].OuterXml;
                XEle1.InnerXml = S2;
                
                NDL1[i].ParentNode.InsertAfter(XEle1, NDL1[i]);
                NDL1[i].ParentNode.RemoveChild(NDL1[i]);
            }

            StrFinal = XMLDoc1.OuterXml;
            StrFinal = StrFinal.Replace("</List1><List1>", "");
            StrFinal = StrFinal.Replace("</List2><List2>", "");
            StrFinal = StrFinal.Replace("</List3><List3>", "");
            StrFinal = StrFinal.Replace("</List4><List4>", "");

            StrFinal = StrFinal.Replace("</list-item></List1><List2>", "<List2>");
            StrFinal = StrFinal.Replace("</list-item></List2><List3>", "<List3>");
            StrFinal = StrFinal.Replace("</list-item></List3><List4>", "<List4>");

            StrFinal = StrFinal.Replace("</List2><List1>", "</List2></list-item>");
            StrFinal = StrFinal.Replace("</List3><List2>", "</List3></list-item>");
            StrFinal = StrFinal.Replace("</List4><List3>", "</List4></list-item>");

            StrFinal = StrFinal.Replace("><", ">\n<");

            SW1 = new StreamWriter(args[1]);
            SW1.Write(StrFinal);
            SW1.Close();
        }
    }
}

Требуемый XML:

<?xml version = "1.0" encoding = "UTF-8"?>
<article>
   <p>The Simple list sample</p>
   <List1>
      <list-item>1. First</list-item>
      <list-item>2. Second</list-item>
      <list-item>3. Third</list-item>
   </List1>
   <p>The Nested list sample</p>
   <List1>
      <list-item>1. FirstLevel First Text
         <List2>
            <list-item>1.1 SecondLevel First Text
               <List3>
                  <list-item>1.1.1 ThirdLevel First Text</list-item>
                  <list-item>1.1.2 ThirdLevel Second Text</list-item>
               </List3>
            </list-item>
            <list-item>1.2 SecondLevel Second Text</list-item>
         </List2>
      </list-item>
      <list-item>2. FirstLevel Second Text
         <List2>
            <list-item>2.1 SecondLevel First Text</list-item>
            <list-item>2.2 SecondLevel Second Text</list-item>
         </List2>
      </list-item>
      <list-item>3. FirstLevel Third Text</list-item>
      <list-item>4. FirstLevel Fourth Text</list-item>
   </List1>
</article>

🤔 А знаете ли вы, что...
C# активно развивается и обновляется, с появлением новых версий и функциональности.


1
241
3

Ответы:

Это задача для XSLT, например. XSLT 3 с

<xsl:stylesheet xmlns:xsl = "http://www.w3.org/1999/XSL/Transform"
    xmlns:xs = "http://www.w3.org/2001/XMLSchema"
    xmlns:mf = "http://example.com/mf"
    exclude-result-prefixes = "#all"
    version = "3.0">
  
  <xsl:function name = "mf:group" as = "node()*">
    <xsl:param name = "items" as = "map(*)*"/>
    <xsl:param name = "level" as = "xs:integer"/>
    <xsl:choose>
      <xsl:when test = "exists($items[count(?levels) ge $level])">
        <xsl:element name = "List{$level}">
        <xsl:for-each-group select = "$items" group-starting-with = ".[count(?levels) eq $level]">
          <xsl:copy select = "?item">
            <xsl:apply-templates select = "node()"/>
            <xsl:sequence select = "mf:group(tail(current-group()), $level + 1)"/>
          </xsl:copy>
        </xsl:for-each-group>
        </xsl:element>        
      </xsl:when>
      <xsl:otherwise>
        <xsl:apply-templates select = "$items?item"/>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:function>

  <xsl:mode on-no-match = "shallow-copy"/>

  <xsl:output method = "xml" indent = "yes"/>

  <xsl:template match = "article">
    <xsl:copy>
      <xsl:for-each-group select = "*" group-adjacent = "boolean(self::list-item)">
        <xsl:choose>
          <xsl:when test = "current-grouping-key()">
              <xsl:sequence select = "mf:group(current-group()!map { 'item' : ., 'levels' : (. => substring-before(' ') => tokenize('\.'))[normalize-space()]}, 1)"/>
          </xsl:when>
          <xsl:otherwise>
            <xsl:apply-templates select = "current-group()"/>
          </xsl:otherwise>
        </xsl:choose>
      </xsl:for-each-group>
    </xsl:copy>
  </xsl:template>
  
</xsl:stylesheet>

Для .NET framework Saxon HE (последняя версия .NET framework — Saxon HE 10.8) доступен в виде пакета с открытым исходным кодом https://www.nuget.org/packages/Saxon-HE на NuGet, а также загрузите исполняемый файл https://github.com/Saxonica/Saxon-HE/tree/main/10/Dotnet для запуска XSLT 3.

На .NET Core 6/7 Saxonica в настоящее время доступна только коммерческая версия SaxonCS для предприятий (https://www.nuget.org/packages/SaxonCS), но мне удалось перекрестно скомпилировать как Saxon HE 10.8, так и Saxon. HE 11 использует IKVM для .NET Core, так что даже там у вас есть возможность запустить XSLT 3.0 без необходимости покупать коммерческую лицензию:


Решено

код С#

using System;
using System.Text.RegularExpressions;

public class Example
{
    public static void Main()
    {
        string pattern1 = @"(<list-item>1\. [\s\S]*?</list-item>(?!\s+<list-item>\d))";
        string substitution1 = @"<list1>$1</list1>";

        string pattern2 = @"(<list-item>\d\.1 [\s\S]*?</list-item>(?!\s+<list-item>\d.\d))";
        string substitution2 = @"<list2>$1</list2>";


        string pattern3 = @"(<list-item>\d.\d\.1 [\s\S]*?</list-item>(?!\s+<list-item>\d.\d.\d))";
        string substitution3 = @"<list3>$1</list3>";


        string input = @"<?xml version = ""1.0"" encoding = ""UTF-8""?>
<article>
   <p>The Simple list sample</p>
   <list-item>1. First</list-item>
   <list-item>2. Second</list-item>
   <list-item>3. Third</list-item>
   <p>The Nested list sample</p>
   <list-item>1. FirstLevel First Text</list-item>
   <list-item>1.1 SecondLevel First Text</list-item>
   <list-item>1.1.1 ThirdLevel First Text</list-item>
   <list-item>1.1.2 ThirdLevel Second Text</list-item>
   <list-item>1.2 SecondLevel Second Text</list-item>
   <list-item>2. FirstLevel Second Text</list-item>
   <list-item>2.1 SecondLevel First Text</list-item>
   <list-item>2.2 SecondLevel Second Text</list-item>
   <list-item>3. FirstLevel Third Text</list-item>
   <list-item>4. FirstLevel Fourth Text</list-item>
</article>";

        Regex regex = new Regex(pattern1);
        input = regex.Replace(input, substitution1);


        Regex regex2 = new Regex(pattern2);
        input = regex2.Replace(input, substitution2);


        Regex regex3 = new Regex(pattern3);
        input = regex3.Replace(input, substitution3);
    }
}

выход

<?xml version = "1.0" encoding = "UTF-8"?>
<article>
    <p>The Simple list sample</p>
    <list1>
        <list-item>1. First</list-item>
        <list-item>2. Second</list-item>
        <list-item>3. Third</list-item>
    </list1>
    <p>The Nested list sample</p>
    <list1>
        <list-item>1. FirstLevel First Text</list-item>
        <list2>
            <list-item>1.1 SecondLevel First Text</list-item>
            <list3>
                <list-item>1.1.1 ThirdLevel First Text</list-item>
                <list-item>1.1.2 ThirdLevel Second Text</list-item>
            </list3>
            <list-item>1.2 SecondLevel Second Text</list-item>
        </list2>
        <list-item>2. FirstLevel Second Text</list-item>
        <list2>
            <list-item>2.1 SecondLevel First Text</list-item>
            <list-item>2.2 SecondLevel Second Text</list-item>
        </list2>
        <list-item>3. FirstLevel Third Text</list-item>
        <list-item>4. FirstLevel Fourth Text</list-item>
    </list1>
</article>

Вот кое-что, с чем вы, возможно, захотите поиграть. Он не соответствует требуемому результату (элементы <Listx> не находятся внутри предыдущего <list-item>, но он довольно близок.

using System.Xml.Linq;

var xml = XmlString();

var s = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(xml));
var x = XElement.Load(s);

XElement rootArticle = new ("article");
XElement parentElement = rootArticle;

List<int> currentLevel = new ();
foreach (var currentElement in x.Descendants())
{
  // When not a list-item, unwind
  if (currentElement.Name != "list-item")
  {
    while (currentLevel.Count > 0)
    {
      parentElement = parentElement.Parent;
      currentLevel.RemoveAt(currentLevel.Count - 1);
    }
    parentElement!.Add(currentElement);
    continue;
  }

  var headertext = (currentElement.FirstNode as XText)?.Value ?? string.Empty;
  List<int> previousLevel = currentLevel;
  currentLevel = headertext[..(headertext + " ").IndexOf(' ')].TrimEnd('.').Split('.').Select(x => { var _ = int.TryParse(x, out var n); return n; }).ToList();
  // If current level is in same sequence
  if (currentLevel.Count > 0 && currentLevel.Count >= previousLevel.Count && Enumerable.Range(0, previousLevel.Count).All(i => currentLevel[i] >= previousLevel[i]))
  {
    // Add required lists to match header depth
    for (int i = 0; i < currentLevel.Count - previousLevel.Count; i++)
    {
      XElement listElement = new ($"List{i + previousLevel.Count + 1}");
      parentElement.Add(listElement);
      parentElement = listElement;
    }
    parentElement.Add(currentElement);
    continue;
  }

  // Go back to parent with matching depth
  var depth = previousLevel.Take(currentLevel.Count).Where((n, i) => n <= currentLevel[i]).Count();
  for (int i=depth; i < previousLevel.Count; i++)
  {
    parentElement = parentElement!.Parent;
  }

  // Add required lists to match header depth
  for (int i = depth; i < currentLevel.Count; i++)
  {
    XElement listElement = new ($"List{i + 1}");
    parentElement!.Add(listElement);
    parentElement = listElement;
  }
  parentElement!.Add(currentElement);
}

Console.WriteLine(rootArticle.ToString());

// <article>
//   <p>The Simple list sample</p>
//   <List1>
//     <list-item>1. First</list-item>
//     <list-item>2. Second</list-item>
//     <list-item>3. Third</list-item>
//   </List1>
//   <p>The Nested list sample</p>
//   <List1>
//     <list-item>1. FirstLevel First Text</list-item>
//     <List2>
//       <list-item>1.1 SecondLevel First Text</list-item>
//       <List3>
//         <list-item>1.1.1 ThirdLevel First Text</list-item>
//         <list-item>1.1.2 ThirdLevel Second Text</list-item>
//       </List3>
//       <list-item>1.2 SecondLevel Second Text</list-item>
//     </List2>
//     <list-item>2. FirstLevel Second Text</list-item>
//     <List2>
//       <list-item>2.1 SecondLevel First Text</list-item>
//       <list-item>2.2 SecondLevel Second Text</list-item>
//     </List2>
//     <list-item>3. FirstLevel Third Text</list-item>
//     <list-item>4. FirstLevel Fourth Text</list-item>
//   </List1>
// </article>

static string XmlString() => @"<?xml version = ""1.0"" encoding = ""UTF-8""?>
<article>
   <p>The Simple list sample</p>
   <list-item>1. First</list-item>
   <list-item>2. Second</list-item>
   <list-item>3. Third</list-item>
   <p>The Nested list sample</p>
   <list-item>1. FirstLevel First Text</list-item>
   <list-item>1.1 SecondLevel First Text</list-item>
   <list-item>1.1.1 ThirdLevel First Text</list-item>
   <list-item>1.1.2 ThirdLevel Second Text</list-item>
   <list-item>1.2 SecondLevel Second Text</list-item>
   <list-item>2. FirstLevel Second Text</list-item>
   <list-item>2.1 SecondLevel First Text</list-item>
   <list-item>2.2 SecondLevel Second Text</list-item>
   <list-item>3. FirstLevel Third Text</list-item>
   <list-item>4. FirstLevel Fourth Text</list-item>
</article>";


Интересные вопросы для изучения