sw/inc/editsh.hxx              |    2 -
 sw/qa/filter/md/md.cxx         |   51 +++++++++++++++++++++++++++++++++++++++++
 sw/source/filter/md/wrtmd.cxx  |   32 ++++++++++++++++++++++++-
 sw/source/filter/md/wrtmd.hxx  |    6 ++++
 sw/source/uibase/inc/wrtsh.hxx |    2 -
 5 files changed, 90 insertions(+), 3 deletions(-)

New commits:
commit 3b6282b23c4ed8b0ae6229cc65744e0e77f4ebc1
Author:     Miklos Vajna <vmik...@collabora.com>
AuthorDate: Fri Aug 29 09:03:06 2025 +0200
Commit:     Caolán McNamara <caolan.mcnam...@collabora.com>
CommitDate: Fri Aug 29 11:08:06 2025 +0200

    tdf#168152 sw markdown export: handle lists
    
    Support the following cases:
    
    1) Toplevel bullet list
    
    2) Nested bullet list (needs indentation)
    
    3) Toplevel numbered list
    
    4) Nested numbered list
    
    This is similar to how the ascii export adds an indent, bullets and
    numbering for text nodes with a numbering rule, but
    <https://spec.commonmark.org/0.31.2/#list-items> examples 294 -> 297
    shows that the markdown indent has to be dynamic, based on the size of
    the parent prefix, so write dynamic amount of indent, instead of the
    ascii export's fixed 4 spaces.
    
    Change-Id: Ia9eb0c718fc5f5334f19b592245b50bd32e8bd3e
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/190353
    Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoff...@gmail.com>
    Reviewed-by: Caolán McNamara <caolan.mcnam...@collabora.com>

diff --git a/sw/inc/editsh.hxx b/sw/inc/editsh.hxx
index cd79190ba6c7..0e42dca687ee 100644
--- a/sw/inc/editsh.hxx
+++ b/sw/inc/editsh.hxx
@@ -530,7 +530,7 @@ public:
     void NoNum();
 
     /// Delete, split enumeration list.
-    void DelNumRules();
+    SW_DLLPUBLIC void DelNumRules();
 
     SW_DLLPUBLIC void NumUpDown( bool bDown = true );
 
diff --git a/sw/qa/filter/md/md.cxx b/sw/qa/filter/md/md.cxx
index ce747675f6b7..50e0dbd048ec 100644
--- a/sw/qa/filter/md/md.cxx
+++ b/sw/qa/filter/md/md.cxx
@@ -218,6 +218,57 @@ CPPUNIT_TEST_FIXTURE(Test, testExportingCodeSpan)
     CPPUNIT_ASSERT_EQUAL(aExpected, aActual);
 }
 
+CPPUNIT_TEST_FIXTURE(Test, testExportingList)
+{
+    // Given a document that has both toplevel/nested bullets/numberings:
+    createSwDoc();
+    SwDocShell* pDocShell = getSwDocShell();
+    SwWrtShell* pWrtShell = pDocShell->GetWrtShell();
+    pWrtShell->Insert(u"A"_ustr);
+    pWrtShell->SplitNode();
+    pWrtShell->Insert(u"B"_ustr);
+    pWrtShell->BulletOn();
+    pWrtShell->SplitNode();
+    pWrtShell->Insert(u"C"_ustr);
+    pWrtShell->NumUpDown(/*bDown=*/true);
+    pWrtShell->SplitNode();
+    pWrtShell->Insert(u"D"_ustr);
+    pWrtShell->DelNumRules();
+    pWrtShell->SplitNode();
+    pWrtShell->Insert(u"E"_ustr);
+    pWrtShell->NumOn();
+    pWrtShell->SplitNode();
+    pWrtShell->Insert(u"F"_ustr);
+    pWrtShell->SplitNode();
+    pWrtShell->Insert(u"G"_ustr);
+    pWrtShell->NumUpDown(/*bDown=*/true);
+
+    // When saving that to markdown:
+    save(mpFilter);
+
+    // Then make sure list type and level is exported:
+    SvFileStream aStream(maTempFile.GetURL(), StreamMode::READ);
+    std::vector<char> aBuffer(aStream.remainingSize());
+    aStream.ReadBytes(aBuffer.data(), aBuffer.size());
+    std::string_view aActual(aBuffer.data(), aBuffer.size());
+    std::string_view aExpected(
+        // clang-format off
+        "A" SAL_NEWLINE_STRING SAL_NEWLINE_STRING
+        "- B" SAL_NEWLINE_STRING SAL_NEWLINE_STRING
+        // indent is 2 spaces
+        "  - C" SAL_NEWLINE_STRING SAL_NEWLINE_STRING
+        "D" SAL_NEWLINE_STRING SAL_NEWLINE_STRING
+        "1. E" SAL_NEWLINE_STRING SAL_NEWLINE_STRING
+        "2. F" SAL_NEWLINE_STRING SAL_NEWLINE_STRING
+        // indent is 3 spaces
+        "   1. G" SAL_NEWLINE_STRING
+        // clang-format on
+    );
+    // Without the accompanying fix in place, this test would have failed, all 
the "- " and "1. "
+    // style prefixes were lost.
+    CPPUNIT_ASSERT_EQUAL(aExpected, aActual);
+}
+
 CPPUNIT_PLUGIN_IMPLEMENT();
 
 /* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s 
cinkeys+=0=break: */
diff --git a/sw/source/filter/md/wrtmd.cxx b/sw/source/filter/md/wrtmd.cxx
index 4021762510df..ec5acca16aa0 100644
--- a/sw/source/filter/md/wrtmd.cxx
+++ b/sw/source/filter/md/wrtmd.cxx
@@ -29,6 +29,7 @@
 #include <sax/tools/converter.hxx>
 #include <svl/itemiter.hxx>
 #include <editeng/fontitem.hxx>
+#include <comphelper/string.hxx>
 
 #include <officecfg/Office/Writer.hxx>
 
@@ -406,8 +407,32 @@ void OutMarkdown_SwTextNode(SwMDWriter& rWrt, const 
SwTextNode& rNode, bool bFir
                 rWrt.Strm().WriteUniOrByteChar('#');
             rWrt.Strm().WriteUniOrByteChar(' ');
         }
+        else if (rNode.GetNumRule())
+        {
+            // <https://spec.commonmark.org/0.31.2/#list-items>, the amount of 
indent we have to use
+            // depends on the parent's prefix size.
+            OUStringBuffer aLevel;
+            auto it = 
rWrt.GetListLevelPrefixSizes().find(rNode.GetActualListLevel() - 1);
+            if (it != rWrt.GetListLevelPrefixSizes().end())
+            {
+                comphelper::string::padToLength(aLevel, it->second, ' ');
+            }
+
+            // In "1." form, should be one of "1." or "1)".
+            OUString aNumString(rNode.GetNumString());
+            if (aNumString.isEmpty() && rNode.HasBullet())
+            {
+                // Should be one of -, +, or *.
+                aNumString = u"-"_ustr;
+            }
 
-        // TODO: handle lists
+            if (!aLevel.isEmpty() || !aNumString.isEmpty())
+            {
+                OUString aPrefix = aLevel + aNumString + " ";
+                rWrt.Strm().WriteUnicodeOrByteText(aPrefix);
+                rWrt.SetListLevelPrefixSize(rNode.GetActualListLevel(), 
aPrefix.getLength());
+            }
+        }
 
         sal_Int32 nStrPos = rWrt.m_pCurrentPam->GetPoint()->GetContentIndex();
         sal_Int32 nEnd = rNodeText.getLength();
@@ -617,6 +642,11 @@ void SwMDWriter::Out_SwDoc(SwPaM* pPam)
     m_bWriteAll = bSaveWriteAll; // reset to old values
 }
 
+void SwMDWriter::SetListLevelPrefixSize(int nListLevel, int nPrefixSize)
+{
+    m_aListLevelPrefixSizes[nListLevel] = nPrefixSize;
+}
+
 void GetMDWriter(std::u16string_view /*rFilterOptions*/, const OUString& 
rBaseURL, WriterRef& xRet)
 {
     xRet = new SwMDWriter(rBaseURL);
diff --git a/sw/source/filter/md/wrtmd.hxx b/sw/source/filter/md/wrtmd.hxx
index 6b26135ea5a5..23490bb96b1b 100644
--- a/sw/source/filter/md/wrtmd.hxx
+++ b/sw/source/filter/md/wrtmd.hxx
@@ -21,6 +21,8 @@
 
 #include <sal/config.h>
 
+#include <map>
+
 #include <rtl/ustring.hxx>
 
 #include <shellio.hxx>
@@ -33,6 +35,8 @@ public:
 
     bool isInTable() const { return m_bOutTable; }
     SwNodeOffset StartNodeIndex() const { return m_nStartNodeIndex; }
+    void SetListLevelPrefixSize(int nListLevel, int nPrefixSize);
+    const std::map<int, int>& GetListLevelPrefixSizes() const { return 
m_aListLevelPrefixSizes; }
 
 protected:
     ErrCode WriteStream() override;
@@ -42,6 +46,8 @@ private:
 
     bool m_bOutTable = false;
     SwNodeOffset m_nStartNodeIndex{ 0 };
+    /// List level -> prefix size map, e.g. "1. " size is 3.
+    std::map<int, int> m_aListLevelPrefixSizes;
 };
 
 /* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s 
cinkeys+=0=break: */
diff --git a/sw/source/uibase/inc/wrtsh.hxx b/sw/source/uibase/inc/wrtsh.hxx
index 0e49e2c889ea..5fbd8d1876c7 100644
--- a/sw/source/uibase/inc/wrtsh.hxx
+++ b/sw/source/uibase/inc/wrtsh.hxx
@@ -340,7 +340,7 @@ typedef bool (SwWrtShell::*FNSimpleMove)();
     */
     void    NumOrBulletOn(bool bNum); // #i29560#
     void    NumOrBulletOff(); // #i29560#
-    void    NumOn();
+    SW_DLLPUBLIC void    NumOn();
     SW_DLLPUBLIC void BulletOn();
 
     //OLE

Reply via email to